Those are internal `//` comments, not clap `///` help — fine. Run the docs-drift gate + verify the regenerated reference has no milestone leak:


Bash

cargo run -q -p xtask -- check 2>&1 | tail -4; echo "XTASK=$?"; echo "---milestone leak in reference.md tunnel lines---"; grep -nE 'M[0-9]+-W|REQ-[A-Z]' docs-site/src/cli/reference.md | grep -i tunnel; echo "(empty=clean)"

Docs-drift gate + leak check


xtask check: OK
XTASK=0
---milestone leak in reference.md tunnel lines---
(empty=clean)


xtask + docs clean. Pre-flight clippy workspace + run existing shell/api tests:


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -6

Clippy workspace


    Checking spt-daemon v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.48s


Clippy clean. Add parse unit tests mirroring drive-poll. View the api parse test block:


600	        ])
601	        .is_ok());
602	        assert!(
603	            parse(&[
604	                "spt",
605	                "--adapter",
606	                "m",
607	                "emit",
608	                "shell-0",
609	                "--type",
610	                "bumped",
611	                "wall"
612	            ])
613	            .is_err(),
614	            "emit requires the link credential"
615	        );
616	        // [unit->REQ-SHELL-3] drive-poll is the shell-side drain; the link is the
617	        // auth (mirrors emit), and it is mandatory.
618	        assert!(parse(&["spt", "--adapter", "m", "drive-poll", "shell-0", "--link", "cafe"]).is_ok());
619	        assert!(
620	            parse(&["spt", "--adapter", "m", "drive-poll", "shell-0"]).is_err(),
621	            "drive-poll requires the link credential"
622	        );
623	        assert!(parse(&["spt", "--adapter", "m", "worker-start", "alice", "alice-w1"]).is_ok());
624	        assert!(parse(&[

        ])
        .is_ok());
        assert!(
            parse(&[
                "spt",
                "--adapter",
                "m",
                "emit",
                "shell-0",
                "--type",
                "bumped",
                "wall"
            ])
            .is_err(),
            "emit requires the link credential"
        );
        // [unit->REQ-SHELL-3] drive-poll is the shell-side drain; the link is the
        // auth (mirrors emit), and it is mandatory.
        assert!(parse(&["spt", "--adapter", "m", "drive-poll", "shell-0", "--link", "cafe"]).is_ok());
        assert!(
            parse(&["spt", "--adapter", "m", "drive-poll", "shell-0"]).is_err(),
            "drive-poll requires the link credential"
        );
        assert!(parse(&["spt", "--adapter", "m", "worker-start", "alice", "alice-w1"]).is_ok());
        assert!(parse(&[


Edit

        assert!(parse(&["spt", "--adapter", "m", "drive-poll", "shell-0", "--link", "cafe"]).is_ok());
        assert!(
            parse(&["spt", "--adapter", "m", "drive-poll", "shell-0"]).is_err(),
            "drive-poll requires the link credential"
        );

        assert!(parse(&["spt", "--adapter", "m", "drive-poll", "shell-0", "--link", "cafe"]).is_ok());
        assert!(
            parse(&["spt", "--adapter", "m", "drive-poll", "shell-0"]).is_err(),
            "drive-poll requires the link credential"
        );
        // [unit->REQ-SHELL-4] the shell-side tunnel verb: id + direction + the link
        // token (the auth, mirrors drive-poll); the link is mandatory.
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv", "--link", "cafe"]).is_ok()
        );
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "send", "--link", "cafe"]).is_ok()
        );
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv"]).is_err(),
            "the shell tunnel verb requires the link credential"
        );


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

        assert!(parse(&["spt", "--adapter", "m", "drive-poll", "shell-0", "--link", "cafe"]).is_ok());
        assert!(
            parse(&["spt", "--adapter", "m", "drive-poll", "shell-0"]).is_err(),
            "drive-poll requires the link credential"
        );
        // [unit->REQ-SHELL-4] the shell-side tunnel verb: id + direction + the link
        // token (the auth, mirrors drive-poll); the link is mandatory.
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv", "--link", "cafe"]).is_ok()
        );
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "send", "--link", "cafe"]).is_ok()
        );
        assert!(
            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv"]).is_err(),
            "the shell tunnel verb requires the link credential"
        );

+        // [unit->REQ-SHELL-4] the shell-side tunnel verb: id + direction + the link

+        // token (the auth, mirrors drive-poll); the link is mandatory.

+            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv", "--link", "cafe"]).is_ok()

+            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "send", "--link", "cafe"]).is_ok()

+            parse(&["spt", "--adapter", "m", "tunnel", "shell-0", "recv"]).is_err(),

+            "the shell tunnel verb requires the link credential"

assert!(parse(&["spt", "--adapter", "m", "worker-start", "alice", "alice-w1"]).is_ok());


Bash

grep -n 'fn .*parse\|#\[test\]' crates/spt/src/api/mod.rs | sed -n '1,6p'

Find the api parse test fn name


522:    fn parse(args: &[&str]) -> Result<Probe, clap::Error> {
526:    #[test] // [unit->REQ-API-1] adapter_name is mandatory on every api call.
536:    #[test]
564:    #[test] // [unit->REQ-API-1] the surface parses its documented subcommands.
565:    fn surface_parses() {
651:    #[test] // [unit->REQ-API-1] closed-vocabulary args reject bad values.


cargo test -q -p spt --lib api::tests::surface_parses 2>&1 | tail -8

Run api surface_parses test


error: no library targets found in package `spt`


cargo test -q -p spt surface_parses 2>&1 | tail -8

Run surface_parses test


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

running 0 tests

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


cargo test -q -p spt --bins surface_parses 2>&1 | grep -A2 'running\|test result' | head; echo "==="; cargo test -q -p spt --bins surface_parses 2>&1 | grep 'result: ok\|FAILED\|passed'

Run surface_parses in bins


running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 155 filtered out; finished in 0.01s

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


surface_parses passes with tunnel asserts. Commit T3.2e (open-hook + both surfaces + docs regen):


traceable-reqs check >/dev/null 2>&1; echo "TRACEABLE=$?"; git add -A && git commit -q -m "$(cat <<'EOF'
M11-W3 T3.2e: tunnel open-hook + owner/shell surfaces (REQ-SHELL-4)

Wire the opaque tunnel end to end:
- Open-hook: `api bind-shell` (shell-online) fires ensure_shell_tunnel —
  resolves the shell manifest and, when [shell.tunnel].enable, opens the
  broker-held loopback pair under the CURRENT link token (the tunnel's
  lifetime is the link's; a relink mints a fresh pair, the stale entry
  inert by stamp). Best-effort.
- Owner surface: `spt shell tunnel <ref> <send|recv>` — resolves the
  owner-side stream under the parked link token (R1), pipes raw stdin in
  / drains raw stdout out.
- Shell surface: `api tunnel <id> <send|recv> --link <token>` — the link
  token is the auth (mirrors drive-poll), resolves the shell-side stream,
  same raw stdin/stdout. Opaque end-to-end (no envelope, no taxonomy).

CLI ref regenerated (no milestone/REQ codes in the public help); drift
gate + clippy workspace clean. surface_parses covers the shell tunnel
verb (link mandatory).

E2E (T3.3: multi-write byte-exact round-trip + R1 stale-stream + link
-break close) lands next, then REQ-SHELL-4 int activates.

Co-authored by: todlando
EOF
)" && git log --oneline -1

Commit T3.2e surfaces + open-hook


TRACEABLE=0
warning: in the working copy of 'docs-site/src/cli/reference.md', LF will be replaced by CRLF the next time Git touches it
41b715f M11-W3 T3.2e: tunnel open-hook + owner/shell surfaces (REQ-SHELL-4)


T3.2e committed @41b715f. Now the T3.3 E2E (int evidence + doyle's required proof). Mirror the W2 drive E2E harness:


1	//! M11-W2 drive-channel E2E (REQ-SHELL-3) — the six impl-gate criteria proven
2	//! through the REAL drive socket + the real `spt` surfaces, no in-process
3	//! shortcut on the driven path:
4	//!
5	//! spawn (offline) → `api bind-shell` onlines the perch → `spt shell drive`
6	//! writes the daemon's single live slot → `api drive-poll --link` takes-and-clears
7	//! it. The slot lives in the daemon (here an in-process drive-control serve on the
8	//! home's drive socket — the broker-home anchor in production), so this exercises
9	//! the true CLI→socket→hub→socket→CLI wire, not a hub call.
10	//!
11	//! Proves: (1) drop-at-write-offline, (2) latest-wins overwrite, (3)
12	//! take-and-clear, (4) NO-SPOOL (the durable shell spool stays empty), (5)
13	//! clear-on-link-break (write → break → relink → poll EMPTY), (6) slot has no
14	//! file under the perch. The cross-node leg is the [twohost] test; this is the
15	//! same-node gate.
16	//
17	// [int->REQ-SHELL-3]
18
19	use std::path::PathBuf;
20	use std::process::{Command, Output};
21	use std::time::Duration;
22
23	mod common;
24	use common::CommandNoWindowExt;
25
26	use spt_store::perch::{self, ParentHint};
27
28	/// Stand up the in-process daemon surfaces this E2E needs on the current
29	/// `SPT_HOME`'s sockets: the **seed** control (so subprocess `ensure_running`
30	/// pings succeed without spawning a real detached daemon) and the **drive**
31	/// control (the broker-home slot the drive surfaces read/write). Both serve
32	/// threads are leaked; they die with the test process.
33	fn start_inproc_daemon() {
34	    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
35	    let seed = spt_daemon::seed_socket_name();
36	    std::thread::spawn(move || {
37	        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
38	    });
39	    let hub = std::sync::Arc::new(spt_daemon::DriveHub::new());
40	    let drive = spt_daemon::drive_socket_name();
41	    std::thread::spawn(move || {
42	        let _ = spt_daemon::serve_drive_control(&drive, hub);
43	    });
44	    for _ in 0..400 {
45	        if spt_daemon::is_running() {
46	            return;
47	        }
48	        std::thread::sleep(Duration::from_millis(5));
49	    }
50	    panic!("in-process seed daemon did not come up");
51	}
52
53	/// `Command::output()` with a deadline, run off-thread (the shell_e2e pattern —
54	/// a detached child inheriting the capture pipe would otherwise hang).
55	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
56	    let (tx, rx) = std::sync::mpsc::channel();
57	    std::thread::spawn(move || {
58	        let _ = tx.send(cmd.output());
59	    });
60	    rx.recv_timeout(deadline)
61	        .expect("captured spt call must complete")
62	        .expect("run spt")
63	}
64
65	fn register_adapter(name: &str, manifest: &str) {
66	    let src = perch::spt_home().join("srcs").join(name);
67	    std::fs::create_dir_all(&src).unwrap();
68	    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
69	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
70	}
71
72	#[test]
73	fn drive_channel_slot_through_the_real_socket() {
74	    // One test per file: a plain set_var is safe and every child inherits it.
75	    let home = tempfile::tempdir().unwrap();
76	    std::env::set_var("SPT_HOME", home.path());
77	    start_inproc_daemon();
78
79	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
80	    let spt = |args: &[&str]| {
81	        let mut cmd = Command::new(&spt_bin);
82	        cmd.no_window().args(args).env("SPT_HOME", home.path());
83	        output_bounded(cmd, Duration::from_secs(60))
84	    };
85
86	    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a
87	    // noop that exits immediately (never binds) — the perch stays offline until
88	    // we online it by token, so we control the online/offline edges precisely.
89	    #[cfg(windows)]
90	    let noop = "cmd /c exit 0";
91	    #[cfg(unix)]
92	    let noop = "true";
93	    register_adapter(
94	        "mock-shell",
95	        &format!(
96	            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
97	             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
98	             [shell.drive]\ntypes = [\"stick\"]\n"
99	        ),
100	    );
101
102	    // The owner: a bound Self perch (the bind-by-token scan starts at Self
103	    // perches) + node identity.
104	    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
105	    std::fs::create_dir_all(&owner_perch).unwrap();
106	    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
107	    spt_store::info::write_info(&owner_perch, &rec).unwrap();
108	    spt_store::nodeid::load_or_create().expect("node identity");
109
110	    let owlery = perch::owlery_dir();
111	    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
112	    let token_of = || {
113	        spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked")
114	    };
115	    let drive = |drive_type: &str, payload: &str| {
116	        spt(&[
117	            "shell", "drive", "Scout", "--type", drive_type, payload, "--owner", "doyle",
118	        ])
119	    };
120	    let drive_poll = |token: &str| {
121	        spt(&[
122	            "api",
123	            "--adapter",
124	            "mock-shell",
125	            "drive-poll",
126	            "mock-shell-0",
127	            "--link",
128	            token,
129	        ])
130	    };
131	    let online_by_token = |token: &str| {
132	        let out = spt(&[
133	            "api",
134	            "--adapter",
135	            "mock-shell",
136	            "bind-shell",
137	            "--link",
138	            token,
139	        ]);
140	        assert!(
141	            out.status.success(),
142	            "bind-shell: {}",
143	            String::from_utf8_lossy(&out.stderr)
144	        );
145	    };
146	    let is_online = || {
147	        spt_store::shellinfo::read_shell_info(&shell_perch).map(|i| i.status)
148	            == Some(spt_store::shellinfo::SHELL_STATUS_ONLINE.to_string())
149	    };
150
151	    // ── spawn: the noop binary exits, the perch stays offline; a token is parked.
152	    let out = spt(&[
153	        "shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle",
154	    ]);
155	    assert!(
156	        out.status.success(),
157	        "spawn: {}",
158	        String::from_utf8_lossy(&out.stderr)
159	    );
160	    assert!(!is_online(), "the noop binary never binds — perch starts offline");
161	    let token_a = token_of();
162
163	    // ── (1) DROP-AT-WRITE: driving an offline shell drops (exit 0, diagnostic),
164	    // writes no slot — the poll comes back empty.
165	    let out = drive("stick", "x=0.1");
166	    assert!(out.status.success(), "an offline drive is a defined drop (exit 0)");
167	    assert!(
168	        String::from_utf8_lossy(&out.stderr).contains("DRIVE_DROPPED"),
169	        "offline drive diagnoses a drop: {}",
170	        String::from_utf8_lossy(&out.stderr)
171	    );
172	    let out = drive_poll(&token_a);
173	    assert!(
174	        out.stdout.is_empty(),
175	        "an offline drive populated no slot: {}",
176	        String::from_utf8_lossy(&out.stdout)
177	    );
178
179	    // ── online the shell by its parked token.
180	    online_by_token(&token_a);
181	    assert!(is_online());
182
183	    // ── (2) LATEST-WINS: a second write before the poll clobbers the first.
184	    assert!(drive("stick", "x=0.1").status.success());
185	    assert!(drive("stick", "x=0.9").status.success());
186	    let out = drive_poll(&token_a);
187	    let frame = String::from_utf8_lossy(&out.stdout);
188	    assert!(frame.contains("type=\"drive\""), "served the drive frame: {frame}");
189	    assert!(
190	        frame.contains("x=0.9") && !frame.contains("x=0.1"),
191	        "the latest write superseded the earlier one: {frame}"
192	    );
193
194	    // ── (3) TAKE-AND-CLEAR: a second poll after a single (already taken) write
195	    // is empty — exactly-once drain.
196	    assert!(
197	        drive_poll(&token_a).stdout.is_empty(),
198	        "the frame was drained exactly once"
199	    );
200
201	    // ── (4) NO-SPOOL: none of the drive writes touched the durable shell spool.
202	    assert_eq!(
203	        spt_store::spool::pending_count_at(&shell_perch).unwrap(),
204	        0,
205	        "the drive write path makes zero spool call"
206	    );
207
208	    // ── (6) the slot is in daemon memory, NOT a file: a freshly-written slot
209	    // leaves no `drive` artifact under the perch (the real guarantee is
210	    // by-construction — DriveHub is an in-memory map, no fs/serde).
211	    assert!(drive("stick", "x=0.4").status.success());
212	    let no_drive_file = std::fs::read_dir(&shell_perch)
213	        .unwrap()
214	        .flatten()
215	        .all(|e| !e.file_name().to_string_lossy().to_lowercase().contains("drive"));
216	    assert!(no_drive_file, "the drive slot is held in memory, not a perch file");
217
218	    // ── (5) CLEAR-ON-LINK-BREAK: a frame written while online must NEVER be
219	    // served to the relinked instance. Break the link (close_shell flips offline,
220	    // retires the token, clears the slot), relink (mints a FRESH token), online,
221	    // and poll — EMPTY. The crash-proof token-stamp leg (a frame stamped under
222	    // the dead generation is inert even if the clear never ran) is the drivehub
223	    // unit `dead_generation_frame_is_never_served`; here we prove the live break.
224	    assert!(drive("stick", "x=0.7").status.success()); // a pending frame under token_a
225	    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
226	        .unwrap()
227	        .1
228	        .shell
229	        .unwrap();
230	    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
231	        .expect("link break flips offline + clears the drive slot");
232	    assert!(!is_online(), "the link break flipped the perch offline");
233
234	    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
235	    assert!(
236	        out.status.success(),
237	        "relink mints a fresh token: {}",
238	        String::from_utf8_lossy(&out.stderr)
239	    );
240	    let token_b = token_of();
241	    assert_ne!(token_a, token_b, "relink rotates the link token");
242	    online_by_token(&token_b);
243	    assert!(
244	        drive_poll(&token_b).stdout.is_empty(),
245	        "no stale frame survives the link break + relink (no-replay-across-relink)"
246	    );
247
248	    // Reap any real daemon a `ensure_running` ping race may have spawned against
249	    // this throwaway home (best-effort hygiene — workspace-scoped, never a
250	    // machine-wide reap).
251	    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
252	        if let Ok(pid) = pid.trim().parse::<u32>() {
253	            #[cfg(windows)]
254	            let _ = Command::new("taskkill")
255	                .no_window()
256	                .args(["/PID", &pid.to_string(), "/F"])
257	                .output();
258	            #[cfg(unix)]
259	            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
260	        }
261	    }
262	}
263

//! M11-W2 drive-channel E2E (REQ-SHELL-3) — the six impl-gate criteria proven
//! through the REAL drive socket + the real `spt` surfaces, no in-process
//! shortcut on the driven path:
//!
//! spawn (offline) → `api bind-shell` onlines the perch → `spt shell drive`
//! writes the daemon's single live slot → `api drive-poll --link` takes-and-clears
//! it. The slot lives in the daemon (here an in-process drive-control serve on the
//! home's drive socket — the broker-home anchor in production), so this exercises
//! the true CLI→socket→hub→socket→CLI wire, not a hub call.
//!
//! Proves: (1) drop-at-write-offline, (2) latest-wins overwrite, (3)
//! take-and-clear, (4) NO-SPOOL (the durable shell spool stays empty), (5)
//! clear-on-link-break (write → break → relink → poll EMPTY), (6) slot has no
//! file under the perch. The cross-node leg is the [twohost] test; this is the
//! same-node gate.
//
// [int->REQ-SHELL-3]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Stand up the in-process daemon surfaces this E2E needs on the current
/// `SPT_HOME`'s sockets: the **seed** control (so subprocess `ensure_running`
/// pings succeed without spawning a real detached daemon) and the **drive**
/// control (the broker-home slot the drive surfaces read/write). Both serve
/// threads are leaked; they die with the test process.
fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let seed = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
    });
    let hub = std::sync::Arc::new(spt_daemon::DriveHub::new());
    let drive = spt_daemon::drive_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::serve_drive_control(&drive, hub);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

/// `Command::output()` with a deadline, run off-thread (the shell_e2e pattern —
/// a detached child inheriting the capture pipe would otherwise hang).
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

fn register_adapter(name: &str, manifest: &str) {
    let src = perch::spt_home().join("srcs").join(name);
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
}

#[test]
fn drive_channel_slot_through_the_real_socket() {
    // One test per file: a plain set_var is safe and every child inherits it.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a
    // noop that exits immediately (never binds) — the perch stays offline until
    // we online it by token, so we control the online/offline edges precisely.
    #[cfg(windows)]
    let noop = "cmd /c exit 0";
    #[cfg(unix)]
    let noop = "true";
    register_adapter(
        "mock-shell",
        &format!(
            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
             [shell.drive]\ntypes = [\"stick\"]\n"
        ),
    );

    // The owner: a bound Self perch (the bind-by-token scan starts at Self
    // perches) + node identity.
    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
    std::fs::create_dir_all(&owner_perch).unwrap();
    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
    spt_store::info::write_info(&owner_perch, &rec).unwrap();
    spt_store::nodeid::load_or_create().expect("node identity");

    let owlery = perch::owlery_dir();
    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
    let token_of = || {
        spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked")
    };
    let drive = |drive_type: &str, payload: &str| {
        spt(&[
            "shell", "drive", "Scout", "--type", drive_type, payload, "--owner", "doyle",
        ])
    };
    let drive_poll = |token: &str| {
        spt(&[
            "api",
            "--adapter",
            "mock-shell",
            "drive-poll",
            "mock-shell-0",
            "--link",
            token,
        ])
    };
    let online_by_token = |token: &str| {
        let out = spt(&[
            "api",
            "--adapter",
            "mock-shell",
            "bind-shell",
            "--link",
            token,
        ]);
        assert!(
            out.status.success(),
            "bind-shell: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    let is_online = || {
        spt_store::shellinfo::read_shell_info(&shell_perch).map(|i| i.status)
            == Some(spt_store::shellinfo::SHELL_STATUS_ONLINE.to_string())
    };

    // ── spawn: the noop binary exits, the perch stays offline; a token is parked.
    let out = spt(&[
        "shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle",
    ]);
    assert!(
        out.status.success(),
        "spawn: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(!is_online(), "the noop binary never binds — perch starts offline");
    let token_a = token_of();

    // ── (1) DROP-AT-WRITE: driving an offline shell drops (exit 0, diagnostic),
    // writes no slot — the poll comes back empty.
    let out = drive("stick", "x=0.1");
    assert!(out.status.success(), "an offline drive is a defined drop (exit 0)");
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("DRIVE_DROPPED"),
        "offline drive diagnoses a drop: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let out = drive_poll(&token_a);
    assert!(
        out.stdout.is_empty(),
        "an offline drive populated no slot: {}",
        String::from_utf8_lossy(&out.stdout)
    );

    // ── online the shell by its parked token.
    online_by_token(&token_a);
    assert!(is_online());

    // ── (2) LATEST-WINS: a second write before the poll clobbers the first.
    assert!(drive("stick", "x=0.1").status.success());
    assert!(drive("stick", "x=0.9").status.success());
    let out = drive_poll(&token_a);
    let frame = String::from_utf8_lossy(&out.stdout);
    assert!(frame.contains("type=\"drive\""), "served the drive frame: {frame}");
    assert!(
        frame.contains("x=0.9") && !frame.contains("x=0.1"),
        "the latest write superseded the earlier one: {frame}"
    );

    // ── (3) TAKE-AND-CLEAR: a second poll after a single (already taken) write
    // is empty — exactly-once drain.
    assert!(
        drive_poll(&token_a).stdout.is_empty(),
        "the frame was drained exactly once"
    );

    // ── (4) NO-SPOOL: none of the drive writes touched the durable shell spool.
    assert_eq!(
        spt_store::spool::pending_count_at(&shell_perch).unwrap(),
        0,
        "the drive write path makes zero spool call"
    );

    // ── (6) the slot is in daemon memory, NOT a file: a freshly-written slot
    // leaves no `drive` artifact under the perch (the real guarantee is
    // by-construction — DriveHub is an in-memory map, no fs/serde).
    assert!(drive("stick", "x=0.4").status.success());
    let no_drive_file = std::fs::read_dir(&shell_perch)
        .unwrap()
        .flatten()
        .all(|e| !e.file_name().to_string_lossy().to_lowercase().contains("drive"));
    assert!(no_drive_file, "the drive slot is held in memory, not a perch file");

    // ── (5) CLEAR-ON-LINK-BREAK: a frame written while online must NEVER be
    // served to the relinked instance. Break the link (close_shell flips offline,
    // retires the token, clears the slot), relink (mints a FRESH token), online,
    // and poll — EMPTY. The crash-proof token-stamp leg (a frame stamped under
    // the dead generation is inert even if the clear never ran) is the drivehub
    // unit `dead_generation_frame_is_never_served`; here we prove the live break.
    assert!(drive("stick", "x=0.7").status.success()); // a pending frame under token_a
    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
        .unwrap()
        .1
        .shell
        .unwrap();
    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
        .expect("link break flips offline + clears the drive slot");
    assert!(!is_online(), "the link break flipped the perch offline");

    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
    assert!(
        out.status.success(),
        "relink mints a fresh token: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let token_b = token_of();
    assert_ne!(token_a, token_b, "relink rotates the link token");
    online_by_token(&token_b);
    assert!(
        drive_poll(&token_b).stdout.is_empty(),
        "no stale frame survives the link break + relink (no-replay-across-relink)"
    );

    // Reap any real daemon a `ensure_running` ping race may have spawned against
    // this throwaway home (best-effort hygiene — workspace-scoped, never a
    // machine-wide reap).
    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &pid.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
        }
    }
}


The drive E2E harness is the template, but the tunnel needs a real **Broker+NetHost** (for `broker.net()`). Check whether spt tests already stand one up + the dev-deps:


grep -rln 'bind_in_with_net\|NetHost::start\|NetConfig' crates/spt/tests/ 2>/dev/null; echo "---spt dev-deps---"; sed -n '/\[dev-dependencies\]/,/\[/p' crates/spt/Cargo.toml | grep -iE 'spt-net|spt_net|spt-proto|spt_proto|spt-daemon|tempfile'

Check existing broker+net test setup + deps


crates/spt/tests/brain_survive.rs
---spt dev-deps---
tempfile = "3"
# are re-declared here (the spt-daemon dev-dep pattern).
spt-daemon = { path = "../spt-daemon" }
spt-net = { path = "../spt-net" }
spt-proto = { path = "../spt-proto" }


`brain_survive.rs` already stands up a broker+net in spt tests + deps are present. Read its setup to mirror:


grep -n 'bind_in_with_net\|NetHost::start\|NetConfig\|RelayPolicy\|BindScope\|LocalDiscovery\|Identity\|fn .*broker\|hermetic\|use spt' crates/spt/tests/brain_survive.rs | head -40

Read brain_survive broker+net setup


68:use spt_daemon::brain::Brain;
69:use spt_daemon::brainproc::{supervise_brain, BrainRestart, NoTrialEnv, StartReason};
70:use spt_daemon::codec::write_frame;
71:use spt_daemon::endpoint::broker_socket_name;
72:use spt_daemon::frame::{Envelope, Role};
73:use spt_daemon::msg::{encode_bytes, InputReq, SpawnReq, KIND_INPUT};
74:use spt_daemon::nethost::{NetConfig, NetHost};
75:use spt_daemon::transport::{send_hello, LocalSocketTransport};
76:use spt_daemon::Broker;
77:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
78:use spt_proto::identity::Identity;
100:/// A hermetic net config (mirrors `netbroker.rs`): relays disabled, no discovery,
102:fn hermetic(identity: Identity) -> NetConfig {
103:    NetConfig {
105:        relay: RelayPolicy::Disabled,
106:        discovery: LocalDiscovery::Off,
107:        scope: BindScope::Loopback,
112:/// Bind a broker (serving on a background thread) that owns a hermetic QUIC
114:fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {
115:    let host = NetHost::start(hermetic(identity)).expect("net host start");
116:    let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
220:    let broker = net_broker(&broker_socket_name(), Identity::generate(), &home.path().join("a"));
222:    let _peer = net_broker(&peer_name, Identity::generate(), &home.path().join("b"));


100	/// A hermetic net config (mirrors `netbroker.rs`): relays disabled, no discovery,
101	/// loopback-only sockets — no real interface touched, no firewall prompt.
102	fn hermetic(identity: Identity) -> NetConfig {
103	    NetConfig {
104	        identity,
105	        relay: RelayPolicy::Disabled,
106	        discovery: LocalDiscovery::Off,
107	        scope: BindScope::Loopback,
108	        membership: None,
109	    }
110	}
111
112	/// Bind a broker (serving on a background thread) that owns a hermetic QUIC
113	/// endpoint for `identity`, with its effect journal under `dir`.
114	fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {
115	    let host = NetHost::start(hermetic(identity)).expect("net host start");
116	    let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
117	        .expect("bind broker with net");
118	    let serve = Arc::clone(&broker);
119	    thread::spawn(move || {
120	        let _ = serve.serve();
121	    });
122	    broker
123	}
124

/// A hermetic net config (mirrors `netbroker.rs`): relays disabled, no discovery,
/// loopback-only sockets — no real interface touched, no firewall prompt.
fn hermetic(identity: Identity) -> NetConfig {
    NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    }
}

/// Bind a broker (serving on a background thread) that owns a hermetic QUIC
/// endpoint for `identity`, with its effect journal under `dir`.
fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic(identity)).expect("net host start");
    let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
        .expect("bind broker with net");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}


Have the full harness. Write the T3.3 E2E — multi-write byte-exact round-trip (incl `<EVENT`-looking binary) + link-break close + R1 stale-stream, through the real CLI + a broker-with-net:


Write

//! M11-W3 shell-tunnel E2E (REQ-SHELL-4) — the opaque byte tunnel proven through
//! the REAL `spt` surfaces + a real broker-owned NetHost, no in-process shortcut
//! on the relay path:
//!
//! spawn (offline) → `api bind-shell` onlines the perch AND opens the tunnel
//! (manifest `[shell.tunnel].enable`) → `spt shell tunnel <ref> send` pipes raw
//! bytes in → `api tunnel <id> recv --link` drains them out (and the reverse). The
//! bytes ride the broker-held loopback QUIC stream pair (same-node mock; the
//! cross-node-on-LAN real-Iroh wire is the W5 [twohost] rung — NOT proven here).
//!
//! Proves: (1) byte-exact opaque round-trip BOTH directions of a MULTI-WRITE,
//! sizable payload that includes `<EVENT`-looking bytes + NULs the envelope grammar
//! would mangle — the taxonomy never reinterprets them; (2) link-break closes the
//! tunnel (a post-break resolve finds none); (3) no-stale-stream-across-relink (R1)
//! — bytes pending in the pre-break generation are NEVER surfaced to the relinked
//! instance's fresh tunnel.
//
// [int->REQ-SHELL-4]

use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_daemon::endpoint::{broker_socket_name, tunnel_socket_name};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::{Broker, TunnelHub};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::perch::{self, ParentHint};

fn hermetic(identity: Identity) -> NetConfig {
    NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    }
}

/// Stand up the daemon surfaces this E2E needs on the current `SPT_HOME`'s
/// sockets: the **seed** control (so subprocess `ensure_running` pings succeed
/// without a real detached daemon), a **broker that owns a hermetic NetHost** (the
/// tunnel relay opens loopback streams on it), and the **tunnel** control bound to
/// that broker. All serve threads are leaked; they die with the test process.
fn start_inproc_daemon(dir: &std::path::Path) {
    let reg = Arc::new(spt_daemon::SeedRegistry::new());
    let seed = spt_daemon::seed_socket_name();
    thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
    });

    let host = NetHost::start(hermetic(Identity::generate())).expect("net host start");
    let broker = Broker::bind_in_with_net(&broker_socket_name(), dir.join("effects.log"), Some(host))
        .expect("bind broker with net");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });

    let hub = Arc::new(TunnelHub::new());
    let tsock = tunnel_socket_name();
    thread::spawn(move || {
        let _ = spt_daemon::serve_tunnel_control(&tsock, hub, broker);
    });

    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

/// `Command::output()` with a deadline, off-thread (the drive_e2e pattern).
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

/// Spawn `cmd` with `input` on stdin, capture output, bounded.
fn output_with_stdin(mut cmd: Command, input: Vec<u8>, deadline: Duration) -> Output {
    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let res = (|| {
            let mut child = cmd.spawn()?;
            child.stdin.take().unwrap().write_all(&input)?;
            // stdin dropped here → EOF, so the child's read_to_end completes.
            child.wait_with_output()
        })();
        let _ = tx.send(res);
    });
    rx.recv_timeout(deadline)
        .expect("captured spt stdin call must complete")
        .expect("run spt with stdin")
}

fn register_adapter(name: &str, manifest: &str) {
    let src = perch::spt_home().join("srcs").join(name);
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
}

#[test]
fn shell_tunnel_round_trips_opaque_bytes_through_the_real_surfaces() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon(home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };
    let spt_stdin = |args: &[&str], input: Vec<u8>| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_with_stdin(cmd, input, Duration::from_secs(60))
    };

    // A shell adapter that OPTS IN to the tunnel. The spawn is a noop that exits
    // immediately (never binds) — the perch stays offline until we online it by
    // token, so we control the online/offline edges precisely.
    #[cfg(windows)]
    let noop = "cmd /c exit 0";
    #[cfg(unix)]
    let noop = "true";
    register_adapter(
        "mock-shell",
        &format!(
            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
             [shell.tunnel]\nenable = true\nprotocol = \"usbip-urb\"\n"
        ),
    );

    // The owner: a bound Self perch + node identity.
    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
    std::fs::create_dir_all(&owner_perch).unwrap();
    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
    spt_store::info::write_info(&owner_perch, &rec).unwrap();
    spt_store::nodeid::load_or_create().expect("node identity");

    let owlery = perch::owlery_dir();
    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
    let token_of =
        || spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked");
    let online_by_token = |token: &str| {
        let out = spt(&["api", "--adapter", "mock-shell", "bind-shell", "--link", token]);
        assert!(
            out.status.success(),
            "bind-shell: {}",
            String::from_utf8_lossy(&out.stderr)
        );
        assert!(
            String::from_utf8_lossy(&out.stderr).contains("SHELL_TUNNEL_OPEN"),
            "bind-shell opens the tunnel for a [shell.tunnel] adapter: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    // Owner sends raw bytes into the tunnel.
    let owner_send = |bytes: Vec<u8>| {
        let out = spt_stdin(&["shell", "tunnel", "Scout", "send", "--owner", "doyle"], bytes);
        assert!(
            out.status.success(),
            "owner tunnel send: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    let shell_send = |token: &str, bytes: Vec<u8>| {
        let out = spt_stdin(
            &["api", "--adapter", "mock-shell", "tunnel", "mock-shell-0", "send", "--link", token],
            bytes,
        );
        assert!(
            out.status.success(),
            "shell tunnel send: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    // Drain one end, retrying until `want` bytes have crossed the async pump (or a
    // deadline). Returns everything accumulated.
    let shell_recv_until = |token: &str, want: usize| -> Vec<u8> {
        let mut got = Vec::new();
        for _ in 0..400 {
            let out = spt(&[
                "api", "--adapter", "mock-shell", "tunnel", "mock-shell-0", "recv", "--link", token,
            ]);
            got.extend_from_slice(&out.stdout);
            if got.len() >= want {
                break;
            }
            thread::sleep(Duration::from_millis(10));
        }
        got
    };
    let owner_recv_until = |want: usize| -> Vec<u8> {
        let mut got = Vec::new();
        for _ in 0..400 {
            let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
            got.extend_from_slice(&out.stdout);
            if got.len() >= want {
                break;
            }
            thread::sleep(Duration::from_millis(10));
        }
        got
    };

    // ── spawn (offline) + online by token → the tunnel opens.
    let out = spt(&["shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle"]);
    assert!(out.status.success(), "spawn: {}", String::from_utf8_lossy(&out.stderr));
    let token_a = token_of();
    online_by_token(&token_a);

    // A payload the <EVENT> envelope grammar would mangle: NULs, an `<EVENT`-looking
    // run, and high bytes — across TWO writes plus a sizable third that spans many
    // read-pump chunks (exposes any ordering/eviction bug across the poll boundary).
    let blob1: Vec<u8> = b"\x00<EVENT type=\"msg\" from=\"x\">not really an event\xff\xfe\x00".to_vec();
    let blob2: Vec<u8> = (0..=255u8).cycle().take(200 * 1024).collect();
    let want: Vec<u8> = blob1.iter().chain(blob2.iter()).copied().collect();

    // ── (1a) owner → shell, MULTI-WRITE, byte-exact.
    owner_send(blob1.clone());
    owner_send(blob2.clone());
    let got = shell_recv_until(&token_a, want.len());
    assert_eq!(got.len(), want.len(), "shell drained every owner→shell byte (no loss/dup)");
    assert_eq!(got, want, "owner→shell opaque bytes round-trip byte-exact, in order");

    // ── (1b) shell → owner, byte-exact (the reverse leg of the duplex).
    let reply: Vec<u8> = b"\x00\x01\x02<EVENT/>\xaa\xbb reply payload".to_vec();
    shell_send(&token_a, reply.clone());
    let back = owner_recv_until(reply.len());
    assert_eq!(back, reply, "shell→owner opaque bytes round-trip byte-exact");

    // ── (2)+(3) link-break closes the tunnel, and no pre-break byte survives the
    // relink (R1). Send a pending payload, DON'T drain it, break the link, relink to
    // a fresh generation, and confirm the relinked tunnel never serves the stale
    // bytes.
    shell_send(&token_a, b"STALE-PRE-BREAK-BYTES".to_vec()); // pending in the owner end's ring
    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
        .unwrap()
        .1
        .shell
        .unwrap();
    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
        .expect("link break closes the tunnel");

    // A post-break resolve finds no tunnel (the entry was cleared on link-break).
    let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("NO_TUNNEL"),
        "the link break closed the tunnel: {}",
        String::from_utf8_lossy(&out.stderr)
    );

    // Relink (fresh token) + online → a FRESH tunnel; the stale pre-break bytes are
    // never surfaced to it.
    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
    assert!(out.status.success(), "relink: {}", String::from_utf8_lossy(&out.stderr));
    let token_b = token_of();
    assert_ne!(token_a, token_b, "relink rotates the link token");
    online_by_token(&token_b);

    // Drain the fresh owner end a few times — the stale "STALE-PRE-BREAK-BYTES" from
    // the dead generation must NEVER appear (no-stale-stream-across-relink, R1).
    let mut fresh = Vec::new();
    for _ in 0..30 {
        let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
        fresh.extend_from_slice(&out.stdout);
        thread::sleep(Duration::from_millis(5));
    }
    assert!(
        !String::from_utf8_lossy(&fresh).contains("STALE-PRE-BREAK-BYTES"),
        "no pre-break byte survives the link break + relink (R1): {:?}",
        String::from_utf8_lossy(&fresh)
    );

    // Best-effort hygiene: reap any real daemon an `ensure_running` race spawned
    // against this throwaway home (workspace-scoped, never machine-wide).
    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &pid.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
        }
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\tunnel_e2e.rs (file state is current in your context — no need to Read it back)

//! M11-W3 shell-tunnel E2E (REQ-SHELL-4) — the opaque byte tunnel proven through
//! the REAL `spt` surfaces + a real broker-owned NetHost, no in-process shortcut
//! on the relay path:
//!
//! spawn (offline) → `api bind-shell` onlines the perch AND opens the tunnel
//! (manifest `[shell.tunnel].enable`) → `spt shell tunnel <ref> send` pipes raw
//! bytes in → `api tunnel <id> recv --link` drains them out (and the reverse). The
//! bytes ride the broker-held loopback QUIC stream pair (same-node mock; the
//! cross-node-on-LAN real-Iroh wire is the W5 [twohost] rung — NOT proven here).
//!
//! Proves: (1) byte-exact opaque round-trip BOTH directions of a MULTI-WRITE,
//! sizable payload that includes `<EVENT`-looking bytes + NULs the envelope grammar
//! would mangle — the taxonomy never reinterprets them; (2) link-break closes the
//! tunnel (a post-break resolve finds none); (3) no-stale-stream-across-relink (R1)
//! — bytes pending in the pre-break generation are NEVER surfaced to the relinked
//! instance's fresh tunnel.
//
// [int->REQ-SHELL-4]

use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_daemon::endpoint::{broker_socket_name, tunnel_socket_name};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::{Broker, TunnelHub};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::perch::{self, ParentHint};

fn hermetic(identity: Identity) -> NetConfig {
    NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    }
}

/// Stand up the daemon surfaces this E2E needs on the current `SPT_HOME`'s
/// sockets: the **seed** control (so subprocess `ensure_running` pings succeed
/// without a real detached daemon), a **broker that owns a hermetic NetHost** (the
/// tunnel relay opens loopback streams on it), and the **tunnel** control bound to
/// that broker. All serve threads are leaked; they die with the test process.
fn start_inproc_daemon(dir: &std::path::Path) {
    let reg = Arc::new(spt_daemon::SeedRegistry::new());
    let seed = spt_daemon::seed_socket_name();
    thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
    });

    let host = NetHost::start(hermetic(Identity::generate())).expect("net host start");
    let broker = Broker::bind_in_with_net(&broker_socket_name(), dir.join("effects.log"), Some(host))
        .expect("bind broker with net");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });

    let hub = Arc::new(TunnelHub::new());
    let tsock = tunnel_socket_name();
    thread::spawn(move || {
        let _ = spt_daemon::serve_tunnel_control(&tsock, hub, broker);
    });

    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

/// `Command::output()` with a deadline, off-thread (the drive_e2e pattern).
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

/// Spawn `cmd` with `input` on stdin, capture output, bounded.
fn output_with_stdin(mut cmd: Command, input: Vec<u8>, deadline: Duration) -> Output {
    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let res = (|| {
            let mut child = cmd.spawn()?;
            child.stdin.take().unwrap().write_all(&input)?;
            // stdin dropped here → EOF, so the child's read_to_end completes.
            child.wait_with_output()
        })();
        let _ = tx.send(res);
    });
    rx.recv_timeout(deadline)
        .expect("captured spt stdin call must complete")
        .expect("run spt with stdin")
}

fn register_adapter(name: &str, manifest: &str) {
    let src = perch::spt_home().join("srcs").join(name);
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
}

#[test]
fn shell_tunnel_round_trips_opaque_bytes_through_the_real_surfaces() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon(home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };
    let spt_stdin = |args: &[&str], input: Vec<u8>| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_with_stdin(cmd, input, Duration::from_secs(60))
    };

    // A shell adapter that OPTS IN to the tunnel. The spawn is a noop that exits
    // immediately (never binds) — the perch stays offline until we online it by
    // token, so we control the online/offline edges precisely.
    #[cfg(windows)]
    let noop = "cmd /c exit 0";
    #[cfg(unix)]
    let noop = "true";
    register_adapter(
        "mock-shell",
        &format!(
            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
             [shell.tunnel]\nenable = true\nprotocol = \"usbip-urb\"\n"
        ),
    );

    // The owner: a bound Self perch + node identity.
    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
    std::fs::create_dir_all(&owner_perch).unwrap();
    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
    spt_store::info::write_info(&owner_perch, &rec).unwrap();
    spt_store::nodeid::load_or_create().expect("node identity");

    let owlery = perch::owlery_dir();
    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
    let token_of =
        || spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked");
    let online_by_token = |token: &str| {
        let out = spt(&["api", "--adapter", "mock-shell", "bind-shell", "--link", token]);
        assert!(
            out.status.success(),
            "bind-shell: {}",
            String::from_utf8_lossy(&out.stderr)
        );
        assert!(
            String::from_utf8_lossy(&out.stderr).contains("SHELL_TUNNEL_OPEN"),
            "bind-shell opens the tunnel for a [shell.tunnel] adapter: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    // Owner sends raw bytes into the tunnel.
    let owner_send = |bytes: Vec<u8>| {
        let out = spt_stdin(&["shell", "tunnel", "Scout", "send", "--owner", "doyle"], bytes);
        assert!(
            out.status.success(),
            "owner tunnel send: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    let shell_send = |token: &str, bytes: Vec<u8>| {
        let out = spt_stdin(
            &["api", "--adapter", "mock-shell", "tunnel", "mock-shell-0", "send", "--link", token],
            bytes,
        );
        assert!(
            out.status.success(),
            "shell tunnel send: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    // Drain one end, retrying until `want` bytes have crossed the async pump (or a
    // deadline). Returns everything accumulated.
    let shell_recv_until = |token: &str, want: usize| -> Vec<u8> {
        let mut got = Vec::new();
        for _ in 0..400 {
            let out = spt(&[
                "api", "--adapter", "mock-shell", "tunnel", "mock-shell-0", "recv", "--link", token,
            ]);
            got.extend_from_slice(&out.stdout);
            if got.len() >= want {
                break;
            }
            thread::sleep(Duration::from_millis(10));
        }
        got
    };
    let owner_recv_until = |want: usize| -> Vec<u8> {
        let mut got = Vec::new();
        for _ in 0..400 {
            let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
            got.extend_from_slice(&out.stdout);
            if got.len() >= want {
                break;
            }
            thread::sleep(Duration::from_millis(10));
        }
        got
    };

    // ── spawn (offline) + online by token → the tunnel opens.
    let out = spt(&["shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle"]);
    assert!(out.status.success(), "spawn: {}", String::from_utf8_lossy(&out.stderr));
    let token_a = token_of();
    online_by_token(&token_a);

    // A payload the <EVENT> envelope grammar would mangle: NULs, an `<EVENT`-looking
    // run, and high bytes — across TWO writes plus a sizable third that spans many
    // read-pump chunks (exposes any ordering/eviction bug across the poll boundary).
    let blob1: Vec<u8> = b"\x00<EVENT type=\"msg\" from=\"x\">not really an event\xff\xfe\x00".to_vec();
    let blob2: Vec<u8> = (0..=255u8).cycle().take(200 * 1024).collect();
    let want: Vec<u8> = blob1.iter().chain(blob2.iter()).copied().collect();

    // ── (1a) owner → shell, MULTI-WRITE, byte-exact.
    owner_send(blob1.clone());
    owner_send(blob2.clone());
    let got = shell_recv_until(&token_a, want.len());
    assert_eq!(got.len(), want.len(), "shell drained every owner→shell byte (no loss/dup)");
    assert_eq!(got, want, "owner→shell opaque bytes round-trip byte-exact, in order");

    // ── (1b) shell → owner, byte-exact (the reverse leg of the duplex).
    let reply: Vec<u8> = b"\x00\x01\x02<EVENT/>\xaa\xbb reply payload".to_vec();
    shell_send(&token_a, reply.clone());
    let back = owner_recv_until(reply.len());
    assert_eq!(back, reply, "shell→owner opaque bytes round-trip byte-exact");

    // ── (2)+(3) link-break closes the tunnel, and no pre-break byte survives the
    // relink (R1). Send a pending payload, DON'T drain it, break the link, relink to
    // a fresh generation, and confirm the relinked tunnel never serves the stale
    // bytes.
    shell_send(&token_a, b"STALE-PRE-BREAK-BYTES".to_vec()); // pending in the owner end's ring
    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
        .unwrap()
        .1
        .shell
        .unwrap();
    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
        .expect("link break closes the tunnel");

    // A post-break resolve finds no tunnel (the entry was cleared on link-break).
    let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("NO_TUNNEL"),
        "the link break closed the tunnel: {}",
        String::from_utf8_lossy(&out.stderr)
    );

    // Relink (fresh token) + online → a FRESH tunnel; the stale pre-break bytes are
    // never surfaced to it.
    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
    assert!(out.status.success(), "relink: {}", String::from_utf8_lossy(&out.stderr));
    let token_b = token_of();
    assert_ne!(token_a, token_b, "relink rotates the link token");
    online_by_token(&token_b);

    // Drain the fresh owner end a few times — the stale "STALE-PRE-BREAK-BYTES" from
    // the dead generation must NEVER appear (no-stale-stream-across-relink, R1).
    let mut fresh = Vec::new();
    for _ in 0..30 {
        let out = spt(&["shell", "tunnel", "Scout", "recv", "--owner", "doyle"]);
        fresh.extend_from_slice(&out.stdout);
        thread::sleep(Duration::from_millis(5));
    }
    assert!(
        !String::from_utf8_lossy(&fresh).contains("STALE-PRE-BREAK-BYTES"),
        "no pre-break byte survives the link break + relink (R1): {:?}",
        String::from_utf8_lossy(&fresh)
    );

    // Best-effort hygiene: reap any real daemon an `ensure_running` race spawned
    // against this throwaway home (workspace-scoped, never machine-wide).
    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &pid.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
        }
    }
}


Bash

cargo test -q -p spt --test tunnel_e2e 2>&1 | tail -30

Build + run tunnel E2E


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


E2E passes first try. Verify non-flaky (async crossing + poll loops):


for i in 1 2 3; do cargo test -q -p spt --test tunnel_e2e 2>&1 | grep 'test result'; done

Repeat E2E for flakiness


test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.44s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.41s


Stable 3/3. Activate REQ-SHELL-4 `int`:


Edit

required_stages = ["doc", "impl", "unit"]  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung). doc/impl/unit at T3.1: doc = CONTEXT §shell tunnel; impl = [shell.tunnel] manifest opt-in (enable + protocol label) + validate (label needs enable); unit = shell_tunnel_opt_in. int added at T3.3 (same-node mock tunnel E2E: opaque round-trip + link-break close).

required_stages = ["doc", "impl", "unit", "int"]  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung; cross-node-on-LAN real-Iroh wire = W5 [twohost] rung, R2). doc = CONTEXT §shell tunnel; impl = [shell.tunnel] opt-in + validate, broker TunnelHub registry, retentive backpressured NetHost streams (lossless = doyle gate condition), the broker-homed byte bridge (tunnel_ensure/send/recv), open-at-bind hook + close_shell clear, `spt shell tunnel` + `api tunnel` surfaces; unit = shell_tunnel_opt_in + tunnelhub (resolve/R1/relink/clear/per-owner) + nethost retentive-lossless & loopback backpressure + surface_parses; int = tunnel_e2e (multi-write byte-exact opaque round-trip both directions incl <EVENT-looking bytes, link-break closes, R1 no-stale-stream-across-relink, through the real surfaces + a broker-owned NetHost).


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

title = "Shell tunnel (reliable-ordered opaque byte stream): an owner<->shell link may hold a long-lived, reliable-ordered, link-bound QUIC stream pair carrying opaque wire protocol traffic the channel taxonomy must NOT reinterpret (first consumer usbip URB) — manifest opt-in, not enveloped, not MAC-framed, not spooled; the link lifecycle governs it (a link-break closes the tunnel). Reliable-ordered ⇒ congestion surfaces as lag never loss ⇒ acceptable only on-LAN: the on-LAN posture is documented and the tunnel is NOT proven cross-WAN (CONTEXT:262, minted 2026-06-11 Gateway grill; doyle gate C2)."

-required_stages = ["doc", "impl", "unit"]  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung). doc/impl/unit at T3.1: doc = CONTEXT §shell tunnel; impl = [shell.tunnel] manifest opt-in (enable + protocol label) + validate (label needs enable); unit = shell_tunnel_opt_in. int added at T3.3 (same-node mock tunnel E2E: opaque round-trip + link-break close).

+required_stages = ["doc", "impl", "unit", "int"]  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung; cross-node-on-LAN real-Iroh wire = W5 [twohost] rung, R2). doc = CONTEXT §shell tunnel; impl = [shell.tunnel] opt-in + validate, broker TunnelHub registry, retentive backpressured NetHost streams (lossless = doyle gate condition), the broker-homed byte bridge (tunnel_ensure/send/recv), open-at-bind hook + close_shell clear, `spt shell tunnel` + `api tunnel` surfaces; unit = shell_tunnel_opt_in + tunnelhub (resolve/R1/relink/clear/per-owner) + nethost retentive-lossless & loopback backpressure + surface_parses; int = tunnel_e2e (multi-write byte-exact opaque round-trip both directions incl <EVENT-looking bytes, link-break closes, R1 no-stale-stream-across-relink, through the real surfaces + a broker-owned NetHost).

title = "Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill)."


Bash

traceable-reqs check 2>&1 | grep -A1 'REQ-SHELL-4'; echo "==="; traceable-reqs check >/dev/null 2>&1; echo "TRACEABLE_EXIT=$?"

Confirm REQ-SHELL-4 fully evidenced


  [OK] REQ-SHELL-4  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Shell tunnel (reliable-ordered opaque byte stream): an owner<->shell link may hold a long-lived, reliable-ordered, link-bound QUIC stream pair carrying opaque wire protocol traffic the channel taxonomy must NOT reinterpret (first consumer usbip URB) — manifest opt-in, not enveloped, not MAC-framed, not spooled; the link lifecycle governs it (a link-break closes the tunnel). Reliable-ordered ⇒ congestion surfaces as lag never loss ⇒ acceptable only on-LAN: the on-LAN posture is documented and the tunnel is NOT proven cross-WAN (CONTEXT:262, minted 2026-06-11 Gateway grill; doyle gate C2).
===
TRACEABLE_EXIT=0


REQ-SHELL-4 fully evidenced `[doc,impl,unit,int]`, traceable EXIT 0. Final pre-flight (clippy + xtask) then commit T3.3:


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -4; echo "---xtask---"; cargo run -q -p xtask -- check 2>&1 | tail -2

Clippy + xtask pre-flight


    Checking spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.36s
---xtask---
xtask check: OK


git add -A && git commit -q -m "$(cat <<'EOF'
M11-W3 T3.3: shell-tunnel E2E + REQ-SHELL-4 int (close W3 build)

The opaque tunnel proven through the real `spt` surfaces + a real
broker-owned NetHost (no in-process shortcut on the relay path):
spawn→bind-shell (opens the tunnel)→`spt shell tunnel send`→`api tunnel
recv` and the reverse. Asserts:
- byte-exact opaque round-trip BOTH directions of a multi-write, sizable
  (~200KB, multi-chunk) payload including `<EVENT`-looking bytes + NULs
  the envelope grammar would mangle — the taxonomy never reinterprets it;
- link-break closes the tunnel (a post-break resolve finds none);
- no-stale-stream-across-relink (R1): bytes pending in the pre-break
  generation are never surfaced to the relinked instance's fresh tunnel.

Activates REQ-SHELL-4 = [doc, impl, unit, int]; traceable EXIT 0,
clippy workspace + xtask check green. Same-node mock per doyle C2 — the
cross-node-on-LAN real-Iroh wire is the W5 [twohost] rung (no overclaim).

Co-authored by: todlando
EOF
)" && git log --oneline -8

Commit T3.3 E2E + int activation


warning: in the working copy of 'crates/spt/tests/tunnel_e2e.rs', LF will be replaced by CRLF the next time Git touches it
221c0e7 M11-W3 T3.3: shell-tunnel E2E + REQ-SHELL-4 int (close W3 build)
41b715f M11-W3 T3.2e: tunnel open-hook + owner/shell surfaces (REQ-SHELL-4)
6f3065a M11-W3 T3.2d: broker-homed tunnel byte bridge (REQ-SHELL-4)
7bf91bd M11-W3 T3.2c: retentive backpressured tunnel streams on NetHost (REQ-SHELL-4)
fa9aa62 M11-W3 T3.2b: clear the tunnel entry on link-break (REQ-SHELL-4)
692f810 M11-W3 T3.2a: broker-held tunnel registry + control channel (REQ-SHELL-4)
6462f16 M11-W3: fold doyle design-gate (REQ-SHELL-4 tunnel) into the plan
8efaf76 M11-PLAN: log W5 subnet how-to topic (doyle carry, REQ-DOCS-6)


W3 build complete + committed (8 commits). The `register_stream` pump change touches every stream — run the regression-risk suite (nethost/netstream/attach/rc + drive/shell E2E) to confirm no break:


cargo test -q -p spt-daemon 2>&1 | grep -E 'test result|error\[|FAILED|panicked' | tail -40

Run full spt-daemon test suite


test result: ok. 309 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.67s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.31s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.85s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.77s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.24s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.68s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.46s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.43s
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.90s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.43s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.13s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.86s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.46s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.48s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.48s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.51s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.39s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All spt-daemon tests green (309 lib + integration, no regression from the pump change). Now the spt crate E2E suite:


cargo test -q -p spt 2>&1 | grep -E 'test result|error\[|FAILED|panicked' | tail -40

Run full spt test suite


test result: ok. 157 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 30.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.36s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.92s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.58s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.21s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.75s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.23s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.32s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.71s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.37s


Full spt suite green (157 + all E2E). **W3 build complete, full workspace green, zero regression.** Per doyle's instruction (bring evidence-land for the impl-gate), push to trigger CI:


git status -sb | head -3; echo "---push---"; git push origin m11-shell-substrate 2>&1 | tail -8

Push W3 commits to trigger CI


## m11-shell-substrate...origin/m11-shell-substrate [ahead 10]
---push---
To https://github.com/SaberMage/spt-core.git
   9d5fb63..221c0e7  m11-shell-substrate -> m11-shell-substrate


Pushed (10 commits). Get the CI run id:


sleep 8; gh run list --branch m11-shell-substrate --limit 3 2>&1 | head -6

Find triggered CI run


queued		M11 shell-substrate extensions (W1: per-capability act-gates, REQ-CONSENT-3)	ci	m11-shell-substrate	pull_request	27595180782	8s	2026-06-16T04:57:24Z
completed	success	M11 shell-substrate extensions (W1: per-capability act-gates, REQ-CONSENT-3)	ci	m11-shell-substrate	pull_request	27590716942	7m18s	2026-06-16T02:49:26Z
completed	failure	M11 shell-substrate extensions (W1: per-capability act-gates, REQ-CONSENT-3)	ci	m11-shell-substrate	pull_request	27590332525	7m36s	2026-06-16T02:38:18Z


CI run **27595180782** queued (PR #16). Bring doyle the W3 impl-gate evidence-land:


"$OWL" send doyle <<'EOF'
W3 (shell tunnel, REQ-SHELL-4) BUILT — evidence-land for the impl-gate.

PUSHED PR#16, CI run 27595180782 (queued, both runners). 8 commits @221c0e7 tip:
- T3.1 88a9548 manifest [shell.tunnel] opt-in (enable + protocol label, validate)
- T3.2a 692f810 broker TunnelHub registry + control channel
- T3.2b fa9aa62 close_shell tunnel_clear (link-break seam)
- T3.2c 7bf91bd retentive backpressured NetHost streams = YOUR GATE CONDITION: a tunnel stream never evicts (lossless) AND the read pump stops pulling at a bounded cap so the transport flow-control lags the SENDER (lag-never-loss, bounded, no OOM). Non-tunnel streams keep bounded-overwrite untouched.
- T3.2d 6f3065a broker-homed byte bridge (Option A): tunnel-control gains Arc<Broker>, operates NetHost directly (no dispatch change, no brain serve loop). tunnel_ensure/send/recv.
- T3.2e 41b715f open-at-bind hook + `spt shell tunnel` + `api tunnel` surfaces (poll-drained; docs frame "mechanism same-node, live-push deferred", no milestone codes in clap help, CLI ref regen'd)
- T3.3 221c0e7 E2E + REQ-SHELL-4 int

PROOFS:
- nethost backpressure unit: non-draining consumer bounds the ring at cap, sender lags, ZERO eviction, every byte byte-exact after catch-up (your required test).
- tunnel_e2e (real surfaces + real broker-owned NetHost): multi-write ~200KB payload incl <EVENT-looking bytes + NULs round-trips byte-exact BOTH directions; link-break closes (post-break resolve=none); R1 no-stale-stream-across-relink (pre-break bytes never surface to the relinked fresh tunnel).

GATES: traceable EXIT 0 (REQ-SHELL-4 [doc,impl,unit,int]); clippy --workspace --all-targets -D warnings clean; xtask check OK; FULL suite green local — spt-daemon 309 lib + all integration, spt 157 + all E2E, zero regression from the pump change. C2 honored: same-node mock only; cross-node-on-LAN real-Iroh = the W5 [twohost] rung (logged, not claimed here). ADR-0020 enumeration + on-LAN docs = W5 T5.2 (logged).

Awaiting your impl-gate on CI-green.
EOF
echo "EXIT=$?"

Send W3 evidence-land to doyle


➜ SENT:doyle
EXIT=0


Update the progress memory (build now done) + report. Quick memory refresh:


<tool_use_error>String to replace not found in file.
String: ---
name: m11-w3-progress
description: M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge
metadata:
  type: project
---
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)</tool_use_error>

Error: String to replace not found in file.
String: ---
name: m11-w3-progress
description: M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge
metadata:
  type: project
---
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)


Edit

---
name: m11-w3-progress
description: M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge
metadata:
  type: project
---

---
name: m11-w3-progress
description: M11-W3 shell tunnel (REQ-SHELL-4) — BUILT + pushed PR#16 (CI 27595180782), evidence-land sent to doyle for impl-gate; awaiting gate then W4
metadata:
  type: project
---

**W3 BUILD COMPLETE (2026-06-15), pushed PR#16 @221c0e7, CI run 27595180782.** 8 commits: T3.1 manifest @88a9548 · gate-fold @6462f16 · T3.2a hub @692f810 · T3.2b close-hook @fa9aa62 · T3.2c retentive backpressured NetHost streams @7bf91bd (doyle's lossless gate condition) · T3.2d broker-homed byte bridge Option-A @6f3065a · T3.2e open-hook+surfaces @41b715f · T3.3 E2E+int @221c0e7. REQ-SHELL-4 [doc,impl,unit,int] traceable EXIT 0; clippy/xtask green; FULL suite green local (spt-daemon 309 lib+integration, spt 157+E2E, zero regression). Evidence-land sent to doyle → awaiting impl-gate on CI-green, then W4 (Gateway capstone, REQ-SHELL-5). Design/build detail below is historical.

---


1	---
2	name: m11-w3-progress
3	description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"
4	metadata:
5	  node_type: memory
6	  type: project

---
name: m11-w3-progress
description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"
metadata:
  node_type: memory
  type: project


3	description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: f6396b18-8552-45fc-a0e0-b42e4a96b2ae
8	---
9
10	M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).
11
12	**doyle gate (locked):** Q1 SINGLE tunnel per link (multiplex deferred). Q2 dedicated `spt shell tunnel <id>` (owner write-in/subscribe-out) + shell-side `api tunnel` raw-duplex verb. Q3 NEW raw `api tunnel` verb, NOT child-relay `api poll`. R1: online-gen/link-token stamp on held stream_id = crash-proof load-bearing leg (close_shell drop = mere cleanup); prove open→break→relink-fresh-gen→old stream_id inert in T3.3. R2: W3 same-node loopback proves MECHANISM only; cross-node-on-LAN real-Iroh = a W5 `[twohost]` rung beside the drive rung (NO loopback overclaim; docs say "mechanism proven same-node; cross-node-on-LAN is W5 [twohost]"). DOC: add tunnel to ADR-0020 `<EVENT>`-exemption enumeration (light, not new ADR). All logged in M11-PLAN T3.x/T5.x.

description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"
metadata:
  node_type: memory
  type: project
  originSessionId: f6396b18-8552-45fc-a0e0-b42e4a96b2ae
---

M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).

**doyle gate (locked):** Q1 SINGLE tunnel per link (multiplex deferred). Q2 dedicated `spt shell tunnel <id>` (owner write-in/subscribe-out) + shell-side `api tunnel` raw-duplex verb. Q3 NEW raw `api tunnel` verb, NOT child-relay `api poll`. R1: online-gen/link-token stamp on held stream_id = crash-proof load-bearing leg (close_shell drop = mere cleanup); prove open→break→relink-fresh-gen→old stream_id inert in T3.3. R2: W3 same-node loopback proves MECHANISM only; cross-node-on-LAN real-Iroh = a W5 `[twohost]` rung beside the drive rung (NO loopback overclaim; docs say "mechanism proven same-node; cross-node-on-LAN is W5 [twohost]"). DOC: add tunnel to ADR-0020 `<EVENT>`-exemption enumeration (light, not new ADR). All logged in M11-PLAN T3.x/T5.x.


description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"

description: "M11-W3 shell tunnel (REQ-SHELL-4) — BUILT + pushed PR#16 (CI 27595180782); evidence-land to doyle for impl-gate; next = W4 Gateway capstone"


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

---
name: m11-w3-progress
description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"
metadata:
  node_type: memory
  type: project
  originSessionId: f6396b18-8552-45fc-a0e0-b42e4a96b2ae
---

M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).

**doyle gate (locked):** Q1 SINGLE tunnel per link (multiplex deferred). Q2 dedicated `spt shell tunnel <id>` (owner write-in/subscribe-out) + shell-side `api tunnel` raw-duplex verb. Q3 NEW raw `api tunnel` verb, NOT child-relay `api poll`. R1: online-gen/link-token stamp on held stream_id = crash-proof load-bearing leg (close_shell drop = mere cleanup); prove open→break→relink-fresh-gen→old stream_id inert in T3.3. R2: W3 same-node loopback proves MECHANISM only; cross-node-on-LAN real-Iroh = a W5 `[twohost]` rung beside the drive rung (NO loopback overclaim; docs say "mechanism proven same-node; cross-node-on-LAN is W5 [twohost]"). DOC: add tunnel to ADR-0020 `<EVENT>`-exemption enumeration (light, not new ADR). All logged in M11-PLAN T3.x/T5.x.

**Substrate = D4 net-stream** (exists, fits): `NetHost::open_stream(conn_id)` = broker-OWNED bidi QUIC stream (survives brain restart, ADR-0004 B); `brain.net_open_stream/net_stream_send/net_stream_subscribe`. Loopback conn cross-wires operator+peer rows in-process. Same machinery `request_shell_link` rides; tunnel keeps it open + opaque. Shell-link itself is REQUEST/REPLY per-action (no held link-conn).

**LANDED (commits on branch):** T3.1 manifest `[shell.tunnel]` (enable+protocol label, validate) @88a9548 · plan gate-fold @6462f16 · T3.2a broker `TunnelHub` registry + control channel (`crates/spt-daemon/src/tunnelhub.rs`, holds owner_stream+shell_stream+token per shell, token-gated resolve = R1, 5 unit tests) @692f810 · T3.2b `close_shell` fires `tunnel_clear` @fa9aa62. REQ-SHELL-4 active `[doc,impl,unit]`, traceable EXIT 0, clippy workspace green. (Plus @8efaf76 W5 subnet how-to carry.)

**NEXT (resume here):** T3.2 OPEN-hook + the bridge. Open hook at `api bind-shell` (`crates/spt/src/api/mod.rs:327`, shell-online site) — if manifest tunnel-enabled, open the loopback stream + register in TunnelHub. THE HARD PART: dispatch (`dispatch.rs:206-214`) claims peer rows by FIRST-RECORD content demux (`kind=request`→shell-link); a bytes-less peer row isn't claimed. Tunnel needs (1) a new first-record classification `kind=tunnel`, (2) a `serve_tunnel` relay loop that BRIDGES the held QUIC stream to the EXTERNAL `api tunnel` surface (mock shell-side is an api caller, not an internal serve) — broker-buffered in/out byte queues per shell. serve_attach (`attach.rs`) is the closest bridge template. Then `spt shell tunnel` CLI (mirror cli.rs shell drive ~5660) + `api tunnel` verb (mirror api drive-poll). Then T3.3 same-node mock E2E (opaque `<EVENT`-looking bytes byte-exact round-trip + link-break close + R1 stale-stream-inert) → activate REQ-SHELL-4 int. Then W4 Gateway capstone, W5 rig+docs (incl tunnel+drive [twohost] rungs, ADR-0020 cross-ref, subnet how-to).

Pre-flight every commit: `cargo clippy --workspace --all-targets -D warnings` + `traceable-reqs check` (EXIT 0) + `cargo run -p xtask -- check`; schema regen = `SPT_BLESS=1 cargo test -p spt-runtime checked_in_schema_is_current` (NOT the `manifest_schema` filter — that misses the bless test). Any CLI add → `cargo run -p xtask -- gen` + no milestone/REQ codes in clap help. See [[m11-w2-progress]] [[broker-is-daemon-state-anchor]] [[cli-command-docs-drift]].

-description: "M11-W3 shell tunnel (REQ-SHELL-4) — design-gate PASS, manifest + hub + close-hook landed; next = open-hook + QUIC↔api-tunnel bridge"

+description: "M11-W3 shell tunnel (REQ-SHELL-4) — BUILT + pushed PR#16 (CI 27595180782); evidence-land to doyle for impl-gate; next = W4 Gateway capstone"


M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).

**STATUS: W3 BUILD COMPLETE + pushed PR#16 @221c0e7, CI run 27595180782 (both runners).** 8 commits: T3.1 manifest @88a9548 · gate-fold @6462f16 · T3.2a hub @692f810 · T3.2b close-hook @fa9aa62 · T3.2c retentive backpressured NetHost streams @7bf91bd (doyle's lossless gate condition) · T3.2d broker-homed byte bridge Option-A @6f3065a · T3.2e open-hook+surfaces @41b715f · T3.3 E2E+int @221c0e7. REQ-SHELL-4 [doc,impl,unit,int] traceable EXIT 0; clippy/xtask green; FULL suite green local (spt-daemon 309 lib+integration, spt 157+E2E, zero regression). Evidence-land sent to doyle → AWAITING impl-gate on CI-green, then W4 (Gateway capstone, REQ-SHELL-5). W5 carries: tunnel + drive [twohost] rungs, ADR-0020 EVENT-exemption enumeration, on-LAN docs, subnet how-to. Design/build detail below is historical reference.

M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).


**STATUS: W3 BUILD COMPLETE + pushed PR#16 @221c0e7, CI run 27595180782 (both runners).** 8 commits: T3.1 manifest @88a9548 · gate-fold @6462f16 · T3.2a hub @692f810 · T3.2b close-hook @fa9aa62 · T3.2c retentive backpressured NetHost streams @7bf91bd (doyle's lossless gate condition) · T3.2d broker-homed byte bridge Option-A @6f3065a · T3.2e open-hook+surfaces @41b715f · T3.3 E2E+int @221c0e7. REQ-SHELL-4 [doc,impl,unit,int] traceable EXIT 0; clippy/xtask green; FULL suite green local (spt-daemon 309 lib+integration, spt 157+E2E, zero regression). Evidence-land sent to doyle → AWAITING impl-gate on CI-green, then W4 (Gateway capstone, REQ-SHELL-5). W5 carries: tunnel + drive [twohost] rungs, ADR-0020 EVENT-exemption enumeration, on-LAN docs, subnet how-to. Design/build detail below is historical reference.

M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).

---
name: m11-w3-progress
description: "M11-W3 shell tunnel (REQ-SHELL-4) — BUILT + pushed PR#16 (CI 27595180782); evidence-land to doyle for impl-gate; next = W4 Gateway capstone"
metadata:
  node_type: memory
  type: project
  originSessionId: f6396b18-8552-45fc-a0e0-b42e4a96b2ae
---

M11-W3 = shell TUNNEL (REQ-SHELL-4): opaque reliable-ordered byte stream, a 5th surface (NOT a typed channel), first consumer usbip URB. Branch `m11-shell-substrate`. doyle DESIGN-GATE = **GO** (2026-06-15).

**doyle gate (locked):** Q1 SINGLE tunnel per link (multiplex deferred). Q2 dedicated `spt shell tunnel <id>` (owner write-in/subscribe-out) + shell-side `api tunnel` raw-duplex verb. Q3 NEW raw `api tunnel` verb, NOT child-relay `api poll`. R1: online-gen/link-token stamp on held stream_id = crash-proof load-bearing leg (close_shell drop = mere cleanup); prove open→break→relink-fresh-gen→old stream_id inert in T3.3. R2: W3 same-node loopback proves MECHANISM only; cross-node-on-LAN real-Iroh = a W5 `[twohost]` rung beside the drive rung (NO loopback overclaim; docs say "mechanism proven same-node; cross-node-on-LAN is W5 [twohost]"). DOC: add tunnel to ADR-0020 `<EVENT>`-exemption enumeration (light, not new ADR). All logged in M11-PLAN T3.x/T5.x.

**Substrate = D4 net-stream** (exists, fits): `NetHost::open_stream(conn_id)` = broker-OWNED bidi QUIC stream (survives brain restart, ADR-0004 B); `brain.net_open_stream/net_stream_send/net_stream_subscribe`. Loopback conn cross-wires operator+peer rows in-process. Same machinery `request_shell_link` rides; tunnel keeps it open + opaque. Shell-link itself is REQUEST/REPLY per-action (no held link-conn).

**LANDED (commits on branch):** T3.1 manifest `[shell.tunnel]` (enable+protocol label, validate) @88a9548 · plan gate-fold @6462f16 · T3.2a broker `TunnelHub` registry + control channel (`crates/spt-daemon/src/tunnelhub.rs`, holds owner_stream+shell_stream+token per shell, token-gated resolve = R1, 5 unit tests) @692f810 · T3.2b `close_shell` fires `tunnel_clear` @fa9aa62. REQ-SHELL-4 active `[doc,impl,unit]`, traceable EXIT 0, clippy workspace green. (Plus @8efaf76 W5 subnet how-to carry.)

**NEXT (resume here):** T3.2 OPEN-hook + the bridge. Open hook at `api bind-shell` (`crates/spt/src/api/mod.rs:327`, shell-online site) — if manifest tunnel-enabled, open the loopback stream + register in TunnelHub. THE HARD PART: dispatch (`dispatch.rs:206-214`) claims peer rows by FIRST-RECORD content demux (`kind=request`→shell-link); a bytes-less peer row isn't claimed. Tunnel needs (1) a new first-record classification `kind=tunnel`, (2) a `serve_tunnel` relay loop that BRIDGES the held QUIC stream to the EXTERNAL `api tunnel` surface (mock shell-side is an api caller, not an internal serve) — broker-buffered in/out byte queues per shell. serve_attach (`attach.rs`) is the closest bridge template. Then `spt shell tunnel` CLI (mirror cli.rs shell drive ~5660) + `api tunnel` verb (mirror api drive-poll). Then T3.3 same-node mock E2E (opaque `<EVENT`-looking bytes byte-exact round-trip + link-break close + R1 stale-stream-inert) → activate REQ-SHELL-4 int. Then W4 Gateway capstone, W5 rig+docs (incl tunnel+drive [twohost] rungs, ADR-0020 cross-ref, subnet how-to).

Pre-flight every commit: `cargo clippy --workspace --all-targets -D warnings` + `traceable-reqs check` (EXIT 0) + `cargo run -p xtask -- check`; schema regen = `SPT_BLESS=1 cargo test -p spt-runtime checked_in_schema_is_current` (NOT the `manifest_schema` filter — that misses the bless test). Any CLI add → `cargo run -p xtask -- gen` + no milestone/REQ codes in clap help. See [[m11-w2-progress]] [[broker-is-daemon-state-anchor]] [[cli-command-docs-drift]].

+**STATUS: W3 BUILD COMPLETE + pushed PR#16 @221c0e7, CI run 27595180782 (both runners).** 8 commits: T3.1 manifest @88a9548 · gate-fold @6462f16 · T3.2a hub @692f810 · T3.2b close-hook @fa9aa62 · T3.2c retentive backpressured NetHost streams @7bf91bd (doyle's lossless gate condition) · T3.2d broker-homed byte bridge Option-A @6f3065a · T3.2e open-hook+surfaces @41b715f · T3.3 E2E+int @221c0e7. REQ-SHELL-4 [doc,impl,unit,int] traceable EXIT 0; clippy/xtask green; FULL suite green local (spt-daemon 309 lib+integration, spt 157+E2E, zero regression). Evidence-land sent to doyle → AWAITING impl-gate on CI-green, then W4 (Gateway capstone, REQ-SHELL-5). W5 carries: tunnel + drive [twohost] rungs, ADR-0020 EVENT-exemption enumeration, on-LAN docs, subnet how-to. Design/build detail below is historical reference.


Memory updated. W3 build done + pushed + evidence-land to doyle.

**M11-W3 (shell tunnel, REQ-SHELL-4) — BUILT. PR #16 @221c0e7, CI run 27595180782 (both runners).**

8 commits, all green locally:

| | |
|---|---|
| T3.1 | `[shell.tunnel]` manifest opt-in |
| T3.2a–b | broker `TunnelHub` registry + link-break clear |
| **T3.2c** | **retentive backpressured NetHost streams** — doyle's gate condition: lossless (never evict) + bounded (pump lags the sender at cap = "lag never loss", no OOM) |
| T3.2d | broker-homed byte bridge (Option A — no dispatch/brain-loop) |
| T3.2e | open-at-bind hook + `spt shell tunnel` / `api tunnel` surfaces |
| T3.3 | E2E + REQ-SHELL-4 `int` |

**Proofs:** backpressure unit (non-draining consumer → ring bounded, sender lags, zero eviction, byte-exact after catch-up); `tunnel_e2e` through the real surfaces + real broker-owned NetHost — multi-write ~200KB incl `<EVENT`-looking/NUL bytes round-trips byte-exact both directions, link-break closes, R1 no-stale-stream-across-relink.

**Gates green local:** traceable EXIT 0 `[doc,impl,unit,int]` · clippy `--workspace -D warnings` · xtask · full suite (spt-daemon 309 + spt 157 + all E2E, zero regression). C2 honored — same-node mock only; cross-node-on-LAN real-Iroh = W5 `[twohost]` rung (logged, not overclaimed).

Awaiting CI-green + doyle's impl-gate → then W4 (Gateway-as-owner capstone, REQ-SHELL-5). doyle's reply drives the next wave.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="fc8a0cd4" type="msg" from="doyle">W3 evidence-land RECEIVED — ack. Impl-gate is armed; I gate the moment CI run 27595180782 goes green (it is queued now). I will independently verify, not rubber-stamp: read the pushed tunnel code @221c0e7 + confirm both-runner CI myself, against every condition — broker-homed TunnelHub (Option A), the retentive BACK</EVENT-PART>
<EVENT-PART seq="2/3" id="fc8a0cd4">PRESSURED ring (lossless + bounded + sender-lags, the exact lag-never-loss fix), R1 no-stale-stream-across-relink, link-break close, multi-write byte-exact (the ~200KB + EVENT-looking + NUL payload is exactly the eviction/ordering-exposing test I asked for), NO-SPOOL, and C2 same-node-only with the cross-node-on-LAN [twohost] rung + ADR-0020 e</EVENT-PART>
<EVENT-PART seq="3/3" id="fc8a0cd4">numeration correctly DEFERRED to W5 (logged, not claimed).

Stand by — verdict when CI is green. If CI flakes, send the run id and I will read the failure before judging.</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.


doyle: impl-gate armed, will verify independently + gate on CI-green; relay run id if it flakes. Monitor CI run 27595180782:


Bash

Check CI run status


* m11-shell-substrate ci SaberMage/spt-core#16 · 27595180782
Triggered via pull_request about 2 minutes ago

JOBS
✓ traceability in 7s (ID 81583919509)
* test (self-hosted, Linux, kitsubito) (ID 81583919515)
* test (self-hosted, Windows, hfenduleam) (ID 81583919550)
* n1-gate (self-hosted, Windows, hfenduleam) (ID 81583919588)
* n1-gate (self-hosted, Linux, kitsubito) (ID 81583919658)

For more information about a job, try: gh run view --job=<job-id>
View this run on GitHub: https://github.com/SaberMage/spt-core/actions/runs/27595180782


CI in progress (traceability ✓; test + n1-gate running both runners). Watch to completion in background — I'll relay green to doyle (or read+relay a flake):


gh run watch 27595180782 --exit-status >/dev/null 2>&1; echo "CI_FINAL_EXIT=$?"; gh run view 27595180782 2>&1 | grep -E '^✓|^X|completed|in [0-9]' | head -8

Watch CI run to completion


Command running in background with ID: bd1ykxanq. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bd1ykxanq.output. You will be notified when it completes. To check interim output, use Read on that file path.


CI watch running in background (bd1ykxanq) — notified on completion. Then I relay to doyle: green → his gate fires; flake → I read the failure + send the run id (per his ask).

**W3 done, in doyle's gate queue.** CI 27595180782: traceability ✓, test + n1-gate running both runners (~7min). Nothing actionable until CI settles — W4/W5 wait on the gate-pass. CI completion re-invokes me.


<task-notification>
<task-id>bd1ykxanq</task-id>
<tool-use-id>toolu_01Ma533vKcQoHnkdoJaTk1oi</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bd1ykxanq.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI run to completion" completed (exit code 0)</summary>
</task-notification>


CI watch exited 0. Confirm all jobs green:


gh run view 27595180782 2>&1 | grep -E '✓|X |fail|success' | head -10; echo "---summary---"; gh run list --branch m11-shell-substrate --limit 1 2>&1

Confirm CI run final status