# M5 Plan — shells, presence, deferred capabilities

> **✅ M5 COMPLETE (2026-06-04).** D0–D9 all closed (per-task JIT plans
> `M5-D1…D9-PLAN.md`); the acceptance demo ran on the real rig — `[twohost]`
> run `26998058816`, the full 11-rung ladder green with the REAL standalone
> notify adapter (`docs/TWO-HOST-RUNBOOK.md` M5 evidence appendix). The D9b
> sweep flipped REQ-EP-5/SHELL-1/NOTIF-2 `int` on; REQ-CONSENT int +
> REQ-INSTALL-4 int deferred with rationale (`docs/DEFERRED.md`).
> `ROADMAP.md` carries the delivered summary.

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a/M3b/M3c/M4-PLAN.md`.
> Task layer authored in full 2026-06-04 at M5 start (see §Tasks).
> Branch: `dev-freeform`. Authoritative architecture: **CONTEXT §Shell model**,
> **CONTEXT §Consent & security gates**, **CONTEXT §Instances**,
> `docs/MANIFEST.md` §Shell adapters (`kind = "shell"`), ADR-0004 (daemon hosting),
> ADR-0009 (gate nesting), and `ROADMAP.md` §M5.

> **Upstream is done:** **M4 COMPLETE (2026-06-04)** — `spt-net` (WAN/pairing/registry),
> the multi-instance model (resting states, resources, home+fork), cross-node Psyche
> sync, WAN msg/remote-drive/xfer, the notif primitive, update peer-propagation, and the
> production trigger loops, proven on the real two-host rig (CI `26958175812`). M5 builds
> the *driven-surface* layer on those substrates: shells ride the broker process table +
> typed/binary payloads; presence rides the registry distribution; consent rides the
> security-material plumbing the access whitelist (REQ-SEC-1) already shaped.

## Scope decisions (user, 2026-06-04 — resolve the ROADMAP §M5 ↔ PRD/DEFERRED forks)

1. **Presence: resolution only.** Presence gossip (`last_active_node, last_active_endpoint, ts`
   subnet-wide) + a first-class most-recently-active resolution consumed by notif/update/
   consent/wake routing. The PresenceChannel *endpoint* (dispatch/bind/thread styles, the
   durable shell-agnostic thread) stays **deferred past v1** (DEFERRED.md stance holds).
2. **Consent: framework seam only.** The real grant store + interactive escalation ship;
   **both gated capabilities stay deferred** — remote command execution (REQ-REACH-2) *and*
   instantiate-anywhere. Consequence: the **remote fork arm** (cross-node `spt fork`
   placement) defers with instantiate-anywhere, despite the ROADMAP §M5 carried-seam
   listing — ROADMAP wording amended at D0.
3. **Shells: machinery + mock + one real shell.** Full shell hosting machinery proven by
   the generic mock shell adapter, **plus one minimal real shell — the OS-notification
   shell** (Windows toast / Linux `notify-send`) as the dogfood proof. This deliberately
   pulls one concrete shell out of the DEFERRED.md concrete-shells row; GameRobot-class
   surfaces stay deferred. Cross-node **spawn** stays deferred (rides instantiate-anywhere);
   cross-node **link** of an existing shell (owner active on another node) is in scope —
   it is part of the locked model ("an agent and its shell can link across nodes").
4. **Process:** plan authored directly from these decisions; further forks surfaced
   per-task as they arise.

## Goal

An agent embodies itself in driven surfaces: `spt shell spawn` mints an owner-exclusive
shell instance hosted by the daemon (broker-launched binary, three typed channels,
approval/cap gates), full sleep/wake from either end (owner cascade + wake-watcher), with
subnet presence resolution routing user-directed traffic to the right place — proven by a
real OS-notification shell rendering an agent's `spt notify` as a native toast on the
user's active node, and by the carried M4 seams going live (deferred-message resting
gate, remote suspend, relay rendezvous, notif-ack→apply orchestration, cross-runner CI).

## Scope

### In

- **Consent framework seam** (CONTEXT §Consent & security gates): grant store
  (`capability × agent × target-node`, target-enforced, subnet-settable, revocable, lives
  with security material near the trust store) + interactive escalation (consent prompt →
  most-recently-active session; allow-once / allow-always / deny) + pre-consent flag
  authoring (`can_shutdown`, `shell_wake_spawn_anywhere` as manifest/settings-authored
  grants). The two gated capability ids are **reserved, not implemented**.
- **Adapter registration lifecycle** (`REQ-INSTALL-4`): `spt adapter add <path|--github>`
  (manifest-first; install-is-first-update via the declared `[update]` avenue), soft
  `remove` + optional `uninstall` template; one command for `kind="harness"` and
  `kind="shell"`; the registered set feeds creation-time adapter selection, shell
  discovery, and the R-UPD-5 ripple.
- **Shell hosting machinery** (`REQ-EP-5` + new `REQ-SHELL-*`): shell perches
  (`perches/<owner>/shells/<adapter>-<n>/`), spawn-mints-instance vs relink/persistent/
  wake online switch, broker-launched binary + `api bind` local-link handshake, the three
  channels (command durable · text+file durable · sensory REST-only never-spooled),
  owner exclusivity, `require_approval` / `max_instances_per_owner` / `over_cap` gates
  (consuming the grant store), broadcast-vs-discovery, aliasing, context injection,
  `shell list/cmd/relink/teardown/rename`.
- **Shell sleep/wake**: offline-on-owner-offline, `persistent` auto-online,
  `wake_command` wake-watcher (exit-opcode supervision, exponential backoff + give-up),
  state-keyed wake resolution, `spt shutdown` owner cascade, `api owner-shutdown`
  (gated by `can_shutdown`).
- **Presence resolution** (new `REQ-PRES-1`): the presence datum gossiped subnet-wide
  (agent-interaction heartbeat; rides registry distribution) + one first-class
  most-recently-active resolution API replacing the per-feature precursors
  (notif first-fire, update consent, escalation, wake resolution).
- **Carried M4 seams**: `REQ-INST-6` deferred-message resting gate (seam documented in
  `spt-daemon::resting`), remote `spt suspend <id@node>` arm, relay rendezvous routing
  (D2f Q1 — consumer now exists), notif-ack → `apply_brain_only` apply orchestration
  (the production caller), cross-runner CI two-host job, KH 7.4
  (`REQ-HAZARD-DAEMON-SCHED-NONBLOCKING`) binding at the per-agent-runtime fan-out.
- **One real shell**: the **OS-notification shell** — minimal `kind="shell"` adapter +
  binary rendering a typed notify command natively; integrates the `notif_command`
  manifest seam (R-NOTIF-2's "render via the attached shell's template" M5 generalization).

### Out

- **PresenceChannel endpoint** (dispatch/bind/thread, durable cross-shell thread) —
  deferred past v1 (decision 1). The presence-resolution API is its future substrate.
- **Remote command execution** (`REQ-REACH-2`) + **instantiate-anywhere** — deferred
  (decision 2); the consent framework ships with their capability ids reserved. The
  **remote fork arm** and **cross-node shell spawn** defer with instantiate-anywhere;
  `shell_wake_spawn_anywhere` ships as grant *shape* only (wake resolution's
  no-reachable-instance branch refuses with a documented deferral).
- **Concrete shells beyond the OS-notification shell** (GameRobot, in-session-inject) —
  deferred (DEFERRED.md row amended, not removed).
- **Cross-user / per-(subnet,user)** generalizations — unchanged forward seams.
- **Shell-binary sandboxing** — unchanged accepted-risk stance (CONTEXT §binary-trust).

## Requirements

Existing inactive reqs that activate per task (rule 5): `REQ-EP-5`, `REQ-INSTALL-4`,
`REQ-INST-6`, `REQ-HAZARD-DAEMON-SCHED-NONBLOCKING` (7.4), `REQ-HAZARD-STALE-INDEX-LOCK`
(1.3 — binds when the daemon owns boot sweeps for the git-touching runtimes it hosts),
`REQ-HAZARD-STDIN-SESSION-ID` (2.2 — binds if/when the M5 adapter api surface grows a
stdin session-id path; re-assess at D3). New reqs to register first (rule 3) at D0:
`REQ-CONSENT-1` (grant store), `REQ-CONSENT-2` (interactive escalation),
`REQ-PRES-1` (presence gossip + MRA resolution), `REQ-SHELL-1` (shell hosting machinery:
channels/lifecycle/exclusivity), `REQ-SHELL-2` (sleep/wake watcher + cascade).
`REQ-INST-1`'s activation rationale (instantiate-anywhere) no longer fires at M5 —
update its rule-5 note at D0 to defer with the capability. `REQ-REACH-2` stays inactive.

## Clean-room posture

Nothing here has a sister-project analog to copy (the sister had no shells, no presence,
no consent store) — clean-room throughout. The OS-notification shell binary is original;
its OS integration uses stock platform surfaces (Windows toast API / `notify-send`).

## Tasks (expanded 2026-06-04 at M5 start)

> Sub-tasks are the executable unit; each lands with its tag in the same commit
> (TRACEABILITY rule 1). Activate listed reqs **at the sub-task that delivers them**
> (rule 5). Conventions carried: no `cargo fmt`; `traceable-reqs check` from repo root
> before declaring done; clippy `-D warnings` on Linux CI is a real gate; push each
> slice → watch the sha-pinned FINAL conclusion.

**Sequencing.** D0 is the doc/registry alignment. D1 (consent) and D2 (adapter
registration) are the floor — D3's gates consume D1's grant store and D3's shell
adapters must be registerable via D2. D4 (sleep/wake) sits on D3. D5 (resting arms) is
independent after M4 and can interleave. D6 (presence) upgrades routing consumers built
through D1/D4 — build the precursor-consuming API late enough to swap call sites once.
D7 (carried infra) is parallelizable; D7c (CI job) early pays off for everything after.
D8 (real shell) needs D2+D3+D4+D6. D9 is the closeout. **Each D-task is itself a
milestone-sized body** — expect a `/self-clear` + commune between most; commit atomically
per sub-task.

### D0 — Scope alignment + requirement registry prep
- **D0a** Amend `ROADMAP.md` §M5 to the decided scope (presence resolution only; consent
  framework seam only; remote-fork-arm + cross-node-spawn defer with instantiate-anywhere;
  mock + OS-notification shell). Amend `docs/DEFERRED.md`: presence-gossip row leaves the
  table (ships M5); concrete-shells row notes the OS-notification carve-out;
  instantiate-anywhere / remote-exec rows note the framework landed M5, capabilities still
  parked. CONTEXT touch-ups only where wording drifts from the above.
- **D0b** Register the new reqs (rule 3, all `required_stages = []` until their sub-task):
  `REQ-CONSENT-1`, `REQ-CONSENT-2`, `REQ-PRES-1`, `REQ-SHELL-1`, `REQ-SHELL-2`. Update
  `REQ-INST-1`'s rule-5 rationale (defers with instantiate-anywhere, not M5).
- **Acceptance:** `traceable-reqs check` green; docs consistent with the scope decisions.

### D1 — Consent framework seam (grant store + escalation)
- **D1a** Grant store: `capability × subject-agent × target-node` rows; **enforced at the
  target node**; settable subnet-wide (replicates as security material near the trust
  store — the REQ-SEC-1 plumbing); revocable. Capability ids `remote-exec` /
  `instantiate-anywhere` reserved-but-refusing; real v1 consumers are the D3 shell gates.
  · **REQ-CONSENT-1**
- **D1b** Interactive escalation: ungated action → consent prompt routed to the user's
  most-recently-active session (the M4 notif-producer path; upgraded to REQ-PRES-1 at
  D6b); **allow-once / allow-always (writes a grant) / deny**. · **REQ-CONSENT-2**
- **D1c** Pre-consent flag authoring: manifest `can_shutdown` + endpoint settings
  `shell_wake_spawn_anywhere` author grants through the same store (different authoring
  path, same model). Flag *shape* only for spawn-anywhere (behavior deferred).
- **Acceptance:** grant present → allowed; absent → escalation; deny blocks; allow-always
  persists + replicates; revocation propagates; gate nesting order (visibility →
  whitelist → grants, ADR-0009) holds in tests.

### D2 — Adapter registration lifecycle
- **D2a** `spt adapter add <path>` / `--github <user/repo>`: manifest-first validation,
  record under `{SPT_HOME}/…/adapters/` (copy for `file_pull`, pointer for `delegated`);
  **install is the first update** through the declared `[update]` avenue; one command for
  both adapter kinds. · **REQ-INSTALL-4 (impl/unit)**
- **D2b** `spt adapter remove`: soft-deregister (hidden from new-creation; live instances
  keep running) + optional manifest `uninstall` template once quiesced (or `--force`).
- **D2c** Wire the registered set as the source for: creation-time adapter selection
  (auto-if-one/ask-if-many), shell discovery (D3d), and the R-UPD-5 ripple set.
- **Acceptance:** add → validate → use → remove lifecycle E2E with mock harness + mock
  shell manifests; a removed adapter's live instance survives.

### D3 — Shell hosting machinery
- **D3a** Shell perch layout (`perches/<owner>/shells/<adapter>-<n>/`; `info.json` carries
  `type=Shell, owner, adapter_name, status, alias?` only — capabilities resolve from the
  manifest). `spt shell spawn <adapter> [--alias]` **mints** a new instance (canonical id
  `<adapter>-<n>`); `relink` / `teardown` / `list` / `cmd` / `rename`. Spawn:create ::
  relink/persistent/wake:online. · **REQ-EP-5 (impl/unit begins)**
- **D3b** Broker-launched binary (never user-launched) + `api bind` (type=Shell) with the
  **local-link handshake** (link token + channel encryption on every message); the three
  command-delivery modes (HTTP / stdin-via-broker / child-relay `api poll`).
- **D3c** The three content channels over the typed/binary substrate: **command**
  (agent→shell, durable/spooled), **text+file** (2-way durable; transfer
  progress-queryable), **sensory** (`api emit --type <t>`, shell→agent, **REST-only,
  never spooled**, dropped unless the owner is live). · **REQ-SHELL-1**
- **D3d** Gates + discovery: `require_approval` (`none|remembered|always`; manifest floor,
  node/endpoint may tighten) and `max_instances_per_owner` + `over_cap` consume the D1
  grant store (`spawn-shell × agent × (node, shell-adapter)` grants); broadcast policy
  (`subnet|same-node|none`) governs **discovery only**; discovery scope = own instances +
  instantiable adapters; owner exclusivity enforced; shell awareness injected into the
  agent's context. · **REQ-EP-5 (gates + discovery)**
- **D3e** Per-agent-runtime fan-out scheduling: the daemon now hosts many per-agent
  loops/processes (shells, watchers, pulse/Psyche) — each agent's bounded blocking work
  off the shared scheduler; one slow agent must not stall another's tick.
  · **REQ-HAZARD-DAEMON-SCHED-NONBLOCKING (7.4)**; boot-time stale-lock sweep for the
  git-touching runtimes the daemon hosts · **REQ-HAZARD-STALE-INDEX-LOCK (1.3)**;
  re-assess **REQ-HAZARD-STDIN-SESSION-ID (2.2)** — activate iff a stdin session-id api
  path now exists, else leave with an updated rationale.
- **Acceptance:** mock-shell E2E spawn → bind+handshake → command → sensory → teardown;
  non-owner refused; `remembered`/`always` approval + `over_cap` enforced through real
  grants; alias addressing works; `--no-default-features` lib build stays clean.

### D4 — Shell sleep/wake + owner cascade
- **D4a** Lifecycle: link-break **always** closes broker link + binary (manifest pre-close
  instruction + termination timeout); **ephemeral** ⇒ full teardown + history erase;
  **persistent** ⇒ perch offline, re-linkable (binary re-spawned on relink);
  offline-on-owner-offline; `persistent` auto-onlines with the owner. · **REQ-SHELL-2 begins**
- **D4b** `wake_command` wake-watcher: runs **while offline** (mutually exclusive with the
  binary), on the shell's node; **exit(wake-opcode)** → wake resolution; any other exit →
  respawn with exponential backoff + eventual give-up; one watcher per offline instance.
- **D4c** State-keyed wake resolution: owner dormant → online the shell; suspended →
  revive owner, then online; active elsewhere → attach/online; **no reachable instance →
  no-op with a documented refusal** (the `shell_wake_spawn_anywhere` fresh-spawn branch
  stays deferred with instantiate-anywhere; the D1c grant shape is its seam).
  · **REQ-SHELL-2**
- **D4d** `spt shutdown` (agent gracefully suspends its own endpoint: suspend boundary
  signoff fires, persistent shells cascade offline) + `api owner-shutdown` (shell
  suspends its owner directly, gated by the `can_shutdown` pre-consent grant).
- **Acceptance:** full sleep/wake cycle driven from both ends (owner shutdown → shell
  offline → watcher up → wake opcode → owner revived → shell online); crash-exit
  backoff observed; ephemeral-vs-persistent divergence proven.

### D5 — Resting-state arms (carried M4 seams)
- **D5a** Deferred-message resting gate: deferred (spool-only) rows **hold** while the
  instance is dormant/suspended, **release on the wake edge** (the documented
  `spt-daemon::resting` seam; extends KH 1.4/4.4 with the instance-state gate).
  · **REQ-INST-6**
- **D5b** Remote `spt suspend <id[@node]>` + remote `spt wake` completeness (suspend/wake
  any of your instances from any node — ungated, same trust class as remote-drive).
- **D5c** Remote fork arm: **document the deferral** (rides instantiate-anywhere,
  decision 2) — no build.
- **Acceptance:** deferred message held while resting, delivered exactly once after wake;
  remote suspend/wake proven cross-node (loopback int + rig leg at D9).

### D6 — Presence resolution
- **D6a** Presence datum `(last_active_node, last_active_endpoint, ts)` gossiped
  subnet-wide — agent-interaction heartbeat as the signal (the `last_active_ms` recency
  stamp generalized), riding the registry/notif distribution; visibility-gated like
  registry data. · **REQ-PRES-1 begins**
- **D6b** One first-class **most-recently-active resolution API**; swap the per-feature
  precursors to it: notif first-fire, update-consent delivery, consent escalation (D1b),
  wake resolution (D4c). · **REQ-PRES-1**
- **D6c** Document the PresenceChannel endpoint as the deferred consumer (dispatch/bind/
  thread styles build on this datum + the broker PresenceLog seam, REQ-EP-4) — seam note,
  no build.
- **Acceptance:** activity shifting nodes redirects notif/consent delivery cross-node;
  loopback int + rig leg at D9.

### D7 — Carried infra seams
- **D7a** Relay rendezvous routing (M4-D2f Q1): route the pre-trust pairing rendezvous
  via the relay using the existing `H(subnet-name ‖ TOTP-epoch)` token derivation
  (`spt-net::pairing::rendezvous`) — pairing beyond LAN/known-addr; the deferral's
  "no consumer until shells" trigger has fired.
- **D7b** Notif-ack → `apply_brain_only` orchestration: consent-notif acknowledgment
  drives the staged update apply — the **production caller** for `apply_brain_only`
  (closes the M4-D9 "no-production-caller" annotation).
- **D7c** Cross-runner CI two-host job: automate the `[twohost]` ladder as a cross-runner
  workflow (hfenduleam + gravity runners coordinated in one run; runbook stays the manual
  fallback). Gotchas #7/#13/#17 discipline baked in.
- **Acceptance:** relay-routed pairing E2E; a real update applies off a notif ack;
  the cross-runner job goes green on the rig unattended.

### D8 — The real shell: OS-notification shell (dogfood)
- **D8a** Minimal `kind="shell"` adapter + original binary: renders a typed notify
  command as a native OS notification (Windows toast / Linux `notify-send`);
  `broadcast = "same-node"`, `persistent = true`, sane `require_approval` default;
  registered via D2, hosted via D3/D4.
- **D8b** `notif_command` integration (R-NOTIF-2's M5 generalization): when presence
  resolves the user to an endpoint with this shell attached, a subnet notif renders via
  the shell's template — `spt notify` → native toast on the user's active node.
- **D8c** Cross-node owner↔shell **link**: owner active on node B relinks its persistent
  shell living on node A (commands ride Iroh; spawn stays same-node per decision 2).
- **Acceptance:** the dogfood demo — an agent's `spt notify` surfaces as a real OS toast
  on whichever rig host the user last touched; cross-node relink proven.

### D9 — Activation sweep + two-host E2E + closeout
- **D9a** Rig int legs (env-gated `[twohost]`, runbook-extended; or via the D7c job):
  remote suspend/wake · presence-shift redirect · cross-node shell relink + drive ·
  notif→toast. Tag `[int->REQ-*]` on the gated tests.
- **D9b** Sweep: every M5 req's `required_stages` satisfied or rule-5-deferred with
  rationale; `traceable-reqs check` green; amend ROADMAP (M5 delivered) + CONTEXT +
  DEFERRED; CI matrix (ubuntu + windows + traceability + cross-runner) green FINAL.
- **Acceptance:** scope decisions 1–3 demonstrably met; closeout recorded.

## Requirement → task coverage map

| Task | Activates |
|------|-----------|
| D0 | registers REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 (inactive); REQ-INST-1 rationale update |
| D1 | REQ-CONSENT-1, REQ-CONSENT-2 |
| D2 | REQ-INSTALL-4 |
| D3 | REQ-EP-5, REQ-SHELL-1, REQ-HAZARD-DAEMON-SCHED-NONBLOCKING, REQ-HAZARD-STALE-INDEX-LOCK (+ REQ-HAZARD-STDIN-SESSION-ID iff path exists) |
| D4 | REQ-SHELL-2 |
| D5 | REQ-INST-6 |
| D6 | REQ-PRES-1 |
| D7 | (no new activations — closes M4 deferral annotations) |
| D8 | REQ-EP-5/SHELL-1/2 + REQ-NOTIF-2 `int` evidence via the real shell |
| D9 | int sweep + closeout |

**Stays deferred (out of M5):** REQ-REACH-2, instantiate-anywhere (+ remote fork arm,
cross-node shell spawn, `shell_wake_spawn_anywhere` behavior), the PresenceChannel
endpoint (REQ-EP-4 stays at its M4 broker-seam stage), GameRobot-class shells,
cross-user generalizations, REQ-MIGRATE-1.
