# HOST-PLAN — spt-mobile host binary, first milestone (2026-07-06)

JIT plan per AGENTS.md. Scope: the host-side adapter — manifest, gateway bind, spt CLI
surface wrapper, device-link listener skeleton, history log + host spool stores. Everything
grounded in spt-core recon @v0.26.0 (`release/v0.26.0`, REMOTE-TRUTH merged).

## Scope (this milestone)

Deliver `host/` — a standalone Rust binary (`spt-mobile-host`) that:

1. Ships as a normal spt adapter: `manifest.toml` with `kind = "harness"`,
   `hostable_types = ["Gateway"]`, `[session.self]` launching the binary; registered via
   `spt adapter add`. Zero spt-core changes. [REQ-HOST-ADAPTER]
2. Binds its perch: `spt api bind <id> --type gateway --set-session-id <sid>` (spt-hosted
   flow; `{id}` via env inject). Gateway type earns `user-msg` authority at the identity
   gate (`gate_user_msg_type`, spt-core `crates/spt-proto/src/event.rs:150`).
   [REQ-GATEWAY-PERIPHERAL — host half]
3. Wraps the spt CLI as its local api surface (subprocess, never the daemon socket —
   spt-net/spt-daemon are internal, R-ARCH-2):
   - `spt send <target> --user-msg --json-payload '{"origin":…,"captured":…,"msg-id":…}'`
     — treat `SENT`/`QUEUED` as success, `NO_PERCH`/other as failure. [REQ-VOICE-JSON-PAYLOAD
     host half; REQ-HAZARD-QUEUED-RETRY]
   - `spt endpoint list` / registry snapshots — browse data. [REQ-BROWSER-LIVE host half]
   - `spt endpoint digest <id> [--follow] [--json] [--after <seq>]` — snapshot + follow,
     local endpoints only (most-direct-route, ruling 4).
4. Opens the device-link listener: own iroh endpoint (iroh `=0.98.2` pinned, matching
   spt-core), own ALPN `spt-mobile/link/0`, pairing-token bearer auth. v1 RPC set (JSON
   frames, length-delimited): `pair`, `ping`, `list-endpoints`, `send`, `digest-snapshot`,
   `digest-follow` (stream), `history-fetch`, `spool-drain`. [REQ-DEVICE-LINK-IROH host half]
5. Persists two stores under the adapter state dir:
   - **history log** — append-only per endpoint, msg-id keyed (JSONL,
     `state/history/<endpoint>.jsonl`). [REQ-HISTORY-HOST-LOG]
   - **host spool** — per-phone inbound queue for notification delivery while the phone is
     unreachable (JSONL, `state/spool/<phone>.jsonl`, drained FIFO on phone reconnect).

## Non-goals (later milestones)

- Phone side entirely (Rust JNI slice, Kotlin app, voice pipe).
- Multi-host failover logic (phone-side concern; host binary is single-host by nature).
- Cross-node digest proxying (waits on the spt-core seed; host serves local digests only).
- Wake/boot supervision: spt-core's always-on endpoint class (REQ-EP-8/9,
  spt-core `traceable-reqs.toml:205-213`) is **UNBUILT** — v1 the binary is launched via
  `[session.self]` and supervises itself; revisit when the always-on class lands.

## Design decisions (from recon; citations = spt-core @v0.26.0)

| # | Decision | Ground |
|---|----------|--------|
| D1 | Binary template = spt-shell-notify's resident loop (standalone crate, spt CLI subprocess, `<EVENT>` decode with **amp-last** order) | `spt-shell-notify/src/main.rs:72-85,231-247` |
| D2 | Manifest/packaging template = spt-claude-code (fat `adapter.spt`, per-triple subdirs, ADR-0024; `[update] avenue="gh_release"`) | `spt-claude-code/ci/publish/package-adapter.sh`; spt-core ADR-0024 |
| D3 | Bind = spt-hosted flow, `api bind <id> --type gateway` | `docs-site/src/cli/reference.md:1180-1183`; REQ-EP-6; `adapters/mock-gateway` |
| D4 | **No spt-core crate/path deps** (source-independence rule, AGENTS.md — supersedes the initial reuse-spt-proto idea): wire/format parity clean-room from the public docs (<https://sabermage.github.io/spt-releases>), pinned by round-trip tests; spt CLI is the api surface. Device link = own iroh dep + own ALPN; phone pairing = own bearer-token layer | AGENTS.md source-independence rule; ADR-0002 consequences |
| D5 | msg-id dedup against digest echoes is viable: delivered user-msg becomes an `owl_message` Context row whose body is the composed `<EVENT>` verbatim — `json` attr survives, re-parse with `parse_event().attr("json")`. Context rows carry no `seq` (only transcript rows do) — dedup axes stay separate exactly as ruling 8 says | `crates/spt/src/api/startup.rs:450-459`; `spt-msg/src/emit.rs:59,112-114`; `cli.rs:12358` |
| D6 | Send results: `SENT`/`QUEUED`/`SENT(WAN)` = success; `QUEUED(idle-only)`/`DEFERRED` = success-deferred; `NO_PERCH` = failure. Never re-send on QUEUED | `cli.rs:5349`; REQ-HAZARD-QUEUED-RETRY |

Amendment to DESIGN.md build note carried in this milestone: "reusing spt-core
net/proto/pairing crates" → **no spt-core crate deps at all**; wire/format parity is
clean-room from the public docs; device link and phone pairing are spt-mobile-owned
(own iroh, own token layer).

## Open design questions (resolve during build, smallest-commitment default)

- Q1 `state/` location: adapter install dir vs `$SPT_HOME/spt-mobile/`. Default: adapter
  state dir pattern (`{adapter_dir}/state/…`, per spt-claude-code seam).
- Q2 Pairing token format for QR: `{host node key, endpoint id, bearer token}` — exact
  encoding (base32 vs JSON-in-QR). Default: JSON, camelCase, versioned `"v":1`.
- Q3 Digest-follow proxying: subprocess `--follow` per phone subscription vs one follow +
  fan-out. Default: one follow subprocess per followed endpoint, fan-out in-process.

## Waves

- **W1 — scaffold + spt surface** (this commit series):
  1. `host/` crate (own workspace guard like spt-shell-notify), `manifest.toml`, bind flow
     (`api seed`/`bind --type gateway`), `spt` CLI wrapper module (send/list/digest
     snapshot), send-result classifier, `<EVENT>` decoder (amp-last). Unit tests: manifest
     parses (against vendored schema check optional), send-result classification,
     event decode round-trip.
  2. Add `"host"` to `[scan].roots` same commit. Tag evidence. Gate: `cargo build` +
     `cargo test` + `traceable-reqs check` green.
- **W2 — device link**: iroh endpoint, ALPN `spt-mobile/link/0`, pairing-token auth,
  RPC framing + `pair`/`ping`/`list-endpoints`. Unit: frame codec, token verify.
- **W3 — stores + streams**: history log, host spool, digest-follow fan-out,
  `send`/`history-fetch`/`spool-drain` RPCs. Unit: append/dedup by msg-id, FIFO drain
  order. `int` activation at the wave that first proves phone↔host end-to-end (W2 earliest).

## Requirement activation (this milestone)

| REQ | Stages activated |
|-----|------------------|
| REQ-HOST-ADAPTER | doc, impl, unit |
| REQ-GATEWAY-PERIPHERAL | doc, impl (host half; unit at W2 token auth) |
| REQ-DEVICE-LINK-IROH | doc (impl/unit at W2) |
| REQ-HISTORY-HOST-LOG | doc (impl/unit at W3) |
| REQ-VOICE-JSON-PAYLOAD | doc, impl, unit (host send path) |
| REQ-HAZARD-QUEUED-RETRY | doc, unit (send-result classifier; int when phone loop exists) |

Others stay inactive until their milestone.

## Gate

`cargo build` + `cargo test` green in `host/`; `traceable-reqs check` exit 0; DESIGN.md
build-note amendment landed; docs tagged.

**Gate status (2026-07-06): COMPLETE.** W1–W3 shipped; digest --json handling validated
against the ground-truthed public docs schema + live v0.26.0 daemon (link frame renamed
DigestRow→DigestDelta to match the `{version,from,turns}` delta contract); packaging
landed (`ci/package-adapter.sh` → fat `adapter.spt` tar, manifest `[update]
avenue="gh_release"`, manifest schema-validated against the public
`manifest.schema.json`); DESIGN.md build-note amendment landed. `int` stages defer to
the first cross-process phone↔host proof (JNI-slice milestone, JNI-PLAN.md).
