# M5-D8 — the real shell: OS-notification shell (JIT plan)

**Status:** CLOSED 2026-06-04 — D8a (standalone `spt-shell-notify` + CI hook),
D8b (template seam generalized), D8c (cross-node link + discovery leg) all
shipped; see the revised decisions + pieces below. The dogfood toast demo +
rig legs ride D9a. Upstream: D7 closed (`f0d032c`; D7c ladder green
run 26994211671 — see `M5-D7-PLAN.md`). Authoritative: M5-PLAN §D8 + coverage map
(D8 row: REQ-EP-5/SHELL-1/2 + REQ-NOTIF-2 **int evidence** via the real shell — no
new activations here; `required_stages` flips happen at the D9b sweep), CONTEXT.md
"Shell model" / "Shell sleep/wake", ADR-0007 (notif), ADR-0010 (adapter choice).

## Researched code map (explorer-verified 2026-06-04)

- **Shell hosting (proven, D3):** `shellhost.rs:49-315` — link-token mint /
  `link_key` / per-frame HMAC; `fill_spawn_command` (`{link_token}`, `{id}`,
  `{adapter_name}` keys); `launch_shell` detached-no-inherit (KH 5.6), receipt
  modes relay (poll-drain) + stdin (brokered), http refused;
  `bind_shell_by_token` onlines the perch; `verify_link` gates `api emit/poll`;
  link-break close = `pre_close` template + termination window + force-kill.
  E2E: `crates/spt/tests/shell_e2e.rs` (spawn→bind→command→sensory→teardown),
  `shellchan.rs` (stdin receipt).
- **Sleep/wake (proven, D4):** `shellwake.rs:41-299` — `WAKE_OPCODE=86`
  watcher supervision (exp backoff + give-up latch, `WakeSet` mutual
  exclusivity), state-keyed wake resolution (dormant/suspended/active /
  no-local-instance → `forward_wake` via D5b). `shell_sleepwake_e2e.rs` drives
  the full cycle. `shellwake.rs:281` comment: "cross-node shell *link* is D8c".
- **Mock shell (the contract D8a re-implements for real):**
  `adapters/mock/src/shell.rs` — `api bind-shell --link` → `api poll --link`
  drain → `api emit --type sighted --link` → park. A real binary replaces the
  evidence-file writer with an actual OS render.
- **Manifest seam:** `manifest.rs:48-49` `shell: Option<Shell>` iff
  `kind="shell"`; fields spawn/broadcast/require_approval/persistent/
  command_receipt/pre_close/close_timeout_ms/wake_command/can_shutdown/
  `[shell.capabilities.<op>]`/`[shell.sensory]`. Registration:
  `registry.rs:118-150` (manifest-first validation; invalid records nothing).
- **notif_command seam (exists, UNCALLED):** `notif.rs:236` `NOTIF_ROLE`,
  `notif.rs:243` `notif_keys` (`{notif_id}/{notif_from}/{notif_subnet}/
  {notif_body}`), `notif.rs:257-262` `spawn_notif_command` — detached,
  fire-and-forget, opt-in via manifest `[session.notif]`. **No production
  caller** — D8b supplies it.
- **Presence MRA (proven, D6):** `presence.rs:100-149` `PresenceTarget`
  (winner = max last_active_ms across local live perches + registry rows;
  `local: Option<ConsentTarget>`); `notif.rs:187` `first_fire_at` consumes it
  (local ⇒ deliver+mark, remote ⇒ `RemoteTarget` rides replication; the
  winning node's feed-apply surfaces).
- **Discovery (D8c seam):** `shelldisc.rs:59-74` `discover()` — own instances
  + instantiable registered adapters; comment at :10-11 names the D8c
  extension: other-node registry rows join when cross-node link lands.

## Decisions (locked — REVISED after the 2026-06-04 design review)

- **D8a adapter = `SaberMage/spt-shell-notify`, its own PUBLIC repo** (revised
  from in-tree: the adapter model's whole point is that the manifest + binary
  are the only glue — zero spt-core source integration; the mock stays the
  only adapter-shaped artifact in-tree, PRD R-DOCS-2). Self-contained: no
  spt-core crate deps — the binary speaks the public `spt api` surface and
  hand-decodes the documented EVENT envelope format (amp-last). Contract:
  `kind="shell"`, `broadcast = "same-node"`, `persistent = true`,
  `require_approval = "remembered"`, relay receipt, one capability
  `notify(title, body)`, `wake_command = "--wake"` (settle + exit 86),
  `[session.notif]` render template (see D8b). spt-core keeps an
  **env-gated** E2E (`SPT_NOTIFY_SHELL_BIN`, silent-skip — the twohost
  pattern) plus a thin CI hook: checkout the adapter repo, build, run the
  gated leg. The in-tree placement (`642ae81`) was reverted.
- **The binary renders via OS facilities, no deps:** Windows — spawned
  `powershell` WinRT toast snippet (headless-safe); Linux — `notify-send`;
  failures logged, never fatal. `--render-file` is the test observable.
- **D8b = the `[session.notif]` template seam GENERALIZED to shell adapters**
  (revised from a command-channel drive that hardcoded the `notify`
  capability name in the daemon — rejected: spt-core must never know a shell
  vocabulary verb; the manifest is the glue). At a LOCAL surface
  (`first_fire_at` Fired + boundary-resurface Surfaced arms), the surfacing
  node spawns the `[session.notif]` template of every registered SHELL
  adapter with an instance attached to the surfaced endpoint
  (`notif::render_via_shell_notif_templates` — one spawn per adapter,
  instance status not consulted: the template is a one-shot process
  independent of the resident binary). Entirely manifest-driven and
  vocabulary-agnostic; fire-and-forget (ADR-0007: a failed render never
  fails the surface). The harness-adapter `[session.notif]` path
  (reporting.rs boundary arm) is unchanged — same seam, now honored for both
  adapter kinds. The shell's `notify` COMMAND capability stays purely
  agent-driven (`spt shell cmd … notify`), untouched by core. `spt notify` →
  first-fire at MRA winner → template spawn → toast on that node: the
  acceptance demo.
- **D8c = relink + cross-node command drive, spawn stays same-node (M5 scope
  decision 2).** Two legs: (1) discovery join — `shelldisc.discover()` gains
  the subnet-registry leg (other-node instances of `broadcast="subnet"`
  adapters list as relinkable; `same-node` ones list only on their node);
  (2) the link leg — an owner active on node B issues the relink, the
  daemon routes the shell command channel over the existing iroh wire to node
  A's daemon (the D5b/D6 remote-op pattern: REST-op request/serve pair), node
  A's daemon drives its local link (wake the binary via the D4 watcher if
  offline, then deliver). Sensory stays REST-only/dropped-unless-owner-live —
  cross-node sensory rides the same conn, dropped at the source when the
  owner isn't live there. Rig (real two-host) leg of all of this is **D9a**;
  D8c proves it loopback (two daemons, one host) like attach/dispatch tests.
- **Evidence:** D8a tags `[impl->REQ-EP-5]`/`[unit->REQ-EP-5]` (instantiation
  contract via a real adapter) + the binary's link conformance under
  `[impl->REQ-SHELL-1]`; D8b tags `[impl->REQ-NOTIF-2]`/`[unit->REQ-NOTIF-2]`
  + `[int->REQ-NOTIF-2]` (loopback: produce → MRA → shell command observed);
  D8c tags `[impl->REQ-SHELL-2]`/`[int->REQ-SHELL-1]` (cross-daemon relink
  drive). No `required_stages` edits until D9b.

## Pieces (build order)

1. **D8a — adapter + binary.** DONE (revised): standalone repo
   `SaberMage/spt-shell-notify` (manifest + self-contained bin, unit tests for
   frame decode + PS embedding); spt-core side = env-gated
   `notify_shell_e2e.rs` (spawn → bind → vocabulary-checked notify command →
   `--render-file` observable → foreign-op refusal → teardown) + the ci.yml
   cross-repo hook. The in-tree first cut (`642ae81`) reverted in the same
   slice.
2. **D8b — surface → endpoint-native render.** DONE (revised): the
   `[session.notif]` seam generalized to shell adapters
   (`render_via_shell_notif_templates` at both surface arms); unit = attached
   template-bearing shell renders with keys filled, template-less /
   unattached / shell-less all silent no-ops. The dogfood toast leg (real
   `spt notify` → toast) rides the CI-hook E2E + D9a rig.
3. **D8c — cross-node relink.** DONE: `spt-net::shelllink` wire records
   (`kind=request` + `owner`+`shell_ref` demux shape) +
   `spt-daemon::linkhost` — the serve/request pair mirroring the D5b rest
   pattern (access gate on the handshake-proven origin, subject = the
   wire-named owner; owner exclusivity = resolution scope), with the local
   action core (`drive_shell`/`relink_shell`) extracted so the CLI and the
   wire serve run the IDENTICAL paths. Remote cmd against an offline
   persistent instance wakes the binary first (relaunch), then spools;
   stdin-receipt frames push best-effort. CLI: `spt shell cmd|relink
   <ref>@<node>` resolves the owner's instance at the shell's node
   (wansend discipline) and ships the verb. Discovery registry leg:
   additive `Instance.shell_adapters` datum (the D6a presence pattern —
   rides the rows, same epoch lease, pre-D8 rows parse clean) advertised
   from the registered set; `shelldisc::discover_across` joins other-node
   `broadcast=subnet` adapters into discovery + the boundary context render
   (caveated: spawn stays deferred). Loopback int test
   (`dispatcher_serves_a_cross_node_shell_link`): relink relaunches at "B",
   the command frame lands MAC-stamped under B's fresh token, foreign owner
   refuses. Rig leg rides D9a.

## NOT in D8
- Instantiate-anywhere / cross-node SPAWN (scope decision 2 — spawn stays
  same-node). GameRobot-class shells. PresenceChannel. `required_stages`
  flips (D9b). The rig two-host legs (D9a). HTTP command receipt.

## Conventions (carried)
- NO `cargo fmt`; tag evidence in the same commit; `traceable-reqs check` before done.
- Linux clippy `-D warnings` + `--no-default-features` stay green.
- Push per slice → sha-pinned FINAL; never push over an in-flight run.
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
