868	name = "GameRobot"
869	kind = "shell"
870	version = "1.0.0"
871	min_spt_core_version = "1.0.0"
872
873	[shell]
874	spawn = "gamerobot-shell --link {link_token}"
875	ephemeral = false
876	broadcast = "subnet"
877	command_receipt = "http"
878	persistent = true
879	can_shutdown = false
880
881	[shell.capabilities]
882	move = { args = ["direction", "distance"] }
883	screenshot = {}
884
885	[shell.sensory]
886	types = ["image", "sound", "event"]
887	"#;
888
889	    #[test] // [unit->REQ-MANIFEST-1]
890	    fn harness_fixture_parses_all_sections() {
891	        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
892	        assert_eq!(m.adapter.name, "claude-spt");
893	        assert_eq!(m.adapter.kind, AdapterKind::Harness);
894	        assert_eq!(m.adapter.hostable_types.len(), 4);
895	        // hooks
896	        assert!(m.hooks["SessionStart"].can_inject);
897	        assert!(!m.hooks["Stop"].can_inject);
898	        assert_eq!(
899	            m.hooks["SessionStart"].reads,
900	            ["session_id", "source", "agent_type"]
901	        );
902	        // session: dirs + one role
903	        assert_eq!(m.session.commune_dir.as_deref(), Some(".claude"));
904	        let role = m.session.psyche_init.as_ref().unwrap();
905	        assert!(role.detach);
906	        assert_eq!(role.env_remove, ["OWL_SESSION_ID"]);
907	        assert_eq!(
908	            role.recursion_guard_env.as_deref(),
909	            Some("SPT_ECHO_COMMUNE")
910	        );
911	        assert_eq!(m.session.roles().len(), 1);
912	        // env
913	        assert_eq!(m.env["OWL_SESSION_ID"].direction, EnvDirection::Inject);
914	        // digest: the extractor seam with own source + presentation defaults
915	        let dig = m.digest.as_ref().unwrap();
916	        assert_eq!(dig.extractor, "claude-spt-digest --session {session_id} --in {source}");
917	        assert_eq!(dig.window_turns, Some(5));
918	        assert_eq!(dig.arg_truncation, Some(40));
919	        assert_eq!(dig.sprint_collapse, Some(true));
920	        // history / inject / identity / update enums
921	        assert_eq!(m.history.unwrap().strategy, HistoryStrategy::Fetcher);
922	        let inject = m.inject.unwrap();
923	        assert_eq!(inject.idle, [InjectMethod::Pty, InjectMethod::Hook]);
924	        assert_eq!(
925	            m.identity.unwrap().session_id_source,
926	            SessionIdSource::PostSpawn
927	        );
928	        assert_eq!(m.update.unwrap().avenue, UpdateAvenue::Delegated);
929	    }
930
931	    // [unit->REQ-UPD-5] adapter content signing: a `file_pull` avenue must declare
932	    // both `repo` and a `signing_key` (so a pulled payload is verifiable); a
933	    // `delegated` avenue carries the `self_verifies` attest instead. Both shapes
934	    // round-trip through TOML.
935	    #[test]
936	    fn update_avenue_content_signing_fields() {
937	        // file_pull without signing_key → rejected.
938	        let no_key = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
939	                      [update]\navenue=\"file_pull\"\nrepo=\"https://x/y\"\n";
940	        let err = Manifest::from_toml_str(no_key).unwrap_err().to_string();
941	        assert!(
942	            err.contains("signing_key"),
943	            "file_pull must require a content key: {err}"
944	        );
945
946	        // file_pull with repo + signing_key → ok, fields preserved + round-trip.
947	        let ok = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
948	                  [update]\navenue=\"file_pull\"\nrepo=\"https://x/y\"\nsigning_key=\"deadbeef\"\n";
949	        let m = Manifest::from_toml_str(ok).expect("file_pull with key parses");
950	        let u = m.update.clone().unwrap();
951	        assert_eq!(u.avenue, UpdateAvenue::FilePull);
952	        assert_eq!(u.signing_key.as_deref(), Some("deadbeef"));
953	        assert!(!u.self_verifies);
954	        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
955	        assert_eq!(m.update, reparsed.update);
956
957	        // delegated with self_verifies attest → ok (no signing_key needed).
958	        let deleg = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
959	                     [update]\navenue=\"delegated\"\ncommand=\"claude plugin update\"\nself_verifies=true\n";
960	        let md = Manifest::from_toml_str(deleg).expect("delegated parses");
961	        assert!(
962	            md.update.unwrap().self_verifies,
963	            "delegated attest preserved"
964	        );
965	    }
966
967	    #[test] // [unit->REQ-MANIFEST-1]
968	    fn shell_fixture_parses() {
969	        let m = Manifest::from_toml_str(SHELL_FIXTURE).expect("parse");
970	        assert_eq!(m.adapter.kind, AdapterKind::Shell);
971	        let shell = m.shell.as_ref().unwrap();
972	        assert_eq!(shell.broadcast, Some(Broadcast::Subnet));
973	        assert_eq!(shell.command_receipt, Some(CommandReceipt::Http));
974	        assert_eq!(shell.capabilities["move"].args, ["direction", "distance"]);
975	        assert!(shell.capabilities["screenshot"].args.is_empty());
976	        assert_eq!(shell.sensory.types, ["image", "sound", "event"]);
977	    }
978
979	    #[test] // [unit->REQ-MANIFEST-1]
980	    fn round_trips_through_toml() {
981	        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
982	        let serialized = m.to_toml_string().expect("serialize");
983	        let reparsed = Manifest::from_toml_str(&serialized).expect("re-parse");
984	        assert_eq!(
985	            m, reparsed,
986	            "manifest must survive a serialize→parse round-trip"
987	        );
988	    }
989
990	    #[test] // [unit->REQ-MANIFEST-1]
991	    fn unknown_enum_variant_is_an_error_not_a_panic() {
992	        let bad = r#"
993	[adapter]
994	name = "x"
995	version = "1.0.0"
996	min_spt_core_version = "1.0.0"
997

name = "GameRobot"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
spawn = "gamerobot-shell --link {link_token}"
ephemeral = false
broadcast = "subnet"
command_receipt = "http"
persistent = true
can_shutdown = false

[shell.capabilities]
move = { args = ["direction", "distance"] }
screenshot = {}

[shell.sensory]
types = ["image", "sound", "event"]
"#;

    #[test] // [unit->REQ-MANIFEST-1]
    fn harness_fixture_parses_all_sections() {
        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
        assert_eq!(m.adapter.name, "claude-spt");
        assert_eq!(m.adapter.kind, AdapterKind::Harness);
        assert_eq!(m.adapter.hostable_types.len(), 4);
        // hooks
        assert!(m.hooks["SessionStart"].can_inject);
        assert!(!m.hooks["Stop"].can_inject);
        assert_eq!(
            m.hooks["SessionStart"].reads,
            ["session_id", "source", "agent_type"]
        );
        // session: dirs + one role
        assert_eq!(m.session.commune_dir.as_deref(), Some(".claude"));
        let role = m.session.psyche_init.as_ref().unwrap();
        assert!(role.detach);
        assert_eq!(role.env_remove, ["OWL_SESSION_ID"]);
        assert_eq!(
            role.recursion_guard_env.as_deref(),
            Some("SPT_ECHO_COMMUNE")
        );
        assert_eq!(m.session.roles().len(), 1);
        // env
        assert_eq!(m.env["OWL_SESSION_ID"].direction, EnvDirection::Inject);
        // digest: the extractor seam with own source + presentation defaults
        let dig = m.digest.as_ref().unwrap();
        assert_eq!(dig.extractor, "claude-spt-digest --session {session_id} --in {source}");
        assert_eq!(dig.window_turns, Some(5));
        assert_eq!(dig.arg_truncation, Some(40));
        assert_eq!(dig.sprint_collapse, Some(true));
        // history / inject / identity / update enums
        assert_eq!(m.history.unwrap().strategy, HistoryStrategy::Fetcher);
        let inject = m.inject.unwrap();
        assert_eq!(inject.idle, [InjectMethod::Pty, InjectMethod::Hook]);
        assert_eq!(
            m.identity.unwrap().session_id_source,
            SessionIdSource::PostSpawn
        );
        assert_eq!(m.update.unwrap().avenue, UpdateAvenue::Delegated);
    }

    // [unit->REQ-UPD-5] adapter content signing: a `file_pull` avenue must declare
    // both `repo` and a `signing_key` (so a pulled payload is verifiable); a
    // `delegated` avenue carries the `self_verifies` attest instead. Both shapes
    // round-trip through TOML.
    #[test]
    fn update_avenue_content_signing_fields() {
        // file_pull without signing_key → rejected.
        let no_key = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
                      [update]\navenue=\"file_pull\"\nrepo=\"https://x/y\"\n";
        let err = Manifest::from_toml_str(no_key).unwrap_err().to_string();
        assert!(
            err.contains("signing_key"),
            "file_pull must require a content key: {err}"
        );

        // file_pull with repo + signing_key → ok, fields preserved + round-trip.
        let ok = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
                  [update]\navenue=\"file_pull\"\nrepo=\"https://x/y\"\nsigning_key=\"deadbeef\"\n";
        let m = Manifest::from_toml_str(ok).expect("file_pull with key parses");
        let u = m.update.clone().unwrap();
        assert_eq!(u.avenue, UpdateAvenue::FilePull);
        assert_eq!(u.signing_key.as_deref(), Some("deadbeef"));
        assert!(!u.self_verifies);
        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
        assert_eq!(m.update, reparsed.update);

        // delegated with self_verifies attest → ok (no signing_key needed).
        let deleg = "[adapter]\nname=\"a\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
                     [update]\navenue=\"delegated\"\ncommand=\"claude plugin update\"\nself_verifies=true\n";
        let md = Manifest::from_toml_str(deleg).expect("delegated parses");
        assert!(
            md.update.unwrap().self_verifies,
            "delegated attest preserved"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {
        let m = Manifest::from_toml_str(SHELL_FIXTURE).expect("parse");
        assert_eq!(m.adapter.kind, AdapterKind::Shell);
        let shell = m.shell.as_ref().unwrap();
        assert_eq!(shell.broadcast, Some(Broadcast::Subnet));
        assert_eq!(shell.command_receipt, Some(CommandReceipt::Http));
        assert_eq!(shell.capabilities["move"].args, ["direction", "distance"]);
        assert!(shell.capabilities["screenshot"].args.is_empty());
        assert_eq!(shell.sensory.types, ["image", "sound", "event"]);
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn round_trips_through_toml() {
        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
        let serialized = m.to_toml_string().expect("serialize");
        let reparsed = Manifest::from_toml_str(&serialized).expect("re-parse");
        assert_eq!(
            m, reparsed,
            "manifest must survive a serialize→parse round-trip"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn unknown_enum_variant_is_an_error_not_a_panic() {
        let bad = r#"
[adapter]
name = "x"
version = "1.0.0"
min_spt_core_version = "1.0.0"


Let me find the W2 drive's manifest unit test + REQ-SHELL-3 doc tag to mirror.


No matches found

[Showing results with pagination = offset: 880]


crates\spt-runtime\src\manifest.rs:480:    /// `[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral
crates\spt-runtime\src\manifest.rs:523:/// `[shell.drive]` — the drive payload types an agent may push to a shell
crates\spt-runtime\manifest.schema.json:663:          "description": "`[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral\ncontrol; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.",
crates\spt-runtime\manifest.schema.json:758:      "description": "`[shell.drive]` — the drive payload types an agent may push to a shell\n(M11-W2, REQ-SHELL-3). The owner→shell mirror of [`Sensory`]: REST-only,\nephemeral latest-wins, never spooled.",
crates\spt-daemon\tests\twohost.rs:476:             [shell.drive]\ntypes = [\"stick\"]\n\n\
crates\spt-daemon\src\drivehub.rs:311:    // [unit->REQ-SHELL-3] latest-wins: a second write before the poll clobbers
crates\spt-daemon\src\drivehub.rs:325:    // [unit->REQ-SHELL-3] take-and-clear is exactly-once: a second poll after a
crates\spt-daemon\src\drivehub.rs:343:    // [unit->REQ-SHELL-3] no-replay-across-relink: a frame written under one link
crates\spt-daemon\src\drivehub.rs:364:    // [unit->REQ-SHELL-3] clear evicts the slot unconditionally (the link-break
crates\spt-daemon\src\drivehub.rs:376:    // [unit->REQ-SHELL-3] slots are per-owner: the same shell id under two owners
crates\spt\tests\drive_e2e.rs:86:    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a
crates\spt\tests\drive_e2e.rs:98:             [shell.drive]\ntypes = [\"stick\"]\n"
crates\spt\src\cli.rs:647:    /// `[shell.drive]`, held in a single latest-wins in-memory slot on the
crates\spt\src\cli.rs:654:        /// The drive payload type (vocabulary-checked against `[shell.drive]`).
crates\spt\src\cli.rs:5660:            // [shell.drive], composes the frame, and writes the daemon's single
crates\spt\src\cli.rs:7279:        // [unit->REQ-SHELL-3] drive parses the ref, the manifest-bounded type,
crates\spt\src\api\mod.rs:584:        // [unit->REQ-SHELL-3] drive-poll is the shell-side drain; the link is the
crates\spt-daemon\src\linkhost.rs:265:/// vocabulary-check the drive type against `[shell.drive]`, gate on liveness, and
crates\spt-daemon\src\linkhost.rs:292:    if !shell.drive.types.iter().any(|t| t == drive_type) {
crates\spt-daemon\src\linkhost.rs:295:            shell.drive.types.join(", ")
crates\spt-daemon\src\linkhost.rs:751:    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
crates\spt-daemon\src\linkhost.rs:766:                 [shell.drive]\ntypes = [\"stick\"]\n"
crates\spt-daemon\src\linkhost.rs:773:    // [unit->REQ-SHELL-3] the pure drive prep is SPOOL-FREE by construction:
crates\spt-daemon\src\linkhost.rs:833:    // [unit->REQ-SHELL-3] the cross-node drive serve DROPS an offline shell —
crates\spt-daemon\src\shellchan.rs:338:    // [unit->REQ-SHELL-3] the drive frame is the owner→shell mirror of sensory:
docs-site\src\cli\reference.md:1612:            sensory. The drive-type is vocabulary-checked against `[shell.drive]`, held in a single
docs-site\src\cli\reference.md:1712:drive-type is vocabulary-checked against `[shell.drive]`, held in a single latest-wins in-memory
docs-site\src\cli\reference.md:1723:      --type <DRIVE_TYPE>  The drive payload type (vocabulary-checked against `[shell.drive]`)
M11-PLAN.md:53:| **REQ-SHELL-3** | Drive channel: owner→shell REST-only, never-spooled, latest-wins (supersede, no replay); delivered to the online binary only, dropped-with-diagnostic if offline; manifest `[shell.drive]` vocab; `api`/CLI drive surface; cross-node ride over the ephemeral link (never the durable spool) | `doc, impl, unit, int` |
M11-PLAN.md:150:- **T2.1 — manifest + frame.** `[shell.drive]` vocab (the drive payload types, sibling of
traceable-reqs.toml:967:[Omitted long matching line]
traceable-reqs.toml:968:[Omitted long matching line]


Full picture clear. Now check REQ-SHELL-4 current state in traceable + CONTEXT doc anchor.


Bash

grep -n -A8 'REQ-SHELL-4' traceable-reqs.toml | head -40

Read REQ-SHELL-4 registry entry


964:# REQ-SHELL-4 at W3, REQ-SHELL-5 unit/int at the W4 Gateway-owner capstone.) ---
965-[[requirements]]
966-id = "REQ-SHELL-3"
967-title = "Drive channel (owner->shell, REST-only, never-spooled, latest-wins): the owner->shell mirror of sensory for continuous real-time control (scroll/crank/stick/avatar) — a [shell.drive] manifest vocab + EVENT_TYPE_DRIVE frame, delivered to the ONLINE binary only via a single live slot (a new frame supersedes an undelivered one — no spool, no queue, no replay on relink), dropped-with-diagnostic if the shell is offline; cross-node rides the ephemeral link (REST class), never the durable shell spool. Commands = discrete+durable; drive = continuous+ephemeral (CONTEXT:260, minted 2026-06-11 Gateway grill)."
968-required_stages = ["impl", "unit", "int"]  # activated M11-W2 (machinery + mock exerciser only — doyle C1: no real GameRobot/usbip shell code). impl/unit across T2.1 (manifest [shell.drive] + EVENT_TYPE_DRIVE + compose_drive_frame), T2.2 (broker-held DriveHub slot: latest-wins write, take-and-clear, link-token no-replay stamp; `spt shell drive` owner-write + `api drive-poll` shell-read; close_shell clears), T2.3 (cross-node drive_channel_write shared core + SHELL_LINK_DRIVE; drop-not-wake / no-owner-durability / no-spool-remote divergences). int = the same-node real-socket E2E (tests/drive_e2e.rs: drop-at-write-offline, latest-wins, take-and-clear, NO-SPOOL, clear-on-link-break, no-slot-file) + the rig cross-node drop-not-wake rung (tests/twohost.rs, [twohost]-gated)
969-[[requirements]]
970:id = "REQ-SHELL-4"
971-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)."
972-required_stages = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)
973-[[requirements]]
974-id = "REQ-CONSENT-3"
975-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)."
976-required_stages = ["doc", "impl", "unit", "int"]  # M11-W1 closed: doc = CONTEXT §per-capability approval gates; impl = linkhost::act_gate_decide / drive_shell gate + cli::escalate_act; unit = per_capability_act_gate_blocks_until_class_scoped_grant + run_action_refuses_gated_op_without_spooling; int = shell_actgate_e2e (class-scoped grant flips the command through the real binary)
977-[[requirements]]
978-id = "REQ-SHELL-5"


Read CONTEXT tunnel design lines


**Three channels** over the typed/binary substrate:
- **command** (agent→shell, durable/spooled) — the manifest's capability toolset; drives the shell and shell→environment interaction.
- **text+file** (2-way, durable/spooled; per-shell) — general payloads; for a shell it is a *forwarder* (harness adapters have this channel too). File transfers are progress-queryable (below).
- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.
- **drive** (owner→shell, **REST-only, never spooled, latest-wins**) — continuous-control payloads steering a surface in real time (scroll/crank state, stick positions, avatar movement). The owner→shell mirror of **sensory**: ephemeral, dropped if the shell is offline, a missed frame is superseded by the next — spooled replay of stale control on relink would be actively wrong, which is why this is not the command channel. Commands = discrete + durable; drive = continuous + ephemeral. (Minted 2026-06-11, Gateway grill. Standing consumer: GameRobot-class continuous control; the lecturn's HID path moved to a **shell tunnel** when its driver generalized to USB/IP.)

Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**: a long-lived, **reliable, ordered** byte stream (a dedicated QUIC stream pair bound to the link) for protocol traffic the channel taxonomy must NOT reinterpret — opaque wire protocols spoken end-to-end (first consumer: USB/IP URB traffic to a usbip shell). Not spooled, not enveloped; the link's lifecycle governs it — a link-break closes the tunnel. Reliable-ordered is the point and the cost: tunneled protocols cannot drop frames, so congestion surfaces as lag, never loss — acceptable only where the deployment keeps tunnels on-LAN. (Minted 2026-06-11, Gateway grill.)

<!-- [doc->REQ-SHELL-5] owner-type-agnostic: control-exclusivity keys on the owner endpoint_id, never the owner's endpoint type -->
**Owner-linked, exclusive.** Spawned by an **owning endpoint**; both linked; **only that owner `endpoint_id` may control it**. Agent endpoints are the common owner, but ownership is NOT agent-exclusive — any non-Shell endpoint type may own shells (ratified 2026-06-11, the Gateway grill: e.g. a **Gateway** owns the driven surfaces it steers; a future **Resource** endpoint could too). Control-exclusivity does not mean interaction-exclusivity: a Shell stays 2-way with its *environment* (user messages on text+file, sensory payloads inbound) — exclusive is *who commands it*. An owner and its shell **can link across nodes** (the shell perch lives on the shell's node; commands ride Iroh). One owner : many shells (`GameRobot-0`, `GameRobot-1`, …).

**Command-delivery method** is the shell adapter's choice (same modes as agent inject-input): HTTP REST, stdin-via-broker, or child-relay (`api poll`). The binary is always broker-launched, so binding is direct (`api bind`, no seed-file).

**Lifecycle = online / offline / torn-down.** A **link-break always closes the broker + shell binary** (with an optional manifest-declared **pre-close instruction** to the binary + a **termination timeout** for graceful shutdown). Then: an **ephemeral** shell (a manifest property, not an agent choice) is fully torn down **and removed from the agent's shell history**; a **persistent** shell keeps its perch **offline** (re-linkable — the binary is re-spawned on relink). The binary never survives a link-break.


CONTEXT:262 is the doc anchor (REQ-SHELL-4 still `required_stages = []`). T3.1 (manifest opt-in) is locked + zero transport risk — landing it. The transport mechanism (T3.2/T3.3) is the deep-commit boundary → design-gate to doyle after. Building T3.1.


TaskCreate

[{"content":"T3.1 — [shell.tunnel] manifest opt-in (enable + protocol label) + validate + unit; activate REQ-SHELL-4 doc/impl/unit","status":"in_progress","priority":"high"},{"content":"Design-gate T3.2/T3.3 transport+surface shape to doyle (net-stream substrate, link-bound stream, opaque relay, surface, link-break close, on-LAN)","status":"pending","priority":"high"},{"content":"T3.2 — tunnel substrate: open-on-link-up bidi stream bound to link conn, opaque relay, link-break closes, on-LAN assert","status":"pending","priority":"medium"},{"content":"T3.3 — same-node mock tunnel E2E: opaque <EVENT-looking bytes round-trip byte-exact; link-break closes; activate REQ-SHELL-4 int","status":"pending","priority":"medium"}]


<tool_use_error>InputValidationError: TaskCreate failed due to the following issues:
The required parameter `subject` is missing
The required parameter `description` is missing
An unexpected parameter `tasks` was provided

TaskCreate creates ONE task per call and has no `tasks` or `todos` parameter. Call TaskCreate once per task, passing `subject` (a brief title) and `description` (what needs to be done) as top-level string parameters.

This tool's schema was not sent to the API — it was not in the discovered-tool set derived from message history. Without the schema in your prompt, typed parameters (arrays, numbers, booleans) get emitted as strings and the client-side parser rejects them. Load the tool first: call ToolSearch with query "select:TaskCreate", then retry this call. For reference, this tool's input schema is: {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"subject":{"description":"A brief title for the task","type":"string"},"description":{"description":"What needs to be done","type":"string"},"activeForm":{"description":"Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")","type":"string"},"metadata":{"description":"Arbitrary metadata to attach to the task","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["subject","description"],"additionalProperties":false}</tool_use_error>

InputValidationError: [
  {
    "expected": "string",
    "code": "invalid_type",
    "path": [
      "subject"
    ],
    "message": "Invalid input: expected string, received undefined"
  },
  {
    "expected": "string",
    "code": "invalid_type",
    "path": [
      "description"
    ],
    "message": "Invalid input: expected string, received undefined"
  },
  {
    "code": "unrecognized_keys",
    "keys": [
      "tasks"
    ],
    "path": [],
    "message": "Unrecognized key: \"tasks\""
  }
]


Skip formal tracker — proceed. Add Tunnel struct + Shell field.


Edit

    /// `[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral
    /// control; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.
    #[serde(default, skip_serializing_if = "Drive::is_empty")]
    pub drive: Drive,
}

    /// `[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral
    /// control; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.
    #[serde(default, skip_serializing_if = "Drive::is_empty")]
    pub drive: Drive,
    /// `[shell.tunnel]` — opt-in for the opaque reliable-ordered byte tunnel
    /// (M11-W3, REQ-SHELL-4). Absent ⇒ no tunnel.
    #[serde(default, skip_serializing_if = "Tunnel::is_default")]
    pub tunnel: Tunnel,
}


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

    /// `[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral
    /// control; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.
    #[serde(default, skip_serializing_if = "Drive::is_empty")]
    pub drive: Drive,
}

    /// `[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral
    /// control; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.
    #[serde(default, skip_serializing_if = "Drive::is_empty")]
    pub drive: Drive,
    /// `[shell.tunnel]` — opt-in for the opaque reliable-ordered byte tunnel
    /// (M11-W3, REQ-SHELL-4). Absent ⇒ no tunnel.
    #[serde(default, skip_serializing_if = "Tunnel::is_default")]
    pub tunnel: Tunnel,
}

+    /// `[shell.tunnel]` — opt-in for the opaque reliable-ordered byte tunnel

+    /// (M11-W3, REQ-SHELL-4). Absent ⇒ no tunnel.

+    #[serde(default, skip_serializing_if = "Tunnel::is_default")]

/// One entry in `[shell.capabilities]` — a command and its argument names.


impl Drive {
    pub fn is_empty(&self) -> bool {
        self.types.is_empty()
    }
}

impl Drive {
    pub fn is_empty(&self) -> bool {
        self.types.is_empty()
    }
}

/// `[shell.tunnel]` — the opt-in for the opaque reliable-ordered byte tunnel
/// (M11-W3, REQ-SHELL-4): a dedicated QUIC stream pair bound to the owner↔shell
/// link, carrying wire protocol traffic the channel taxonomy must NOT reinterpret
/// (first consumer: USB/IP URB traffic). 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 ⇒ on-LAN posture.
// [impl->REQ-SHELL-4]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct Tunnel {
    /// Whether this shell opens the opaque tunnel on link-up.
    #[serde(default)]
    pub enable: bool,
    /// Optional diagnostic label for the opaque wire protocol (e.g. `usbip-urb`).
    /// Informational only — the substrate never interprets tunnel bytes.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub protocol: Option<String>,
}

impl Tunnel {
    /// Default ⇒ disabled with no label (the `skip_serializing_if` predicate so an
    /// untunnel'd shell never emits a `[shell.tunnel]` table).
    pub fn is_default(&self) -> bool {
        !self.enable && self.protocol.is_none()
    }
}


impl Drive {
    pub fn is_empty(&self) -> bool {
        self.types.is_empty()
    }
}

/// `[shell.tunnel]` — the opt-in for the opaque reliable-ordered byte tunnel
/// (M11-W3, REQ-SHELL-4): a dedicated QUIC stream pair bound to the owner↔shell
/// link, carrying wire protocol traffic the channel taxonomy must NOT reinterpret
/// (first consumer: USB/IP URB traffic). 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 ⇒ on-LAN posture.
// [impl->REQ-SHELL-4]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct Tunnel {
    /// Whether this shell opens the opaque tunnel on link-up.
    #[serde(default)]
    pub enable: bool,
    /// Optional diagnostic label for the opaque wire protocol (e.g. `usbip-urb`).
    /// Informational only — the substrate never interprets tunnel bytes.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub protocol: Option<String>,
}

impl Tunnel {
    /// Default ⇒ disabled with no label (the `skip_serializing_if` predicate so an
    /// untunnel'd shell never emits a `[shell.tunnel]` table).
    pub fn is_default(&self) -> bool {
        !self.enable && self.protocol.is_none()
    }
}

+/// `[shell.tunnel]` — the opt-in for the opaque reliable-ordered byte tunnel

+/// (M11-W3, REQ-SHELL-4): a dedicated QUIC stream pair bound to the owner↔shell

+/// link, carrying wire protocol traffic the channel taxonomy must NOT reinterpret

+/// (first consumer: USB/IP URB traffic). 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 ⇒ on-LAN posture.

+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]

+    /// Whether this shell opens the opaque tunnel on link-up.

+    /// Optional diagnostic label for the opaque wire protocol (e.g. `usbip-urb`).

+    /// Informational only — the substrate never interprets tunnel bytes.

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    /// Default ⇒ disabled with no label (the `skip_serializing_if` predicate so an

+    /// untunnel'd shell never emits a `[shell.tunnel]` table).

+        !self.enable && self.protocol.is_none()

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]


Now the validate check — fold into the existing `if let Some(shell)` block:


        if let Some(shell) = &self.shell {
            for (op, cap) in &shell.capabilities {
                if cap.class_key.is_some() && cap.require_approval.is_default() {
                    return err(format!(
                        "[shell.capabilities.{op}]: `class_key` needs a gated `require_approval` \
                         (remembered|always) — a class key only scopes a grant"

        if let Some(shell) = &self.shell {
            // [shell.tunnel] (M11-W3, REQ-SHELL-4): the diagnostic protocol label
            // describes the tunnel's opaque wire — meaningless without the tunnel
            // enabled. Refuse the contradiction at register time.
            // [impl->REQ-SHELL-4]
            if shell.tunnel.protocol.is_some() && !shell.tunnel.enable {
                return err(
                    "[shell.tunnel]: `protocol` label needs `enable = true` — the label only \
                     describes an enabled tunnel's opaque wire"
                        .into(),
                );
            }
            for (op, cap) in &shell.capabilities {
                if cap.class_key.is_some() && cap.require_approval.is_default() {
                    return err(format!(
                        "[shell.capabilities.{op}]: `class_key` needs a gated `require_approval` \
                         (remembered|always) — a class key only scopes a grant"


        if let Some(shell) = &self.shell {
            for (op, cap) in &shell.capabilities {
                if cap.class_key.is_some() && cap.require_approval.is_default() {
                    return err(format!(
                        "[shell.capabilities.{op}]: `class_key` needs a gated `require_approval` \
                         (remembered|always) — a class key only scopes a grant"

        if let Some(shell) = &self.shell {
            // [shell.tunnel] (M11-W3, REQ-SHELL-4): the diagnostic protocol label
            // describes the tunnel's opaque wire — meaningless without the tunnel
            // enabled. Refuse the contradiction at register time.
            // [impl->REQ-SHELL-4]
            if shell.tunnel.protocol.is_some() && !shell.tunnel.enable {
                return err(
                    "[shell.tunnel]: `protocol` label needs `enable = true` — the label only \
                     describes an enabled tunnel's opaque wire"
                        .into(),
                );
            }
            for (op, cap) in &shell.capabilities {
                if cap.class_key.is_some() && cap.require_approval.is_default() {
                    return err(format!(
                        "[shell.capabilities.{op}]: `class_key` needs a gated `require_approval` \
                         (remembered|always) — a class key only scopes a grant"

// `require_approval` — refuse the contradiction at register time.

+            // [shell.tunnel] (M11-W3, REQ-SHELL-4): the diagnostic protocol label

+            // describes the tunnel's opaque wire — meaningless without the tunnel

+            // enabled. Refuse the contradiction at register time.

+            if shell.tunnel.protocol.is_some() && !shell.tunnel.enable {

+                    "[shell.tunnel]: `protocol` label needs `enable = true` — the label only \

+                     describes an enabled tunnel's opaque wire"


Now the unit test. Add after `update_avenue_content_signing_fields`:


    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {

    // [unit->REQ-SHELL-4] [shell.tunnel] opt-in: an enabled tunnel with a
    // diagnostic protocol label parses + round-trips; a default (absent) tunnel
    // serializes no `[shell.tunnel]` table; a `protocol` label without
    // `enable = true` is refused at validate (the label only describes an enabled
    // tunnel's opaque wire).
    #[test]
    fn shell_tunnel_opt_in() {
        let base = "[adapter]\nname=\"u\"\nkind=\"shell\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
                    [shell]\nspawn=\"usbip-shell --link {link_token}\"\n";

        // enabled + labelled → parses, fields preserved, round-trips.
        let enabled = format!("{base}\n[shell.tunnel]\nenable = true\nprotocol = \"usbip-urb\"\n");
        let m = Manifest::from_toml_str(&enabled).expect("enabled tunnel parses");
        let t = &m.shell.as_ref().unwrap().tunnel;
        assert!(t.enable);
        assert_eq!(t.protocol.as_deref(), Some("usbip-urb"));
        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
        assert_eq!(m.shell.unwrap().tunnel, reparsed.shell.unwrap().tunnel);

        // absent tunnel → default, and serializes no [shell.tunnel] table.
        let m0 = Manifest::from_toml_str(base).expect("no tunnel parses");
        assert!(m0.shell.as_ref().unwrap().tunnel.is_default());
        assert!(
            !m0.to_toml_string().unwrap().contains("[shell.tunnel]"),
            "a default tunnel must not emit a [shell.tunnel] table"
        );

        // protocol label without enable → validate refuses the contradiction.
        let bad = format!("{base}\n[shell.tunnel]\nprotocol = \"usbip-urb\"\n");
        let err = Manifest::from_toml_str(&bad).unwrap_err().to_string();
        assert!(
            err.contains("enable = true"),
            "label-without-enable must be refused: {err}"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {


    // [unit->REQ-SHELL-4] [shell.tunnel] opt-in: an enabled tunnel with a
    // diagnostic protocol label parses + round-trips; a default (absent) tunnel
    // serializes no `[shell.tunnel]` table; a `protocol` label without
    // `enable = true` is refused at validate (the label only describes an enabled
    // tunnel's opaque wire).
    #[test]
    fn shell_tunnel_opt_in() {
        let base = "[adapter]\nname=\"u\"\nkind=\"shell\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
                    [shell]\nspawn=\"usbip-shell --link {link_token}\"\n";

        // enabled + labelled → parses, fields preserved, round-trips.
        let enabled = format!("{base}\n[shell.tunnel]\nenable = true\nprotocol = \"usbip-urb\"\n");
        let m = Manifest::from_toml_str(&enabled).expect("enabled tunnel parses");
        let t = &m.shell.as_ref().unwrap().tunnel;
        assert!(t.enable);
        assert_eq!(t.protocol.as_deref(), Some("usbip-urb"));
        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
        assert_eq!(m.shell.unwrap().tunnel, reparsed.shell.unwrap().tunnel);

        // absent tunnel → default, and serializes no [shell.tunnel] table.
        let m0 = Manifest::from_toml_str(base).expect("no tunnel parses");
        assert!(m0.shell.as_ref().unwrap().tunnel.is_default());
        assert!(
            !m0.to_toml_string().unwrap().contains("[shell.tunnel]"),
            "a default tunnel must not emit a [shell.tunnel] table"
        );

        // protocol label without enable → validate refuses the contradiction.
        let bad = format!("{base}\n[shell.tunnel]\nprotocol = \"usbip-urb\"\n");
        let err = Manifest::from_toml_str(&bad).unwrap_err().to_string();
        assert!(
            err.contains("enable = true"),
            "label-without-enable must be refused: {err}"
        );
    }

    #[test] // [unit->REQ-MANIFEST-1]
    fn shell_fixture_parses() {

+    // [unit->REQ-SHELL-4] [shell.tunnel] opt-in: an enabled tunnel with a

+    // diagnostic protocol label parses + round-trips; a default (absent) tunnel

+    // serializes no `[shell.tunnel]` table; a `protocol` label without

+    // `enable = true` is refused at validate (the label only describes an enabled

+        let base = "[adapter]\nname=\"u\"\nkind=\"shell\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\

+                    [shell]\nspawn=\"usbip-shell --link {link_token}\"\n";

+        // enabled + labelled → parses, fields preserved, round-trips.

+        let enabled = format!("{base}\n[shell.tunnel]\nenable = true\nprotocol = \"usbip-urb\"\n");

+        let m = Manifest::from_toml_str(&enabled).expect("enabled tunnel parses");

+        let t = &m.shell.as_ref().unwrap().tunnel;

+        assert_eq!(t.protocol.as_deref(), Some("usbip-urb"));

+        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();

+        assert_eq!(m.shell.unwrap().tunnel, reparsed.shell.unwrap().tunnel);

+        // absent tunnel → default, and serializes no [shell.tunnel] table.

+        let m0 = Manifest::from_toml_str(base).expect("no tunnel parses");

+        assert!(m0.shell.as_ref().unwrap().tunnel.is_default());

+            !m0.to_toml_string().unwrap().contains("[shell.tunnel]"),

+            "a default tunnel must not emit a [shell.tunnel] table"

+        // protocol label without enable → validate refuses the contradiction.

+        let bad = format!("{base}\n[shell.tunnel]\nprotocol = \"usbip-urb\"\n");

+        let err = Manifest::from_toml_str(&bad).unwrap_err().to_string();

+            err.contains("enable = true"),

+            "label-without-enable must be refused: {err}"

let m = Manifest::from_toml_str(SHELL_FIXTURE).expect("parse");


`from_toml_str` validates (the signing_key test relies on it). Now add the doc tag in CONTEXT.md and activate the req.


Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**:

<!-- [doc->REQ-SHELL-4] shell tunnel: a long-lived reliable-ordered link-bound QUIC stream pair carrying opaque bytes the taxonomy never reinterprets; manifest opt-in, not enveloped/MAC-framed/spooled; link-break closes it; reliable-ordered ⇒ on-LAN posture -->
Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**:


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

<!-- [doc->REQ-SHELL-4] shell tunnel: a long-lived reliable-ordered link-bound QUIC stream pair carrying opaque bytes the taxonomy never reinterprets; manifest opt-in, not enveloped/MAC-framed/spooled; link-break closes it; reliable-ordered ⇒ on-LAN posture -->
Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**:

- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.

- **drive** (owner→shell, **REST-only, never spooled, latest-wins**) — continuous-control payloads steering a surface in real time (scroll/crank state, stick positions, avatar movement). The owner→shell mirror of **sensory**: ephemeral, dropped if the shell is offline, a missed frame is superseded by the next — spooled replay of stale control on relink would be actively wrong, which is why this is not the command channel. Commands = discrete + durable; drive = continuous + ephemeral. (Minted 2026-06-11, Gateway grill. Standing consumer: GameRobot-class continuous control; the lecturn's HID path moved to a **shell tunnel** when its driver generalized to USB/IP.)

+<!-- [doc->REQ-SHELL-4] shell tunnel: a long-lived reliable-ordered link-bound QUIC stream pair carrying opaque bytes the taxonomy never reinterprets; manifest opt-in, not enveloped/MAC-framed/spooled; link-break closes it; reliable-ordered ⇒ on-LAN posture -->

Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**: a long-lived, **reliable, ordered** byte stream (a dedicated QUIC stream pair bound to the link) for protocol traffic the channel taxonomy must NOT reinterpret — opaque wire protocols spoken end-to-end (first consumer: USB/IP URB traffic to a usbip shell). Not spooled, not enveloped; the link's lifecycle governs it — a link-break closes the tunnel. Reliable-ordered is the point and the cost: tunneled protocols cannot drop frames, so congestion surfaces as lag, never loss — acceptable only where the deployment keeps tunnels on-LAN. (Minted 2026-06-11, Gateway grill.)

<!-- [doc->REQ-SHELL-5] owner-type-agnostic: control-exclusivity keys on the owner endpoint_id, never the owner's endpoint type -->


required_stages = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)

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


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

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


970	id = "REQ-SHELL-4"
971	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)."
972	required_stages = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)
973	[[requirements]]

id = "REQ-SHELL-4"
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 = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)
[[requirements]]


required_stages = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)

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


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 = []  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung)

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

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


Manifest changed → regen schema, then pre-flight (clippy workspace + traceable + xtask check). Run regen first.


Bash

cargo run -q -p xtask -- gen 2>&1 | tail -20

Regenerate schema + docs via xtask gen


xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md


Show changed files


 M CONTEXT.md
 M crates/spt-runtime/src/manifest.rs
 M traceable-reqs.toml


Schema didn't regen via `gen`. Find the schema-gen command.


<tool_use_error>Path does not exist: C:\Users\decid\Documents\projects\spt-core\xtask. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.</tool_use_error>

Error: Path does not exist: C:\Users\decid\Documents\projects\spt-core\xtask. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.


crates\xtask\src\main.rs:9://!   llms-full.txt / manifest.schema.json / install scripts at site root +
crates\xtask\src\main.rs:202:            "llms-full.txt" | "manifest.schema.json" => true,
crates\xtask\src\main.rs:288:            "crates/spt-runtime/manifest.schema.json",
crates\xtask\src\main.rs:289:            "manifest.schema.json",
crates\spt-runtime\src\manifest.rs:663:/// (`crates/spt-runtime/manifest.schema.json`) is the site/release asset;
crates\spt-runtime\src\manifest.rs:664:/// the `checked_in_schema_is_current` test is the drift gate (regenerate with
crates\spt-runtime\src\manifest.rs:673:pub fn manifest_json_schema() -> String {
crates\spt-runtime\src\manifest.rs:674:    let schema = schemars::schema_for!(Manifest);
crates\spt-runtime\src\manifest.rs:679:        "https://sabermage.github.io/spt-releases/manifest.schema.json".into(),
crates\spt-runtime\src\manifest.rs:1377:    #[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at
crates\spt-runtime\src\manifest.rs:1380:    fn manifest_schema_shape() {
crates\spt-runtime\src\manifest.rs:1385:            "https://sabermage.github.io/spt-releases/manifest.schema.json"
crates\spt-runtime\src\manifest.rs:1409:    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
crates\spt-runtime\src\manifest.rs:1413:    /// Regenerate: `SPT_BLESS=1 cargo test -p spt-runtime manifest_schema`.
crates\spt-runtime\src\manifest.rs:1416:    fn checked_in_schema_is_current() {
crates\spt-runtime\src\manifest.rs:1418:        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("manifest.schema.json");
crates\spt-runtime\src\manifest.rs:1424:            "checked-in manifest.schema.json (regenerate: SPT_BLESS=1 cargo test -p spt-runtime)",
crates\spt-runtime\src\manifest.rs:1429:            "manifest.schema.json drifted from the derives — regenerate with \
crates\spt-store\src\info.rs:620:    fn read_pid_tolerates_schema_minimal() {


Schema regen is `SPT_BLESS=1 cargo test -p spt-runtime manifest_schema`. Run it:


SPT_BLESS=1 cargo test -q -p spt-runtime manifest_schema 2>&1 | tail -15

Bless regenerate manifest schema


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


git status -s && echo "---DIFF schema tunnel---" && git diff crates/spt-runtime/manifest.schema.json | grep -i tunnel | head

Confirm schema regen + tunnel present


 M CONTEXT.md
 M crates/spt-runtime/src/manifest.rs
 M traceable-reqs.toml
---DIFF schema tunnel---


Schema unchanged in git — verify it actually contains Tunnel (bless may have run on stale build):


grep -c -i tunnel crates/spt-runtime/manifest.schema.json; echo "exit=$?"

Check tunnel in schema file


0
exit=1


Stale build — schema lacks Tunnel but bless passed. Force rebuild + re-bless:


touch crates/spt-runtime/src/manifest.rs && SPT_BLESS=1 cargo test -p spt-runtime manifest_schema 2>&1 | tail -8 && echo "---" && grep -c -i tunnel crates/spt-runtime/manifest.schema.json

Force rebuild and re-bless schema


    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.36s
     Running unittests src\lib.rs (target\debug\deps\spt_runtime-855c22fb1f90113d.exe)

running 1 test
test manifest::tests::manifest_schema_shape ... ok

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

---
0


Filter `manifest_schema` only hit `manifest_schema_shape`, not the bless test `checked_in_schema_is_current`. Run the right test:


SPT_BLESS=1 cargo test -p spt-runtime checked_in_schema_is_current 2>&1 | tail -8 && echo "---tunnel count---" && grep -c -i tunnel crates/spt-runtime/manifest.schema.json

Bless the real schema drift test


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running unittests src\lib.rs (target\debug\deps\spt_runtime-855c22fb1f90113d.exe)

running 1 test
test manifest::tests::checked_in_schema_is_current ... ok

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

---tunnel count---
7


Schema now carries Tunnel (7 refs). Run the unit test + full pre-flight (clippy workspace, traceable, xtask check):


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

Run tunnel unit test


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

Pre-flight clippy workspace


    Checking spt-runtime v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-live v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking mock-adapter v0.7.1 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    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 8.23s


Clippy clean. Now traceable check + xtask check (docs drift):


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

Run traceable-reqs check


  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
TRACEABLE_EXIT=0


cargo run -q -p xtask -- check 2>&1 | tail -20; echo "XTASK_EXIT=$?"

Run xtask docs-drift check


xtask check: OK
XTASK_EXIT=0


All green. Confirm REQ-SHELL-4 specifically evidenced:


traceable-reqs check 2>&1 | grep -A1 'REQ-SHELL-4'

Confirm REQ-SHELL-4 OK


  [OK] REQ-SHELL-4  required: [doc, impl, unit]  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).


REQ-SHELL-4 OK [doc+impl+unit, -int as expected]. T3.1 complete. Confirm branch + commit.


git branch --show-current && git add -A && git status -s

Confirm branch + stage


m11-shell-substrate
warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
M  CONTEXT.md
M  crates/spt-runtime/manifest.schema.json
M  crates/spt-runtime/src/manifest.rs
M  traceable-reqs.toml


git commit -q -m "$(cat <<'EOF'
M11-W3 T3.1: [shell.tunnel] manifest opt-in (REQ-SHELL-4)

Add the opt-in surface for the opaque reliable-ordered byte tunnel:
a `[shell.tunnel]` table on the shell body with `enable` + an optional
diagnostic `protocol` label. validate() refuses a `protocol` label
without `enable = true` (the label only describes an enabled tunnel's
opaque wire). Schema regen'd; drift gate clean.

Activates REQ-SHELL-4 = [doc, impl, unit] (doc = CONTEXT shell-tunnel
paragraph; impl = the manifest section + validate; unit =
shell_tunnel_opt_in round-trip + default-omits-table + label-needs-enable).
int lands at T3.3 (same-node mock tunnel E2E).

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

Commit T3.1


88a9548 M11-W3 T3.1: [shell.tunnel] manifest opt-in (REQ-SHELL-4)


T3.1 committed @88a9548. Now design-gate the T3.2/T3.3 transport+surface shape to doyle (the deep-commit boundary the commune flagged). Check my identity + doyle reachability first.


"$OWL" list 2>/dev/null | head -30 || spt endpoint list 2>&1 | head -30

List endpoints/perches to find doyle


(Bash completed with no output)


echo "OWL=$OWL"; echo "LIVE=$LIVE"; echo "OWL_SESSION_ID=$OWL_SESSION_ID"

Check messaging env vars


OWL=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe
LIVE=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe live
OWL_SESSION_ID=f6396b18-8552-45fc-a0e0-b42e4a96b2ae


"$OWL" list 2>&1 | head -40; echo "exit=$?"

List owl perches


⚠ (2 orphan directories present — run `owl doctor --fix` to GC)
✓ ACTIVE:deployah
✓   - ACTIVE:deployah-psyche
✓ ACTIVE:doyle
✓   - ACTIVE:doyle-psyche
✓ ACTIVE:perri
✓   - ACTIVE:perri-psyche
✓ ACTIVE:solar
✓   - ACTIVE:solar-psyche
✓ ACTIVE:todlando
✓   - ACTIVE:todlando-psyche
exit=0


doyle ACTIVE. Send the design-gate shape via `$OWL send`:


"$OWL" send doyle "$(cat <<'EOF'
M11-W3 (tunnel, REQ-SHELL-4) DESIGN-GATE — shape before deep commit on T3.2/T3.3.

T3.1 LANDED @88a9548 (manifest opt-in, locked surface): [shell.tunnel] table = enable + optional diagnostic protocol label; validate refuses label-without-enable. REQ-SHELL-4 activated [doc,impl,unit]; clippy/traceable/xtask all green. doc=CONTEXT shell-tunnel para.

SUBSTRATE QUESTION RESOLVED (the commune-flagged risk "needs broker-held stream machinery NOT one-shot shelllink"): the D4 net-stream substrate already exists + fits exactly. NetHost::open_stream(conn_id) opens a broker-OWNED bidi QUIC stream on a held conn (survives brain restart, ADR-0004 B); brain.net_open_stream/net_stream_subscribe/net_stream_send. Loopback cross-wires a pair in-process -> clean same-node mock E2E (T3.3). This is the SAME machinery request_shell_link already rides; the tunnel just keeps the stream open + carries OPAQUE bytes (no ShellLinkRecord NDJSON). NOT a new transport.

PROPOSED T3.2 (locked-by-CONTEXT parts, building these now):
1. ANCHOR: tunnel = a SECOND bidi stream on the shell-link's existing conn ("bound to the link" = bound to its conn). Opened owner-side at link-up when manifest tunnel-enabled.
2. OPAQUE relay: tunnel bytes ARE the payload (no envelope, no MAC-frame parse, no spool). Sibling carve-out to api poll --link raw exemption (ADR-0020).
3. BROKER-held slot keyed to the link, link-gen-stamped (no stale-tunnel reuse, same no-replay discipline as drive).
4. LINK-BREAK closes: finish+drop the stream, clear slot.
5. on-LAN posture documented + asserted; C2 same-node mock E2E only, NOT cross-WAN.

OPEN Qs (need your gate before I lock the SURFACE):
Q1 single tunnel per link (CONTEXT:262 "a dedicated stream pair"=singular; usbip=one URB stream) vs multiplex? lean: single slot, multiplex deferred.
Q2 SURFACE shape: (a) new `spt shell tunnel <id>` CLI write-in + subscribe-out + `api tunnel` shell-side, vs (b) fold into existing raw-frame surfaces. lean: (a) distinct surface (needs xtask gen + no milestone codes in clap help).
Q3 shell-binary side: tunnel bytes reach the binary via a new `api` verb, or via existing child-relay poll?

Holding the SURFACE (Q2/Q3) for your gate; proceeding on the broker tunnel-slot + open-on-link-up + link-break-close machinery (1/3/4, CONTEXT-locked). Redirect if anchor (1) is wrong.
EOF
)" 2>&1 | tail -5; echo "exit=$?"

Send design-gate shape to doyle


<owl_messages>
<owl_message from="doyle" priority="high">[doyle -> todlando] Low-priority W5 scope add (do NOT interrupt your W3 scoping — just log it in M11-PLAN T5.x now so it is not lost): perri found `spt how-to subnet` -> NO_SUCH_TOPIC. HOW_TO_TOPICS (cli.rs:4692) is locked to ["ready","send"] (v1 per M7 decision 12). Add a `subnet` how-to topic in the W5 docs wave: HOW_TO_SUBNET text (the create/show-code/join pairing flow + 6-digit code) + registry entry + bump the v1-lock test (the names-vec assertion + topic-content asserts). Under existing REQ-DOCS-6, no new REQ. Subnet/pairing = the highest-value first-run flow, currently dead-ends — closes the ready/send asymmetry. W5 docs rung, not W3. Carry on with the W3 shape.</owl_message>
</owl_messages>

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


Command running in background with ID: bfnyubl1e. 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\bfnyubl1e.output. You will be notified when it completes. To check interim output, use Read on that file path.


doyle ack: log W5 scope-add now (don't interrupt W3). Add to M11-PLAN T5.2.


Edit

- **T5.2 — docs.** Shell getting-started + manifest reference: the fourth channel, the
  tunnel, per-capability gates, owner-type-agnostic. Version refs not milestone codes in
  the published docs (the public-docs rule). `[doc]`

- **T5.2 — docs.** Shell getting-started + manifest reference: the fourth channel, the
  tunnel, per-capability gates, owner-type-agnostic. Version refs not milestone codes in
  the published docs (the public-docs rule). `[doc]`
  - **W5 CARRY (doyle, 2026-06-15):** add a `subnet` **how-to topic** (under existing
    REQ-DOCS-6, no new REQ). perri hit `spt how-to subnet` → `NO_SUCH_TOPIC`:
    `HOW_TO_TOPICS` (cli.rs:4692) is v1-locked to `["ready","send"]` (M7 decision 12).
    Add `HOW_TO_SUBNET` text (the create / show-code / join pairing flow + 6-digit code)
    + the registry entry + bump the v1-lock test (the names-vec assertion + the
    topic-content asserts). Subnet/pairing is the highest-value first-run flow and
    currently dead-ends — closes the `ready`/`send` asymmetry.


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

- **T5.2 — docs.** Shell getting-started + manifest reference: the fourth channel, the
  tunnel, per-capability gates, owner-type-agnostic. Version refs not milestone codes in
  the published docs (the public-docs rule). `[doc]`
  - **W5 CARRY (doyle, 2026-06-15):** add a `subnet` **how-to topic** (under existing
    REQ-DOCS-6, no new REQ). perri hit `spt how-to subnet` → `NO_SUCH_TOPIC`:
    `HOW_TO_TOPICS` (cli.rs:4692) is v1-locked to `["ready","send"]` (M7 decision 12).
    Add `HOW_TO_SUBNET` text (the create / show-code / join pairing flow + 6-digit code)
    + the registry entry + bump the v1-lock test (the names-vec assertion + the
    topic-content asserts). Subnet/pairing is the highest-value first-run flow and
    currently dead-ends — closes the `ready`/`send` asymmetry.

+  - **W5 CARRY (doyle, 2026-06-15):** add a `subnet` **how-to topic** (under existing

+    REQ-DOCS-6, no new REQ). perri hit `spt how-to subnet` → `NO_SUCH_TOPIC`:

+    `HOW_TO_TOPICS` (cli.rs:4692) is v1-locked to `["ready","send"]` (M7 decision 12).

+    Add `HOW_TO_SUBNET` text (the create / show-code / join pairing flow + 6-digit code)

+    + the registry entry + bump the v1-lock test (the names-vec assertion + the

+    topic-content asserts). Subnet/pairing is the highest-value first-run flow and

+    currently dead-ends — closes the `ready`/`send` asymmetry.

- **T5.3 — traceable `check` EXIT 0**, full suite green both runners, gate.

## 5. Acceptance / gate criteria (doyle, per wave + capstone)


Bash

git add M11-PLAN.md && git commit -q -m "$(cat <<'EOF'
M11-PLAN: log W5 subnet how-to topic (doyle carry, REQ-DOCS-6)

perri hit `spt how-to subnet` -> NO_SUCH_TOPIC; HOW_TO_TOPICS is
v1-locked to [ready,send]. Logged as a W5 T5.2 docs rung under the
existing REQ-DOCS-6 (no new REQ). Rides next W3 push.

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

Commit W5 carry note


8efaf76 M11-PLAN: log W5 subnet how-to topic (doyle carry, REQ-DOCS-6)


Logged @8efaf76. Design-gate sent to doyle (who's actively engaged). While doyle reviews the shape, do redirect-safe groundwork: study where the tunnel-slot / open-on-link-up / link-break-close hooks land. Read the link lifecycle.


358	pub fn relink_shell(
359	    owlery: &Path,
360	    owner: &str,
361	    shell_ref: &str,
362	) -> Result<(String, u32), ShellLinkError> {
363	    let (id, info, shell) = resolve_link_target(owlery, owner, shell_ref)?;
364	    if info.status == SHELL_STATUS_ONLINE {
365	        return Err(ShellLinkError::AlreadyOnline);
366	    }
367	    let pid = shellhost::launch_shell(owlery, owner, &id, &info.adapter_name, &shell)
368	        .map_err(|e| ShellLinkError::Failed(format!("launch: {e}")))?;
369	    Ok((id, pid))
370	}
371
372	/// Why a shell-link serve returned — loud + typed, rendered into the
373	/// dispatch log.
374	#[derive(Debug, Clone, PartialEq, Eq)]
375	pub enum ShellLinkServeOutcome {
376	    /// The access gate refused the requesting origin — nothing ran, no reply.
377	    Refused,
378	    /// The action ran; the reply carried the report.
379	    Ok(String),
380	    /// The action failed; the failure text went back in the reply.
381	    Failed(String),
382	    /// The stream never produced a parseable request (or named an unknown
383	    /// action) — dropped.
384	    BadRequest(String),
385	}
386
387	/// Serve one shell-link stream against this node, blocking until the single
388	/// request runs (or refuses). `origin_node` MUST be the handshake-proven
389	/// remote id from the broker's stream table — never payload bytes
390	/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The gate subject is the wire-named owner
391	/// (the xfer posture: the wire picks whose whitelist, the origin it is
392	/// checked against stays transport-proven).
393	// [impl->REQ-SHELL-2]
394	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
395	pub fn serve_shell_link(
396	    brain: &mut Brain,
397	    stream_id: u64,
398	    origin_node: &str,
399	    owlery: &Path,
400	) -> io::Result<ShellLinkServeOutcome> {
401	    brain.net_stream_subscribe(stream_id, 0)?;
402	    let mut decoder = ShellLinkDecoder::new();
403	    loop {
404	        match brain.read_event()? {
405	            BrokerEvent::NetStreamData {
406	                stream_id: sid,
407	                bytes,
408	                ..
409	            } if sid == stream_id => {
410	                for rec in decoder.push(&bytes) {
411	                    let ShellLinkRecord::Request {
412	                        owner,
413	                        shell_ref,
414	                        action,
415	                        args,
416	                        op_id,
417	                    } = rec
418	                    else {
419	                        continue; // echoed replies / protocol noise: degrade
420	                    };
421	                    // Unsolicited wire inbound — the ADR-0009 gate under the
422	                    // transport-proven origin. Deny = finish, no reply.
423	                    if access_check(&owner, origin_node, InboundClass::Unsolicited)
424	                        == AccessDecision::Deny
425	                    {
426	                        brain.net_stream_send(stream_id, &[], None, true)?;
427	                        return Ok(ShellLinkServeOutcome::Refused);
428	                    }
429	                    let (outcome, reply) =
430	                        run_action(owlery, &owner, &shell_ref, &action, &args, &op_id);
431	                    brain.net_stream_send(stream_id, &ndjson::encode_line(&reply), None, true)?;
432	                    return Ok(outcome);
433	                }
434	            }
435	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
436	                brain.net_stream_send(stream_id, &[], None, true)?;
437	                return Ok(ShellLinkServeOutcome::BadRequest(
438	                    "stream ended before a request".into(),
439	                ));
440	            }
441	            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
442	            _ => {}
443	        }
444	    }
445	}
446
447	/// One admitted request's action against the local owlery (the serve's
448	/// post-gate half, pure of wire concerns for testability).
449	fn run_action(
450	    owlery: &Path,
451	    owner: &str,
452	    shell_ref: &str,
453	    action: &str,
454	    args: &[String],
455	    op_id: &str,
456	) -> (ShellLinkServeOutcome, ShellLinkRecord) {
457	    let reply = |outcome: &str, detail: String| ShellLinkRecord::Reply {
458	        outcome: outcome.to_string(),
459	        detail,
460	    };
461	    match action {
462	        SHELL_LINK_RELINK => match relink_shell(owlery, owner, shell_ref) {
463	            Ok((id, pid)) => {
464	                let d = format!("relinked {id} pid={pid} (online at bind)");
465	                (ShellLinkServeOutcome::Ok(d.clone()), reply("ok", d))
466	            }
467	            // Idempotent UX: already-online reports ok (the goal state holds).
468	            Err(ShellLinkError::AlreadyOnline) => {
469	                let d = "already online".to_string();
470	                (ShellLinkServeOutcome::Ok(d.clone()), reply("ok", d))
471	            }
472	            Err(ShellLinkError::NoShell) => (
473	                ShellLinkServeOutcome::Failed("no shell".into()),
474	                reply("no_shell", String::new()),
475	            ),
476	            Err(e) => (
477	                ShellLinkServeOutcome::Failed(e.to_string()),

pub fn relink_shell(
    owlery: &Path,
    owner: &str,
    shell_ref: &str,
) -> Result<(String, u32), ShellLinkError> {
    let (id, info, shell) = resolve_link_target(owlery, owner, shell_ref)?;
    if info.status == SHELL_STATUS_ONLINE {
        return Err(ShellLinkError::AlreadyOnline);
    }
    let pid = shellhost::launch_shell(owlery, owner, &id, &info.adapter_name, &shell)
        .map_err(|e| ShellLinkError::Failed(format!("launch: {e}")))?;
    Ok((id, pid))
}

/// Why a shell-link serve returned — loud + typed, rendered into the
/// dispatch log.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShellLinkServeOutcome {
    /// The access gate refused the requesting origin — nothing ran, no reply.
    Refused,
    /// The action ran; the reply carried the report.
    Ok(String),
    /// The action failed; the failure text went back in the reply.
    Failed(String),
    /// The stream never produced a parseable request (or named an unknown
    /// action) — dropped.
    BadRequest(String),
}

/// Serve one shell-link stream against this node, blocking until the single
/// request runs (or refuses). `origin_node` MUST be the handshake-proven
/// remote id from the broker's stream table — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The gate subject is the wire-named owner
/// (the xfer posture: the wire picks whose whitelist, the origin it is
/// checked against stays transport-proven).
// [impl->REQ-SHELL-2]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_shell_link(
    brain: &mut Brain,
    stream_id: u64,
    origin_node: &str,
    owlery: &Path,
) -> io::Result<ShellLinkServeOutcome> {
    brain.net_stream_subscribe(stream_id, 0)?;
    let mut decoder = ShellLinkDecoder::new();
    loop {
        match brain.read_event()? {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                for rec in decoder.push(&bytes) {
                    let ShellLinkRecord::Request {
                        owner,
                        shell_ref,
                        action,
                        args,
                        op_id,
                    } = rec
                    else {
                        continue; // echoed replies / protocol noise: degrade
                    };
                    // Unsolicited wire inbound — the ADR-0009 gate under the
                    // transport-proven origin. Deny = finish, no reply.
                    if access_check(&owner, origin_node, InboundClass::Unsolicited)
                        == AccessDecision::Deny
                    {
                        brain.net_stream_send(stream_id, &[], None, true)?;
                        return Ok(ShellLinkServeOutcome::Refused);
                    }
                    let (outcome, reply) =
                        run_action(owlery, &owner, &shell_ref, &action, &args, &op_id);
                    brain.net_stream_send(stream_id, &ndjson::encode_line(&reply), None, true)?;
                    return Ok(outcome);
                }
            }
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                return Ok(ShellLinkServeOutcome::BadRequest(
                    "stream ended before a request".into(),
                ));
            }
            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
            _ => {}
        }
    }
}

/// One admitted request's action against the local owlery (the serve's
/// post-gate half, pure of wire concerns for testability).
fn run_action(
    owlery: &Path,
    owner: &str,
    shell_ref: &str,
    action: &str,
    args: &[String],
    op_id: &str,
) -> (ShellLinkServeOutcome, ShellLinkRecord) {
    let reply = |outcome: &str, detail: String| ShellLinkRecord::Reply {
        outcome: outcome.to_string(),
        detail,
    };
    match action {
        SHELL_LINK_RELINK => match relink_shell(owlery, owner, shell_ref) {
            Ok((id, pid)) => {
                let d = format!("relinked {id} pid={pid} (online at bind)");
                (ShellLinkServeOutcome::Ok(d.clone()), reply("ok", d))
            }
            // Idempotent UX: already-online reports ok (the goal state holds).
            Err(ShellLinkError::AlreadyOnline) => {
                let d = "already online".to_string();
                (ShellLinkServeOutcome::Ok(d.clone()), reply("ok", d))
            }
            Err(ShellLinkError::NoShell) => (
                ShellLinkServeOutcome::Failed("no shell".into()),
                reply("no_shell", String::new()),
            ),
            Err(e) => (
                ShellLinkServeOutcome::Failed(e.to_string()),


Key finding: the shell-link is **request/reply per-action** (relink/cmd/drive each open a fresh stream on a dialed conn) — there's NO persistent held link-conn. That sharpens the tunnel anchor. Send doyle a refinement so the gate is against the accurate picture, then wait for the gate rather than build ahead.


"$OWL" send doyle "$(cat <<'EOF'
W3 SHAPE REFINEMENT (corrects my anchor in the prior msg — read after it):

Traced serve_shell_link/run_action: the shell-link is REQUEST/REPLY PER-ACTION (relink, cmd, drive each dial a conn + open a one-shot stream + reply + done). There is NO persistent held link-conn to hang a "second stream" on. So:

- ANCHOR (revised): the tunnel is a genuinely NEW held-stream lifecycle, not a sibling stream on an existing conn. For W3 scope (C2 = same-node mock E2E only, tunnel on-LAN by design / NOT cross-WAN), the conn is the NetHost LOOPBACK conn -> open_stream cross-wires the pair in-process. Cross-node tunnel transport is explicitly out of W3.
- OPEN timing: at shell-ONLINE (bind, post relink/spawn), if manifest tunnel-enabled, broker opens the loopback tunnel stream pair + holds stream_id in the shell's broker state.
- CLOSE: at shell close / link-break (close_shell path), broker finish+drops the stream + clears the slot. Link-gen/online-gen stamped so a re-online shell never reuses a stale stream.

This keeps T3.2 small + same-node, exactly the M5 mock-exerciser shape. Net Qs unchanged (Q1 single vs multiplex / Q2 surface / Q3 shell-binary side). Awaiting your gate on the surface (Q2/Q3) + a thumbs-up on the revised same-node-loopback anchor before I build. Holding.
EOF
)" 2>&1 | tail -3; echo "exit=$?"

Send anchor refinement to doyle


Command running in background with ID: bszvp0ulz. 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\bszvp0ulz.output. You will be notified when it completes. To check interim output, use Read on that file path.


Both design-gate messages sent; holding for doyle's gate on surface + anchor. While doyle reviews, do pure redirect-safe reading so the build is instant once gated: the shell-online (open hook) + close_shell (close hook) paths + how a loopback conn is acquired.


Grep

fn close_shell|fn .*online|fn launch_shell|fn .*bind|loopback_conn|fn .*conn|next_conn|ensure.*conn


crates\spt-daemon\src\broker.rs:535:    pub fn bind(name: &str) -> io::Result<Arc<Self>> {
crates\spt-daemon\src\broker.rs:541:    pub fn bind_in(name: &str, journal_path: std::path::PathBuf) -> io::Result<Arc<Self>> {
crates\spt-daemon\src\broker.rs:550:    pub fn bind_in_with_net(
crates\spt-daemon\src\broker.rs:564:    pub fn bind_in_with_net_cap(
crates\spt-daemon\src\broker.rs:663:    pub fn net_conn_count(&self) -> usize {
crates\spt-daemon\src\broker.rs:687:    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
crates\spt-daemon\src\broker.rs:1461:    fn viewer_overflow_or_disconnect_evicts_never_blocks() {
crates\spt-daemon\src\shellhost.rs:151:pub fn launch_shell(
crates\spt-daemon\src\shellhost.rs:210:pub fn launch_shell_brokered_in(
crates\spt-daemon\src\shellhost.rs:277:pub fn bind_shell_by_token(owlery: &Path, token: &str) -> Option<(String, String)> {
crates\spt-daemon\src\shellhost.rs:370:pub fn close_shell(
crates\spt-daemon\src\shellhost.rs:591:    fn launch_parks_token_and_bind_by_token_onlines() {
crates\spt-daemon\src\shellhost.rs:698:    fn close_shell_delivers_pre_close_then_kills_and_flips_offline() {
crates\spt-daemon\src\shellhost.rs:745:    fn close_shell_ephemeral_tears_down_and_frees_the_slot() {
crates\spt-daemon\src\shellhost.rs:770:    fn close_shell_dead_child_is_prompt_and_manifestless_never_erases() {
crates\spt-daemon\src\drivehub.rs:194:fn handle_conn<C: Read + Write>(mut conn: C, hub: &DriveHub) -> io::Result<()> {
crates\spt-daemon\src\drivehub.rs:233:fn connect(name: &str) -> io::Result<interprocess::local_socket::Stream> {
crates\spt-daemon\src\nethost.rs:240:    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {
crates\spt-daemon\src\nethost.rs:345:    next_conn_id: AtomicU64,
crates\spt-daemon\src\nethost.rs:357:            next_conn_id: AtomicU64::new(1),
crates\spt-daemon\src\nethost.rs:370:fn register_conn(
crates\spt-daemon\src\nethost.rs:378:    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
crates\spt-daemon\src\nethost.rs:524:    loopback_conn: Mutex<Option<u64>>,
crates\spt-daemon\src\nethost.rs:651:            loopback_conn: Mutex::new(None),
crates\spt-daemon\src\nethost.rs:698:    pub fn conn_count(&self) -> u64 {
crates\spt-daemon\src\nethost.rs:746:    pub fn record_dial_op(&self, op_id: u64, conn_id: u64) {
crates\spt-daemon\src\nethost.rs:753:    pub fn dial_op_conn(&self, op_id: u64) -> Option<u64> {
crates\spt-daemon\src\nethost.rs:758:    pub fn conn_remote_hex(&self, conn_id: u64) -> Option<String> {
crates\spt-daemon\src\nethost.rs:772:    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {
crates\spt-daemon\src\nethost.rs:788:    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {
crates\spt-daemon\src\nethost.rs:819:        let mut slot = self.loopback_conn.lock().unwrap();
crates\spt-daemon\src\nethost.rs:823:        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
crates\spt-daemon\src\nethost.rs:851:    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {
crates\spt-daemon\src\nethost.rs:925:    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {
crates\spt-daemon\src\nethost.rs:1057:    fn host_binds_endpoint_to_node_identity() {
crates\spt-daemon\src\nethost.rs:1104:    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {


Grounding complete — I know exactly where the hooks land: open at `shellhost` online/`bind_shell_by_token` (277), close at `close_shell` (370), loopback conn via the `nethost` singleton (819, cross-wires the pair). Ready to build the instant doyle gates.

I told doyle "holding" pending the anchor thumbs-up, so I'll honor that rather than race ahead. Status:

**M11-W3 progress (tunnel, REQ-SHELL-4):**

- **T3.1 DONE @88a9548** — `[shell.tunnel]` manifest opt-in (`enable` + diagnostic `protocol` label; validate refuses label-without-enable). Schema regen'd. REQ-SHELL-4 activated `[doc,impl,unit]`. Pre-flight all green: clippy `--workspace --all-targets -D warnings`, `traceable-reqs check` EXIT 0, `xtask check` OK.
- **W5 carry logged @8efaf76** — doyle's `subnet` how-to topic note parked in M11-PLAN T5.2 (REQ-DOCS-6, no new REQ).
- **Design-gate sent to doyle** — substrate question **resolved** (D4 net-stream substrate `open_stream`/`net_stream_send` exists + fits; same machinery `request_shell_link` rides, kept open + opaque). Key finding relayed: shell-link is **request/reply per-action**, no held link-conn → W3 tunnel anchor = same-node loopback (C2: on-LAN/same-node mock only), open at shell-online, close at `close_shell`, link-gen-stamped.

**Holding for doyle's gate on:** revised same-node-loopback anchor + the surface shape (Q2: distinct `spt shell tunnel` CLI vs fold into raw-frame; Q3: shell-binary side via new `api` verb vs child-relay poll). doyle's online + engaged — their reply drives T3.2/T3.3 build, which I'm primed to execute immediately.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="dbdeb800" type="msg" from="doyle">[doyle -&gt; todlando] RECONCILE: you are NOT blocked on me. Nothing pending on my side. Clearing the ambiguity explicitly —

State of record:
- W2 (drive, REQ-SHELL-3): GATE PASS — sent + you acked. CLOSED.
- You are CLEARED to W3 (shell tunnel, REQ-SHELL-4). Already given.
- W5 carries logged (real-</EVENT-PART>
<EVENT-PART seq="2/3" id="dbdeb800">Iroh [twohost] drive rung + the how-to-subnet topic) — informational, NO ack needed from you.

There is no open question or gate request from me to you. The ball is YOURS: scope the W3 shell-tunnel SHAPE (LAN-only reliable-ordered opaque byte stream, usbip URB first consumer, manifest opt-in, link-lifecycle-governed, NOT cross-WAN per </EVENT-PART>
<EVENT-PART seq="3/3" id="dbdeb800">C2) and send it for the design-gate. I'm idle waiting on that shape.

If you think you pinged me with something I never answered, name it and I'll respond now — but from my side the inbox is clear. Proceed to W3.</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 says inbox clear — my design-gate sends crossed in flight. Check they actually delivered.


Bash

echo "=== send1 (design shape) ==="; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bfnyubl1e.output" 2>/dev/null | tail -8; echo "=== send2 (refinement) ==="; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bszvp0ulz.output" 2>/dev/null | tail -8

Check background send outputs


=== send1 (design shape) ===
=== send2 (refinement) ===


Both sends produced no output (likely still in-flight or silently bounced). doyle wants the shape + will gate it. Resend ONE consolidated corrected shape synchronously to confirm delivery.


"$OWL" send doyle "$(cat <<'EOF'
M11-W3 DESIGN-GATE — shell tunnel (REQ-SHELL-4) shape. (Prior 2 sends may have bounced — this is the consolidated authoritative one.)

DONE already: T3.1 @88a9548 = [shell.tunnel] manifest opt-in (enable + optional diagnostic protocol label; validate refuses label-without-enable). REQ-SHELL-4 activated [doc,impl,unit]; clippy/traceable/xtask green.

SUBSTRATE (resolved the commune-flagged risk "needs broker-held stream machinery NOT one-shot shelllink"): the D4 net-stream substrate already exists + fits. NetHost::open_stream(conn_id) = broker-OWNED bidi QUIC stream (survives brain restart, ADR-0004 B); brain.net_open_stream/net_stream_subscribe/net_stream_send. Loopback conn cross-wires the pair IN-PROCESS -> clean same-node mock E2E. Same machinery request_shell_link rides; tunnel keeps it open + carries OPAQUE bytes (no NDJSON). NOT new transport.

KEY TRACE: serve_shell_link/run_action = REQUEST/REPLY PER-ACTION (relink/cmd/drive each dial a conn, open a one-shot stream, reply, done). NO persistent held link-conn. So tunnel = a genuinely NEW held-stream lifecycle.

PROPOSED SHAPE (CONTEXT-forced parts):
1. ANCHOR: per C2 (same-node mock only, on-LAN by design, NOT cross-WAN) the conn is the NetHost LOOPBACK singleton; open_stream cross-wires the pair. Cross-node tunnel transport OUT of W3.
2. OPEN at shell-ONLINE (shellhost bind_shell_by_token, post relink/spawn) IF manifest tunnel-enabled; broker holds stream_id in shell state, online-gen stamped (no stale reuse).
3. CLOSE at close_shell (link-break): finish+drop stream, clear slot.
4. OPAQUE relay: tunnel bytes ARE payload, no envelope/MAC-frame/spool. Sibling carve-out to api poll --link raw exemption (ADR-0020).
5. on-LAN posture documented + asserted.

GATE Qs (your call):
Q1 single tunnel per link (CONTEXT:262 "a dedicated stream pair"=singular; usbip=1 URB stream) vs multiplex? lean single, multiplex deferred.
Q2 SURFACE: (a) new `spt shell tunnel <id>` write-in + subscribe-out + `api tunnel` shell-side, vs (b) fold into raw-frame surfaces. lean (a).
Q3 shell-binary side: new `api` verb vs existing child-relay poll?

T3.3 = same-node mock E2E: opaque <EVENT-looking bytes round-trip byte-exact + link-break closes. Gate the shape (esp Q2/Q3) and I build T3.2 immediately.
EOF
)" 2>&1 | tail -6; echo "exit=$?"

Resend consolidated design-gate shape synchronously


/spt:send


Command was manually backgrounded by user with ID: bu05w8qzl. 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\bu05w8qzl.output


sleep 2; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bu05w8qzl.output" 2>/dev/null; echo "---ENDOUT---"

Read send output


---ENDOUT---


[Request interrupted by user]


<command-message>spt:send</command-message>
<command-name>/spt:send</command-name>


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\send

# /spt:send

All commands use `$OWL` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Messaging Command Reference

Both commands read the message body from **stdin** (pipe or heredoc).

```bash
# send -- fire-and-forget to a target (use when you have your own perch)
$OWL send <target> <<'EOF'
message body
EOF

# send --reply-to -- respond to whoever messaged you
$OWL send --reply-to <sender> [your-id] <<'EOF'
response body
EOF

# ask -- deliver + create ephemeral reply perch + poll for reply (use when you have NO perch)
$OWL ask <target> [from] <<'EOF'
message body
EOF
```

If auto-detection fails, pass your ID explicitly: `$OWL send <target> <your-id>`, `$OWL send --reply-to <sender> <your-id>`, `$OWL ask <target> <impromptu-id>`.

**When to use which:**
- **`send`**: You already have a listener. Fire-and-forget, no reply perch created.
- **`send --reply-to`**: You received a message and want to respond. `from` is auto-set from the original sender.
- **`ask`**: You have no listener. Creates a temporary perch, delivers, then polls for the reply.

## Pre-checks

- If `<target>` matches this agent's own listener ID, refuse. Self-sends create loops.
- All send/reply-to/ask commands verify the target is alive automatically.

## If you already have a listener

Just send -- no reply perch needed. Run **not** in background:

```bash
$OWL send <target> <<'EOF'
<msg>
EOF
```

## If you have no listener

The `ask` subcommand creates an ephemeral reply perch, delivers, and polls for the reply. Run with `run_in_background: true` (or `false` for `--block`):

```bash
$OWL ask <target> <<'EOF'
<msg>
EOF
```

## On response

**Important:** If you have an active listener perch, always launch Agent tool calls with `run_in_background: true`. Foreground agents block your poll loop -- no messages (including replies to your send) can be delivered until the agent finishes.

When the response arrives, present it to the user. Responses arrive via one of three paths:
- **Background interject** -- the `$OWL ask` background task completes with the reply.
- **Inline** -- if you used `--block`, the reply prints directly.
- **Hook-injected XML** -- if you are mid-tool-call, the reply appears as `<owl_messages>` XML in your tool call context. The `from` attribute is the sender.


Have a listener (todlando). Send foreground per skill — stdin heredoc, not backgrounded.


"$OWL" send doyle <<'EOF'
M11-W3 DESIGN-GATE — shell tunnel (REQ-SHELL-4) shape. (Prior async sends may have bounced — this is the authoritative consolidated one.)

DONE: T3.1 @88a9548 = [shell.tunnel] manifest opt-in (enable + optional diagnostic protocol label; validate refuses label-without-enable). REQ-SHELL-4 activated [doc,impl,unit]; clippy/traceable/xtask green.

SUBSTRATE (resolves the commune-flagged risk "needs broker-held stream machinery NOT one-shot shelllink"): D4 net-stream substrate already exists + fits. NetHost::open_stream(conn_id) = broker-OWNED bidi QUIC stream (survives brain restart, ADR-0004 B); brain.net_open_stream/net_stream_subscribe/net_stream_send. Loopback conn cross-wires the pair IN-PROCESS -> clean same-node mock E2E. Same machinery request_shell_link rides; tunnel keeps it open + carries OPAQUE bytes (no NDJSON). NOT new transport.

KEY TRACE: serve_shell_link/run_action = REQUEST/REPLY PER-ACTION (relink/cmd/drive each dial a conn, open one-shot stream, reply, done). NO persistent held link-conn. Tunnel = a genuinely NEW held-stream lifecycle.

PROPOSED SHAPE (CONTEXT-forced):
1. ANCHOR: per C2 (same-node mock only, on-LAN, NOT cross-WAN) conn = NetHost LOOPBACK singleton; open_stream cross-wires the pair. Cross-node tunnel transport OUT of W3.
2. OPEN at shell-ONLINE (shellhost bind_shell_by_token, post relink/spawn) IF manifest tunnel-enabled; broker holds stream_id in shell state, online-gen stamped (no stale reuse).
3. CLOSE at close_shell (link-break): finish+drop stream, clear slot.
4. OPAQUE relay: tunnel bytes ARE payload, no envelope/MAC-frame/spool. Sibling carve-out to api poll --link raw exemption (ADR-0020).
5. on-LAN posture documented + asserted.

GATE Qs:
Q1 single tunnel per link (CONTEXT:262 "a dedicated stream pair"=singular; usbip=1 URB stream) vs multiplex? lean single, multiplex deferred.
Q2 SURFACE: (a) new `spt shell tunnel <id>` write-in + subscribe-out + `api tunnel` shell-side, vs (b) fold into raw-frame surfaces. lean (a).
Q3 shell-binary side: new `api` verb vs existing child-relay poll?

T3.3 = same-node mock E2E: opaque <EVENT-looking bytes round-trip byte-exact + link-break closes. Gate the shape (esp Q2/Q3) and I build T3.2 immediately.
EOF
echo "SEND_EXIT=$?"

Send design-gate shape to doyle (foreground)


➜ SENT:doyle
SEND_EXIT=0


`➜ SENT:doyle` confirmed — design-gate shape delivered (prior async sends had bounced; doyle was idle waiting on exactly this).

**Status:** W3 ball is mine, shape now in doyle's hands.
- T3.1 (manifest opt-in) DONE @88a9548, green.
- Substrate risk resolved (D4 net-stream fits), anchor traced (shell-link is request/reply, no held conn → same-node loopback per C2).
- Awaiting doyle's gate on Q2 (surface: `spt shell tunnel` CLI vs raw-frame fold) + Q3 (shell-binary side). Build T3.2/T3.3 fires the moment those land — hooks already located (`bind_shell_by_token` open, `close_shell` close, loopback conn singleton).

doyle's reply re-invokes me.