# Install-on-demand bootstrap

How an adapter ships spt-core *with itself*. The contract: **the canonical
install one-liner is also every adapter's pack-in installer** — there is no
second mechanism, no vendored binary, no bespoke fetch logic to maintain.
Your adapter checks for `spt`, and runs the official script when it's
missing.

The scripts are non-interactive **by construction** (they will never prompt),
idempotent (safe to re-run), sha256-verify what they download, and register
the user PATH. Served from the permanent canonical URL:

- `https://sabermage.github.io/spt-releases/install.sh`
- `https://sabermage.github.io/spt-releases/install.ps1`

## The generic contract

```text
if `spt` is on PATH        -> done (optionally check `spt --version` ≥ your floor)
else                       -> run the official one-liner for the OS
then                       -> first invocation may need the absolute path (Windows)
```

After first install, spt-core keeps itself current (signed self-update) — the
bootstrap never needs to handle upgrades.

## Check-and-install: POSIX sh

Drop this into your adapter's bootstrap (plugin install step, postinstall
script, first-run guard):

```sh
if ! command -v spt >/dev/null 2>&1; then
  echo "spt-core not found - installing..."
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
  # current shell may not see the PATH update yet:
  SPT="$HOME/.local/bin/spt"
else
  SPT="spt"
fi
"$SPT" --version
```

## Check-and-install: PowerShell

```powershell
if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {
    Write-Output "spt-core not found - installing..."
    irm https://sabermage.github.io/spt-releases/install.ps1 | iex
    # The user-PATH registration only reaches NEW terminals -- use the
    # absolute install path for everything in THIS process:
    $spt = Join-Path $env:LOCALAPPDATA 'spt\bin\spt.exe'
} else {
    $spt = 'spt'
}
& $spt --version
```

## The Windows PATH-refresh gotcha

The installer registers the binary directory on the **user** PATH via the
registry. Already-running processes — including the terminal (and *your
bootstrap*) that just ran the installer — do not see registry PATH changes.

So: **the first invocation after an install must use the absolute path**
(`%LOCALAPPDATA%\spt\bin\spt.exe`; the installer prints it). Every new
terminal after that finds `spt` normally. The snippets above bake this in.
On Linux the equivalent (a `~/.profile` entry the current shell hasn't
sourced) is handled the same way: `$HOME/.local/bin/spt` absolutely, once.

## Pinning and air-gapped installs

The scripts take environment knobs — never flags, so the pipe-to-shell form
stays canonical:

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Install a specific release tag instead of latest |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_ASSET_BASE` | A URL or local directory holding the release assets + `SHA256SUMS` directly (CI, air-gap, mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

Example — pin a version inside a CI job:

```sh
SPT_INSTALL_VERSION=v0.1.0 \
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

## Trust model

First fetch trusts HTTPS + GitHub and verifies the binary's sha256 against
the release's `SHA256SUMS`. From then on, `spt update` performs full Ed25519
signature verification against the two-key trust anchor embedded in every
binary — the installer never needs to be the strong link twice.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\harness-contract\install-on-demand.md (file state is current in your context — no need to Read it back)

# Install-on-demand bootstrap

How an adapter ships spt-core *with itself*. The contract: **the canonical
install one-liner is also every adapter's pack-in installer** — there is no
second mechanism, no vendored binary, no bespoke fetch logic to maintain.
Your adapter checks for `spt`, and runs the official script when it's
missing.

The scripts are non-interactive **by construction** (they will never prompt),
idempotent (safe to re-run), sha256-verify what they download, and register
the user PATH. Served from the permanent canonical URL:

- `https://sabermage.github.io/spt-releases/install.sh`
- `https://sabermage.github.io/spt-releases/install.ps1`

## The generic contract

```text
if `spt` is on PATH        -> done (optionally check `spt --version` ≥ your floor)
else                       -> run the official one-liner for the OS
then                       -> first invocation may need the absolute path (Windows)
```

After first install, spt-core keeps itself current (signed self-update) — the
bootstrap never needs to handle upgrades.

## Check-and-install: POSIX sh

Drop this into your adapter's bootstrap (plugin install step, postinstall
script, first-run guard):

```sh
if ! command -v spt >/dev/null 2>&1; then
  echo "spt-core not found - installing..."
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
  # current shell may not see the PATH update yet:
  SPT="$HOME/.local/bin/spt"
else
  SPT="spt"
fi
"$SPT" --version
```

## Check-and-install: PowerShell

```powershell
if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {
    Write-Output "spt-core not found - installing..."
    irm https://sabermage.github.io/spt-releases/install.ps1 | iex
    # The user-PATH registration only reaches NEW terminals -- use the
    # absolute install path for everything in THIS process:
    $spt = Join-Path $env:LOCALAPPDATA 'spt\bin\spt.exe'
} else {
    $spt = 'spt'
}
& $spt --version
```

## The Windows PATH-refresh gotcha

The installer registers the binary directory on the **user** PATH via the
registry. Already-running processes — including the terminal (and *your
bootstrap*) that just ran the installer — do not see registry PATH changes.

So: **the first invocation after an install must use the absolute path**
(`%LOCALAPPDATA%\spt\bin\spt.exe`; the installer prints it). Every new
terminal after that finds `spt` normally. The snippets above bake this in.
On Linux the equivalent (a `~/.profile` entry the current shell hasn't
sourced) is handled the same way: `$HOME/.local/bin/spt` absolutely, once.

## Pinning and air-gapped installs

The scripts take environment knobs — never flags, so the pipe-to-shell form
stays canonical:

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Install a specific release tag instead of latest |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_ASSET_BASE` | A URL or local directory holding the release assets + `SHA256SUMS` directly (CI, air-gap, mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

Example — pin a version inside a CI job:

```sh
SPT_INSTALL_VERSION=v0.1.0 \
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

## Trust model

First fetch trusts HTTPS + GitHub and verifies the binary's sha256 against
the release's `SHA256SUMS`. From then on, `spt update` performs full Ed25519
signature verification against the two-key trust anchor embedded in every
binary — the installer never needs to be the strong link twice.


M6-D1: spt-releases repo + public face (README, licenses)

Create SaberMage/spt-releases private via gh; author README.md per grill decision 7; LICENSE-MIT + LICENSE-BINARY; Pages config deferred to D7 flip. Activate REQ-REL-1 doc stage.

Creating spt-releases repo + public face

M6-D2: installer scripts (install.sh / install.ps1)

Non-interactive: latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → print absolute path. Idempotent. CI rung against staged artifact + spt --version. Activate REQ-INSTALL-1/2/3/5.

M6-D3: manifest JSON Schema (schemars) + drift gate

schemars derives on spt-runtime manifest types; emit manifest.schema.json at stable site-root path + release asset; CI regenerate+diff gate. Activate REQ-DOCS-4 (partial), REQ-DOCS-5.

M6-D4: docs-site Tier 1 content (mdBook)

mdBook skeleton + Starlight-inspired theme; mental model; human quickstart; adapter quickstart (mock-adapter); harness-contract vertical complete; shell getting-started (spt-shell-notify); stubs for remaining verticals. Activate REQ-DOCS-1/2/3.

M6-D5: agent-export + generation plumbing

llms.txt + llms-full.txt; raw .md alongside HTML; CLI ref from clap; rustdoc → Pages; source-repo CI publish job → spt-releases Pages branch; drift gates. REQ-REL-1 impl, REQ-DOCS-4/5 complete.

M6-D6: release pipeline + signing ceremony

Workspace → 0.1.0; tag-triggered build matrix both runners; cross-repo release via fine-grained token; key ceremony per ADR-0015 (primary + recovery, embed both pubkeys in VerifyPolicy trusted set, seed backups); manual SignedRelease signing. Activate REQ-REL-2/3.

M6-D7: v0.1.0 ship + flip public + acceptance run

Tag v0.1.0; ceremony; publish release; flip spt-releases public; Pages live; acceptance proof per M6-PLAN (clean-machine install, quickstarts, llms.txt + schema fetch, staged v0.1.1 verify, recovery-key verify).


Shell getting-started needs the real spt-shell-notify manifest:


PowerShell

gh api repos/SaberMage/spt-shell-notify/contents/manifest.toml -q '.content' | %{ [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) }; gh api repos/SaberMage/spt-shell-notify/contents/README.md -q '.content' 2>$null | %{ [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) } | Select-Object -First 40

Fetch spt-shell-notify manifest + README


# OS-notification shell adapter — the spt-c
ore real-shell dogfood (M5-D8).
# A `kind = "
shell"` adapter: the manifest + the `notify-s
hell` binary are the
# ONLY glue between this
 project and spt-core. The binary speaks noth
ing but
# the public `spt api` surface and th
e documented EVENT envelope wire format.
#
#
Two render paths, both declared here:
#   - t
he `notify` capability: an owner agent drives
 a toast explicitly
#     (`spt shell cmd <id
> notify <title> <body>`) down the durable co
mmand
#     channel; the resident binary drai
ns and renders.
#   - the `[session.notif]` t
emplate: spt-core spawns it detached whenever
 a
#     subnet notif SURFACES at an endpoint
 this shell is attached to
#     (`spt notify
` → presence resolution → native toast on
 the node the user
#     last touched).
#
# `
spawn` resolves `notify-shell` on PATH; a tes
t/CI registration may rewrite
# the templates
 with absolute paths.

[adapter]
name = "noti
fy"
kind = "shell"
version = "1.0.0"
min_spt_
core_version = "1.0.0"

[shell]
spawn = "noti
fy-shell --link {link_token} --id {id}"
# A d
isplay is node-local; discovery never offers
it off-node.
broadcast = "same-node"
# Auto-o
nline with the owner: a notification surface
should be up whenever
# the user's endpoint i
s.
persistent = true
# First spawn asks once,
 the grant is remembered (the sane floor for
a shell
# that can pop UI on the user's scree
n).
require_approval = "remembered"
# Relay c
ommand receipt (the default): the binary drai
ns `api poll --link`.
pre_close = "closing"
c
lose_timeout_ms = 2000
# Offline wake: the di
splay is assumed always-ready, so the watcher
 reports
# wake (exit 86) after a short settl
e — the shell self-revives whenever its
# o
wner state admits it.
wake_command = "notify-
shell --wake"

# The whole command vocabulary
: one verb. `title`/`body` map positionally
#
 from `spt shell cmd <id> notify <title> <bod
y>`.
[shell.capabilities.notify]
args = ["tit
le", "body"]

# The notif render seam: spt-co
re fills {notif_*} keys and spawns this
# det
ached when a notif surfaces at an endpoint wi
th this shell attached.
# One-shot: render an
d exit. The keys are quoted so spaces survive
 spt-core's
# quote-honoring tokenizer (an em
bedded double-quote in a body renders
# mangl
ed, never breaks the spawn).
[session.notif]

command = 'notify-shell --render-title "{noti
f_from}" --render-body "{notif_body}"'
detach
 = true
keys = ["notif_id", "notif_from", "no
tif_subnet", "notif_body"]

# spt-shell-notify

OS-notification shell ada
pter for [spt-core] — renders agent command
s and
surfaced subnet notifs as **native OS n
otifications** (Windows toast / Linux
`notify
-send`).

This is the reference *real* shell
adapter and the proof of spt-core's
adapter m
odel: **the manifest + one binary are the onl
y glue.** No spt-core
crates, no source integ
ration — the binary speaks the public `spt
api`
command surface and the documented EVENT
 envelope wire format, and the
manifest decla
res everything spt-core needs to host it.

##
 What it does

- `spt shell cmd <id> notify <
title> <body>` → a native toast, driven by
the
  owner agent down the durable command ch
annel (the resident binary drains
  `api poll
 --link` and renders).
- `spt notify <body>`
anywhere in the subnet → presence resolutio
n picks the
  node the user last touched →
spt-core spawns this adapter's
  `[session.no
tif]` template there → native toast. No age
nt in the loop.

## Install

```
cargo instal
l --path .          # puts notify-shell on PA
TH
spt adapter add <this dir>      # register
s the manifest
spt shell spawn notify
  # mints + launches an instance
```

## Layo
ut

- `manifest.toml` — the whole spt-core
contract: spawn/wake/close templates,
  the o
ne-verb `notify` capability, the `[session.no
tif]` render template.
- `src/main.rs` — th
e binary: resident shell mode, one-shot `--re
nder-*`
  mode (the notif template), `--wake`
 watcher mode. Self-contained envelope
  deco
de (amp-last, `<br>`-first — the documented
 order).

## Testing

`cargo test` covers fra
me parsing and the PowerShell embedding. The

end-to-end leg (spawn → bind → command �
� render) lives in spt-core's CI as an
env-ga
ted test: it clones this repo, builds the bin
ary, and drives it through
the real `spt` sur


Write

# Getting started: a notification shell

The fastest way to understand shells is the shipping one:
[`spt-shell-notify`](https://github.com/SaberMage/spt-shell-notify) renders
agent commands and surfaced notifications as **native OS notifications**
(Windows toast / Linux `notify-send`). Its manifest plus one small binary are
the **only** glue to spt-core — no spt-core source, no SDK; the binary speaks
the public `spt api` surface and nothing else. This page installs it, drives
it, and reads its manifest as the template for your own shell.

## 1. Install and spawn it

```console
$ git clone https://github.com/SaberMage/spt-shell-notify
$ cd spt-shell-notify
$ cargo install --path .        # puts `notify-shell` on PATH
$ spt adapter add .             # validates + registers the manifest
ADAPTER_ADD:notify:Shell:Copy (registered)
$ spt shell spawn notify        # mints an instance (notify-1) and launches it
```

`spawn` **mints a new instance identity** — `notify-1` — and launches the
binary; it's the creation act, not an on/off switch. The first spawn asks for
approval once (the manifest sets `require_approval = "remembered"`), and the
grant persists.

## 2. Drive it from an agent

Two render paths, by design:

**Explicit command** — an owner agent drives a toast down the durable command
channel:

```console
$ spt shell cmd notify-1 notify "build finished" "all 139 tests green"
```

The resident binary drains its command frames (`spt api … poll --link`) and
renders. Commands are validated against the manifest's declared vocabulary —
a verb or arity outside `[shell.capabilities]` is refused before it ever
reaches the binary.

**Surfaced notification** — no agent in the loop:

```console
$ spt notify "deploy window opens in 10 minutes"
```

A subnet-wide notification resolves to the node the user most recently
touched, and spt-core spawns the shell's `[session.notif]` template there —
a native toast on the machine you're actually at.

## 3. Read the manifest

The complete contract for this shell, annotated:

```toml
[adapter]
name = "notify"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
# Broker-launched; the {link_token} is the binary's only credential.
spawn = "notify-shell --link {link_token} --id {id}"
# A display is node-local; discovery never offers it off-node.
broadcast = "same-node"
# Auto-online with the owner: the notification surface should be up
# whenever the user's endpoint is.
persistent = true
# First spawn asks once; the grant is remembered.
require_approval = "remembered"
pre_close = "closing"
close_timeout_ms = 2000
# Offline wake-watcher: reports wake (exit code 86) after a short settle.
wake_command = "notify-shell --wake"

# The whole command vocabulary: one verb, two positional args.
[shell.capabilities.notify]
args = ["title", "body"]

# The notif render seam: spt-core fills the {notif_*} keys and spawns this
# detached when a notification surfaces at an endpoint this shell serves.
[session.notif]
command = 'notify-shell --render-title "{notif_from}" --render-body "{notif_body}"'
detach = true
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

What the binary itself does (three modes, ~one file):

- **resident** (`--link …`): calls `api bind-shell --link <token>` to come
  online, then loops `api poll --link <token>` draining command frames and
  rendering them.
- **one-shot render** (`--render-title/--render-body`): the `[session.notif]`
  template — render and exit.
- **wake watcher** (`--wake`): run while the instance is offline; exiting
  with code 86 signals "wake me".

## 4. Make your own

A shell is worth building whenever agents should *drive* something —
a desktop widget, a robot, a lamp, a game character, a sensor feed:

1. Start from this manifest; change `name`, `spawn`, and the
   `[shell.capabilities]` vocabulary to your verbs.
2. Your binary needs exactly three behaviors: **bind** with the link token,
   **drain** commands (`api poll --link`, or declare `command_receipt =
   "http"`/`"stdin"` if those fit better), and optionally **emit** sensory
   payloads back (`api emit … --type <t> --link <token>` — declared in
   `[shell.sensory]`, delivered only to a *live* owner session: sensors
   report the present, never the past).
3. Pick lifecycle behavior: `persistent` for always-up surfaces,
   `ephemeral = true` for fire-and-forget ones, `wake_command` if the
   surface can wake its owner.
4. `spt adapter add .` and `spt shell spawn <name>`.

Field-by-field details: the [manifest reference](../harness-contract/manifest.md#shell-adapters-kind--shell);
the shell-side api calls: the [`spt api` reference](../harness-contract/api.md#shells).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\shells\getting-started.md (file state is current in your context — no need to Read it back)

# Getting started: a notification shell

The fastest way to understand shells is the shipping one:
[`spt-shell-notify`](https://github.com/SaberMage/spt-shell-notify) renders
agent commands and surfaced notifications as **native OS notifications**
(Windows toast / Linux `notify-send`). Its manifest plus one small binary are
the **only** glue to spt-core — no spt-core source, no SDK; the binary speaks
the public `spt api` surface and nothing else. This page installs it, drives
it, and reads its manifest as the template for your own shell.

## 1. Install and spawn it

```console
$ git clone https://github.com/SaberMage/spt-shell-notify
$ cd spt-shell-notify
$ cargo install --path .        # puts `notify-shell` on PATH
$ spt adapter add .             # validates + registers the manifest
ADAPTER_ADD:notify:Shell:Copy (registered)
$ spt shell spawn notify        # mints an instance (notify-1) and launches it
```

`spawn` **mints a new instance identity** — `notify-1` — and launches the
binary; it's the creation act, not an on/off switch. The first spawn asks for
approval once (the manifest sets `require_approval = "remembered"`), and the
grant persists.

## 2. Drive it from an agent

Two render paths, by design:

**Explicit command** — an owner agent drives a toast down the durable command
channel:

```console
$ spt shell cmd notify-1 notify "build finished" "all 139 tests green"
```

The resident binary drains its command frames (`spt api … poll --link`) and
renders. Commands are validated against the manifest's declared vocabulary —
a verb or arity outside `[shell.capabilities]` is refused before it ever
reaches the binary.

**Surfaced notification** — no agent in the loop:

```console
$ spt notify "deploy window opens in 10 minutes"
```

A subnet-wide notification resolves to the node the user most recently
touched, and spt-core spawns the shell's `[session.notif]` template there —
a native toast on the machine you're actually at.

## 3. Read the manifest

The complete contract for this shell, annotated:

```toml
[adapter]
name = "notify"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
# Broker-launched; the {link_token} is the binary's only credential.
spawn = "notify-shell --link {link_token} --id {id}"
# A display is node-local; discovery never offers it off-node.
broadcast = "same-node"
# Auto-online with the owner: the notification surface should be up
# whenever the user's endpoint is.
persistent = true
# First spawn asks once; the grant is remembered.
require_approval = "remembered"
pre_close = "closing"
close_timeout_ms = 2000
# Offline wake-watcher: reports wake (exit code 86) after a short settle.
wake_command = "notify-shell --wake"

# The whole command vocabulary: one verb, two positional args.
[shell.capabilities.notify]
args = ["title", "body"]

# The notif render seam: spt-core fills the {notif_*} keys and spawns this
# detached when a notification surfaces at an endpoint this shell serves.
[session.notif]
command = 'notify-shell --render-title "{notif_from}" --render-body "{notif_body}"'
detach = true
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

What the binary itself does (three modes, ~one file):

- **resident** (`--link …`): calls `api bind-shell --link <token>` to come
  online, then loops `api poll --link <token>` draining command frames and
  rendering them.
- **one-shot render** (`--render-title/--render-body`): the `[session.notif]`
  template — render and exit.
- **wake watcher** (`--wake`): run while the instance is offline; exiting
  with code 86 signals "wake me".

## 4. Make your own

A shell is worth building whenever agents should *drive* something —
a desktop widget, a robot, a lamp, a game character, a sensor feed:

1. Start from this manifest; change `name`, `spawn`, and the
   `[shell.capabilities]` vocabulary to your verbs.
2. Your binary needs exactly three behaviors: **bind** with the link token,
   **drain** commands (`api poll --link`, or declare `command_receipt =
   "http"`/`"stdin"` if those fit better), and optionally **emit** sensory
   payloads back (`api emit … --type <t> --link <token>` — declared in
   `[shell.sensory]`, delivered only to a *live* owner session: sensors
   report the present, never the past).
3. Pick lifecycle behavior: `persistent` for always-up surfaces,
   `ephemeral = true` for fire-and-forget ones, `wake_command` if the
   surface can wake its owner.
4. `spt adapter add .` and `spt shell spawn <name>`.

Field-by-field details: the [manifest reference](../harness-contract/manifest.md#shell-adapters-kind--shell);
the shell-side api calls: the [`spt api` reference](../harness-contract/api.md#shells).


Vertical overviews + stubs (concise per plan):


# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\messaging\overview.md (file state is current in your context — no need to Read it back)

# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).


# Live-agent lifecycle

What makes an agent endpoint a *persistent being* rather than a disposable
session: identity that survives resets, a working memory that follows it
across machines, and graceful endings that never lose context.

## The pieces

- **Perch** — the durable seat (identity, spool, state). Sessions attach to
  it (`api bind`/`listen`), reset across it (`api boundary`), and end
  without destroying it (`api session-end`).
- **The mind, in two tiers** — a *live* tier (who the agent is, what it's
  doing) that follows the endpoint everywhere, and a *project* tier scoped
  to one project. Both are versioned, tracked storage, synced to paired
  machines with the same scoping.
- **Commune** — the agent drops `<id>-commune.md` into the adapter's watched
  directory; spt-core ingests the delta into the right tier. A file-drop,
  not a command — any harness that can write a file can commune.
- **Signoff** — the graceful ending: final commune, then teardown
  (`spt shutdown` / `api shutdown`). The echo-commune fires **before**
  teardown, always.
- **Echo-commune** — sessions that end *without* a signoff don't lose their
  delta: spt-core runs the adapter's bounded summarizer template over the
  session history and ingests the result. The **echo gate** sentinel
  (armed on idle, cleared by graceful signoff) is what marks the need.
- **Psyche** — the endpoint's persistent-context companion process,
  spawned/resumed from the adapter's `psyche_init`/`psyche_resume`
  templates.

## Rest and wake

Endpoints rest instead of dying: **dormant** (warm — zero idle compute,
instantly wakeable) or **suspended** (cold), explicitly via `spt suspend` or
on attention-shift. Resting instances stay addressable; deferred messages
are held and released exactly once on wake (`spt wake`). Every
active→resting edge fires a **transition echo** so the final context delta
lands before the lights go out.

## Commands

`spt shutdown` · `suspend` · `wake` · the `api` lifecycle calls
([reference](../harness-contract/api.md#session-lifecycle)).

*Deeper tutorial coming with the docs' next tier; the contract above is
complete and current.*


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\lifecycle\overview.md (file state is current in your context — no need to Read it back)

# Live-agent lifecycle

What makes an agent endpoint a *persistent being* rather than a disposable
session: identity that survives resets, a working memory that follows it
across machines, and graceful endings that never lose context.

## The pieces

- **Perch** — the durable seat (identity, spool, state). Sessions attach to
  it (`api bind`/`listen`), reset across it (`api boundary`), and end
  without destroying it (`api session-end`).
- **The mind, in two tiers** — a *live* tier (who the agent is, what it's
  doing) that follows the endpoint everywhere, and a *project* tier scoped
  to one project. Both are versioned, tracked storage, synced to paired
  machines with the same scoping.
- **Commune** — the agent drops `<id>-commune.md` into the adapter's watched
  directory; spt-core ingests the delta into the right tier. A file-drop,
  not a command — any harness that can write a file can commune.
- **Signoff** — the graceful ending: final commune, then teardown
  (`spt shutdown` / `api shutdown`). The echo-commune fires **before**
  teardown, always.
- **Echo-commune** — sessions that end *without* a signoff don't lose their
  delta: spt-core runs the adapter's bounded summarizer template over the
  session history and ingests the result. The **echo gate** sentinel
  (armed on idle, cleared by graceful signoff) is what marks the need.
- **Psyche** — the endpoint's persistent-context companion process,
  spawned/resumed from the adapter's `psyche_init`/`psyche_resume`
  templates.

## Rest and wake

Endpoints rest instead of dying: **dormant** (warm — zero idle compute,
instantly wakeable) or **suspended** (cold), explicitly via `spt suspend` or
on attention-shift. Resting instances stay addressable; deferred messages
are held and released exactly once on wake (`spt wake`). Every
active→resting edge fires a **transition echo** so the final context delta
lands before the lights go out.

## Commands

`spt shutdown` · `suspend` · `wake` · the `api` lifecycle calls
([reference](../harness-contract/api.md#session-lifecycle)).

*Deeper tutorial coming with the docs' next tier; the contract above is
complete and current.*


# Terminal hosting

spt-core can *own* agent sessions in its own terminal layer: the daemon's
broker holds a real PTY per hosted session, which is what makes sessions
supervisable, attachable from other machines, and immune to self-update.

## What the broker holding the PTY buys

- **spt-hosted startup** — spt-core spawns sessions itself from the
  manifest's `[session.self]` template and binds them (`api bind`), instead
  of waiting inside someone else's process tree.
- **Remote attach** — a byte-stream viewport onto a live session from any
  paired node (compute and files stay on the hosting node). Restart-safe:
  reconnects resume the stream without gaps or duplicates.
- **Input injection** — `send-keys`/`send-line` style injection per the
  adapter's declared `[inject]` methods, respecting activity state (never
  disrupt a working agent).
- **The live digest** — with adapter-supplied `[pty_digest]` patterns,
  `spt digest <id>` shows an at-a-glance parsed view of what a session is
  doing now (`--follow` streams changes). Raw bytes and parsed digest are
  two channels over the same session surface.
- **Update immunity** — PTYs live in the broker, logic in the brain; a
  self-update swaps the brain while every hosted process and byte stream
  stays intact.

Activity and idleness are always **reported** (`api state busy|idle`), never
inferred from terminal quiescence — quiet terminals lie.

## Commands

`spt digest` · the attach surface · [`spt api` injection-adjacent calls](../harness-contract/api.md).

*Deeper tutorial coming with the docs' next tier.*


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\terminal\overview.md (file state is current in your context — no need to Read it back)

# Terminal hosting

spt-core can *own* agent sessions in its own terminal layer: the daemon's
broker holds a real PTY per hosted session, which is what makes sessions
supervisable, attachable from other machines, and immune to self-update.

## What the broker holding the PTY buys

- **spt-hosted startup** — spt-core spawns sessions itself from the
  manifest's `[session.self]` template and binds them (`api bind`), instead
  of waiting inside someone else's process tree.
- **Remote attach** — a byte-stream viewport onto a live session from any
  paired node (compute and files stay on the hosting node). Restart-safe:
  reconnects resume the stream without gaps or duplicates.
- **Input injection** — `send-keys`/`send-line` style injection per the
  adapter's declared `[inject]` methods, respecting activity state (never
  disrupt a working agent).
- **The live digest** — with adapter-supplied `[pty_digest]` patterns,
  `spt digest <id>` shows an at-a-glance parsed view of what a session is
  doing now (`--follow` streams changes). Raw bytes and parsed digest are
  two channels over the same session surface.
- **Update immunity** — PTYs live in the broker, logic in the brain; a
  self-update swaps the brain while every hosted process and byte stream
  stays intact.

Activity and idleness are always **reported** (`api state busy|idle`), never
inferred from terminal quiescence — quiet terminals lie.

## Commands

`spt digest` · the attach surface · [`spt api` injection-adjacent calls](../harness-contract/api.md).

*Deeper tutorial coming with the docs' next tier.*


# Networking & pairing

Zero-config, no-central-server connectivity between your machines. Pair two
nodes once with a six-digit code; from then on, the same `spt send bob` works
whether bob is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
- **Subnets** — machines pair into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  unpaired nodes, ever.
- **Pairing** — a one-time ceremony seeded by a TOTP code: run
  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI
  — put the seed in your authenticator app), type the subnet name + current
  six digits on the joiner. The code bootstraps a PAKE key exchange — the
  code is never the key, and a wrong guess learns nothing. Both sides pin
  each other's node keys on success (trust-on-first-use; key changes warn
  and never auto-apply).
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same paired substrate.

## Commands

`spt pair` · `spt resources` · the qualified addressing forms
(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).

*Pairing walkthrough tutorial coming with the docs' next tier.*


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\networking\overview.md (file state is current in your context — no need to Read it back)

# Networking & pairing

Zero-config, no-central-server connectivity between your machines. Pair two
nodes once with a six-digit code; from then on, the same `spt send bob` works
whether bob is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
- **Subnets** — machines pair into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  unpaired nodes, ever.
- **Pairing** — a one-time ceremony seeded by a TOTP code: run
  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI
  — put the seed in your authenticator app), type the subnet name + current
  six digits on the joiner. The code bootstraps a PAKE key exchange — the
  code is never the key, and a wrong guess learns nothing. Both sides pin
  each other's node keys on success (trust-on-first-use; key changes warn
  and never auto-apply).
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same paired substrate.

## Commands

`spt pair` · `spt resources` · the qualified addressing forms
(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).

*Pairing walkthrough tutorial coming with the docs' next tier.*


# Instances

One endpoint, several seats. `bob` is a single identity; an **instance** of
bob is his presence on one node. The registry tracks every instance's node
and state (active / dormant / suspended / offline), and the same mind syncs
to wherever he sits.

## The rules that keep it sane

- **Identity is adapter-agnostic and node-spanning** — instances on
  different nodes are rows under one endpoint id; renaming
  (`spt rename`) ripples everywhere, collision-checked.
- **Bare-id resolution never guesses** — `bob` resolves locally first, then
  to the sole live instance; with several live nodes (or several subnets)
  it refuses and makes you qualify (`bob@desktop`, `home:bob`). Per-node
  recency is not comparable across nodes, so there's no silent
  "most recently active" pick.
- **Home subnet is immutable** — assigned at creation. Moving an endpoint
  into another subnet is `spt fork`: a **new identity** seeded with a
  one-time copy of the mind, diverging immediately. Copy-then-diverge, never
  re-home — history stays honest.
- **Visibility is per-(endpoint, subnet)** — hidden means neither advertised
  nor routable there, and hidden gates sync too.
- **Rest states are first-class** — dormant (warm) and suspended (cold)
  instances stay addressable; deferred messages are held and released
  exactly once on wake. Remote `spt suspend bob@desktop` /
  `spt wake bob@desktop` work across paired nodes.

## Commands

`spt list` · `rename` · `fork` · `suspend` · `wake` · `resources` —
[CLI reference](../cli/reference.md).

*Cold-launching an endpoint on a node that has no instance
("instantiate-anywhere") is deliberately deferred behind the consent
framework; the gate exists and refuses today.*


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\instances\overview.md (file state is current in your context — no need to Read it back)

# Instances

One endpoint, several seats. `bob` is a single identity; an **instance** of
bob is his presence on one node. The registry tracks every instance's node
and state (active / dormant / suspended / offline), and the same mind syncs
to wherever he sits.

## The rules that keep it sane

- **Identity is adapter-agnostic and node-spanning** — instances on
  different nodes are rows under one endpoint id; renaming
  (`spt rename`) ripples everywhere, collision-checked.
- **Bare-id resolution never guesses** — `bob` resolves locally first, then
  to the sole live instance; with several live nodes (or several subnets)
  it refuses and makes you qualify (`bob@desktop`, `home:bob`). Per-node
  recency is not comparable across nodes, so there's no silent
  "most recently active" pick.
- **Home subnet is immutable** — assigned at creation. Moving an endpoint
  into another subnet is `spt fork`: a **new identity** seeded with a
  one-time copy of the mind, diverging immediately. Copy-then-diverge, never
  re-home — history stays honest.
- **Visibility is per-(endpoint, subnet)** — hidden means neither advertised
  nor routable there, and hidden gates sync too.
- **Rest states are first-class** — dormant (warm) and suspended (cold)
  instances stay addressable; deferred messages are held and released
  exactly once on wake. Remote `spt suspend bob@desktop` /
  `spt wake bob@desktop` work across paired nodes.

## Commands

`spt list` · `rename` · `fork` · `suspend` · `wake` · `resources` —
[CLI reference](../cli/reference.md).

*Cold-launching an endpoint on a node that has no instance
("instantiate-anywhere") is deliberately deferred behind the consent
framework; the gate exists and refuses today.*


# Shells

A **shell** is the non-agent endpoint kind: a *driven surface*. Notifiers,
robots, lamps, game characters, sensor feeds — anything an agent should be
able to command, and that might sense things back. Shells join the same
network as agents: addressable, discoverable, owned.

## The model in five facts

1. **A shell adapter declares it; instances are minted.** The
   `kind = "shell"` [manifest](../harness-contract/manifest.md#shell-adapters-kind--shell)
   declares the binary, its command vocabulary (`[shell.capabilities]`), and
   its sensory vocabulary (`[shell.sensory]`). `spt shell spawn <adapter>`
   mints a new instance (`notify-1`) — spawn is the creation act, not an
   on/off switch; bringing an existing instance back is relink/wake.
2. **The link token is the credential.** The broker mints a per-launch link
   token into the spawn template; the binary binds with it
   (`api bind-shell --link`), drains commands with it, emits with it. No
   token, no access.
3. **Commands are vocabulary-checked.** `spt shell cmd notify-1 notify
   "title" "body"` is validated against the manifest's declared verbs and
   arity before delivery — agents can't drive a shell outside its contract.
4. **Sensory is live-only.** `api emit` payloads reach a *live* owner
   session or are dropped with a diagnostic — sensors report the present,
   never the past.
5. **Instantiation is governed.** Per-spawn approval
   (`require_approval: none / remembered / always`), per-owner instance caps
   (`max_instances_per_owner` + `over_cap`), and node-local discovery scope
   (`broadcast`) are all manifest-declared floors.

Lifecycle extras: `persistent` shells auto-online with their owner;
`wake_command` runs a watcher while offline (exit code 86 = wake); a shell
with `can_shutdown = true` may suspend its own owner (`api owner-shutdown`)
— fail-closed otherwise.

## Start here

[Getting started: a notification shell](getting-started.md) — install the
shipping `spt-shell-notify` adapter, drive a native toast from an agent, and
copy its manifest for your own surface.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\shells\overview.md (file state is current in your context — no need to Read it back)

# Shells

A **shell** is the non-agent endpoint kind: a *driven surface*. Notifiers,
robots, lamps, game characters, sensor feeds — anything an agent should be
able to command, and that might sense things back. Shells join the same
network as agents: addressable, discoverable, owned.

## The model in five facts

1. **A shell adapter declares it; instances are minted.** The
   `kind = "shell"` [manifest](../harness-contract/manifest.md#shell-adapters-kind--shell)
   declares the binary, its command vocabulary (`[shell.capabilities]`), and
   its sensory vocabulary (`[shell.sensory]`). `spt shell spawn <adapter>`
   mints a new instance (`notify-1`) — spawn is the creation act, not an
   on/off switch; bringing an existing instance back is relink/wake.
2. **The link token is the credential.** The broker mints a per-launch link
   token into the spawn template; the binary binds with it
   (`api bind-shell --link`), drains commands with it, emits with it. No
   token, no access.
3. **Commands are vocabulary-checked.** `spt shell cmd notify-1 notify
   "title" "body"` is validated against the manifest's declared verbs and
   arity before delivery — agents can't drive a shell outside its contract.
4. **Sensory is live-only.** `api emit` payloads reach a *live* owner
   session or are dropped with a diagnostic — sensors report the present,
   never the past.
5. **Instantiation is governed.** Per-spawn approval
   (`require_approval: none / remembered / always`), per-owner instance caps
   (`max_instances_per_owner` + `over_cap`), and node-local discovery scope
   (`broadcast`) are all manifest-declared floors.

Lifecycle extras: `persistent` shells auto-online with their owner;
`wake_command` runs a watcher while offline (exit code 86 = wake); a shell
with `can_shutdown = true` may suspend its own owner (`api owner-shutdown`)
— fail-closed otherwise.

## Start here

[Getting started: a notification shell](getting-started.md) — install the
shipping `spt-shell-notify` adapter, drive a native toast from an agent, and
copy its manifest for your own surface.


# Self-update

spt-core keeps itself current without ever interrupting your agents, and
without trusting anything unsigned.

## The invariant

**No endpoint process terminates or suspends during a self-update.** The
daemon's broker (holding PTYs, child processes, sockets) stays up; the brain
(all logic) swaps under it. A hosted session's process id and byte stream are
identical before and after.

## The trust chain

- Every release ships `SignedRelease` metadata: an Ed25519 signature over the
  release's artifact digests.
- Every binary embeds the **two-key trusted set** — an active primary and a
  never-used offline recovery key. Verification requires a valid signature
  from a trusted key *and* a matching artifact digest; an unverified binary
  never reaches the apply step.
- Losing the primary key is a non-event: the next release is signed with the
  recovery key (already trusted by every deployed binary) and rotates in a
  fresh primary.
- **Adapters sign their own content.** A `file_pull` adapter update is
  verified against the adapter author's key from its manifest; a `delegated`
  update is trusted only when the manifest attests the delegated updater
  verifies its own content (`self_verifies`). spt-core's release keys never
  vouch for adapter bytes.

## How updates move

Peer-propagated: one node fetches a release; paired nodes offer/fetch staged
releases from each other, each verifying independently before staging.
Updating is **consent-gated by default** — a notification surfaces at your
most-recently-active endpoint, and `spt update apply` is the explicit ack
(it re-verifies the staged release before touching the live daemon).
Full-auto is an explicit opt-in.

After self-updating, spt-core **ripple-updates registered adapters** through
each manifest's declared `[update]` avenue.

## Commands

`spt update` · the consent notification flow (`spt notif`) —
[CLI reference](../cli/reference.md).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\self-update\overview.md (file state is current in your context — no need to Read it back)

# Self-update

spt-core keeps itself current without ever interrupting your agents, and
without trusting anything unsigned.

## The invariant

**No endpoint process terminates or suspends during a self-update.** The
daemon's broker (holding PTYs, child processes, sockets) stays up; the brain
(all logic) swaps under it. A hosted session's process id and byte stream are
identical before and after.

## The trust chain

- Every release ships `SignedRelease` metadata: an Ed25519 signature over the
  release's artifact digests.
- Every binary embeds the **two-key trusted set** — an active primary and a
  never-used offline recovery key. Verification requires a valid signature
  from a trusted key *and* a matching artifact digest; an unverified binary
  never reaches the apply step.
- Losing the primary key is a non-event: the next release is signed with the
  recovery key (already trusted by every deployed binary) and rotates in a
  fresh primary.
- **Adapters sign their own content.** A `file_pull` adapter update is
  verified against the adapter author's key from its manifest; a `delegated`
  update is trusted only when the manifest attests the delegated updater
  verifies its own content (`self_verifies`). spt-core's release keys never
  vouch for adapter bytes.

## How updates move

Peer-propagated: one node fetches a release; paired nodes offer/fetch staged
releases from each other, each verifying independently before staging.
Updating is **consent-gated by default** — a notification surfaces at your
most-recently-active endpoint, and `spt update apply` is the explicit ack
(it re-verifies the staged release before touching the live daemon).
Full-auto is an explicit opt-in.

After self-updating, spt-core **ripple-updates registered adapters** through
each manifest's declared `[update]` avenue.

## Commands

`spt update` · the consent notification flow (`spt notif`) —
[CLI reference](../cli/reference.md).


Remaining reference pages + theme CSS:


# CLI reference

> **Generated page.** This reference is produced from the `spt` binary's own
> `--help` output in CI and checked against it on every push — it cannot
> drift from the binary. The generated content replaces this notice in the
> published site.

Until you're reading the generated copy, the binary itself is the same
reference: `spt --help` groups every command, and `spt <command> --help`
documents each flag with examples. The help text is maintained as a
first-class documentation surface for both humans and agents.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\cli\reference.md (file state is current in your context — no need to Read it back)

# CLI reference

> **Generated page.** This reference is produced from the `spt` binary's own
> `--help` output in CI and checked against it on every push — it cannot
> drift from the binary. The generated content replaces this notice in the
> published site.

Until you're reading the generated copy, the binary itself is the same
reference: `spt --help` groups every command, and `spt <command> --help`
documents each flag with examples. The help text is maintained as a
first-class documentation surface for both humans and agents.


# Manifest JSON Schema

The machine-readable contract for adapter manifests, served at a stable URL:

**<https://sabermage.github.io/spt-releases/manifest.schema.json>**

- Generated from the **same code that parses manifests** — the schema is
  always exactly what `spt adapter add` accepts structurally. It also ships
  as a release asset with every release.
- JSON Schema draft 2020-12; the `$id` is the canonical URL above and is
  stable across releases.
- Field doc-comments ride along as `description`s — the schema doubles as
  field-level documentation.
- Manifests are authored as **TOML**; the schema describes the equivalent
  data model (validate the TOML-parsed document).
- Cross-field rules the schema can't express (kind↔`[shell]` agreement,
  strategy/avenue required fields) are listed in the
  [manifest reference](../harness-contract/manifest.md#cross-field-rules-spt-adapter-add-enforces-these)
  and enforced by `spt adapter add`.

Example — validate a manifest mechanically (Python, any JSON-Schema
validator works the same way):

```python
import json, tomllib, urllib.request, jsonschema

schema = json.load(urllib.request.urlopen(
    "https://sabermage.github.io/spt-releases/manifest.schema.json"))
with open("manifest.toml", "rb") as f:
    manifest = tomllib.load(f)
jsonschema.validate(manifest, schema)   # raises on violation
print("manifest is structurally valid")
```


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\reference\schema.md (file state is current in your context — no need to Read it back)

# Manifest JSON Schema

The machine-readable contract for adapter manifests, served at a stable URL:

**<https://sabermage.github.io/spt-releases/manifest.schema.json>**

- Generated from the **same code that parses manifests** — the schema is
  always exactly what `spt adapter add` accepts structurally. It also ships
  as a release asset with every release.
- JSON Schema draft 2020-12; the `$id` is the canonical URL above and is
  stable across releases.
- Field doc-comments ride along as `description`s — the schema doubles as
  field-level documentation.
- Manifests are authored as **TOML**; the schema describes the equivalent
  data model (validate the TOML-parsed document).
- Cross-field rules the schema can't express (kind↔`[shell]` agreement,
  strategy/avenue required fields) are listed in the
  [manifest reference](../harness-contract/manifest.md#cross-field-rules-spt-adapter-add-enforces-these)
  and enforced by `spt adapter add`.

Example — validate a manifest mechanically (Python, any JSON-Schema
validator works the same way):

```python
import json, tomllib, urllib.request, jsonschema

schema = json.load(urllib.request.urlopen(
    "https://sabermage.github.io/spt-releases/manifest.schema.json"))
with open("manifest.toml", "rb") as f:
    manifest = tomllib.load(f)
jsonschema.validate(manifest, schema)   # raises on violation
print("manifest is structurally valid")
```


# Install scripts

The canonical non-interactive installers, served at the permanent URLs:

- **Linux:** <https://sabermage.github.io/spt-releases/install.sh>

  ```sh
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
  ```

- **Windows:** <https://sabermage.github.io/spt-releases/install.ps1>

  ```powershell
  irm https://sabermage.github.io/spt-releases/install.ps1 | iex
  ```

## What they do

1. Resolve the latest release (or `SPT_INSTALL_VERSION`).
2. Download the platform binary and the release's `SHA256SUMS`.
3. **Verify the sha256** — a mismatch refuses before anything is placed.
4. Place the binary under the per-OS install root (`~/.local/bin` /
   `%LOCALAPPDATA%\spt\bin`).
5. Register the **user** PATH (at most once; idempotent re-runs).
6. Print the absolute installed path — on Windows the PATH change reaches
   **new** terminals only, so the first invocation in the current one uses
   that absolute path.

They never prompt (non-interactive by construction — they double as every
adapter's [pack-in installer](../harness-contract/install-on-demand.md)),
and re-running is always safe.

## Environment knobs

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Pin a release tag (default: latest) |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_REPO` | Override the source repo (default `SaberMage/spt-releases`) |
| `SPT_INSTALL_ASSET_BASE` | URL or local dir holding assets + `SHA256SUMS` directly (CI / air-gap / mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

## Trust model

First fetch: HTTPS + GitHub + sha256 against the release's `SHA256SUMS`.
Thereafter `spt update` performs full Ed25519 verification against the
[two-key trust anchor](../self-update/overview.md#the-trust-chain) embedded
in the binary. MIT-licensed — copy them into your own bootstrap freely.

## OS-service registration

Not yet: the daemon auto-starts on any `spt` invocation, which covers
dev-stage use. Known gap until then: after a reboot, a node is unreachable
until something on it invokes `spt`. Service registration ships in a later
release.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\src\reference\install.md (file state is current in your context — no need to Read it back)

# Install scripts

The canonical non-interactive installers, served at the permanent URLs:

- **Linux:** <https://sabermage.github.io/spt-releases/install.sh>

  ```sh
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
  ```

- **Windows:** <https://sabermage.github.io/spt-releases/install.ps1>

  ```powershell
  irm https://sabermage.github.io/spt-releases/install.ps1 | iex
  ```

## What they do

1. Resolve the latest release (or `SPT_INSTALL_VERSION`).
2. Download the platform binary and the release's `SHA256SUMS`.
3. **Verify the sha256** — a mismatch refuses before anything is placed.
4. Place the binary under the per-OS install root (`~/.local/bin` /
   `%LOCALAPPDATA%\spt\bin`).
5. Register the **user** PATH (at most once; idempotent re-runs).
6. Print the absolute installed path — on Windows the PATH change reaches
   **new** terminals only, so the first invocation in the current one uses
   that absolute path.

They never prompt (non-interactive by construction — they double as every
adapter's [pack-in installer](../harness-contract/install-on-demand.md)),
and re-running is always safe.

## Environment knobs

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Pin a release tag (default: latest) |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_REPO` | Override the source repo (default `SaberMage/spt-releases`) |
| `SPT_INSTALL_ASSET_BASE` | URL or local dir holding assets + `SHA256SUMS` directly (CI / air-gap / mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

## Trust model

First fetch: HTTPS + GitHub + sha256 against the release's `SHA256SUMS`.
Thereafter `spt update` performs full Ed25519 verification against the
[two-key trust anchor](../self-update/overview.md#the-trust-chain) embedded
in the binary. MIT-licensed — copy them into your own bootstrap freely.

## OS-service registration

Not yet: the daemon auto-starts on any `spt` invocation, which covers
dev-stage use. Known gap until then: after a reboot, a node is unreachable
until something on it invokes `spt`. Service registration ships in a later
release.


/* SPT docs theme — Starlight-inspired skin over mdBook (M6-D4).
   North star: Astro Starlight's look (accent violet-blue, calm dark surface,
   tight typographic rhythm, soft-rounded panels) — its look, not its
   toolchain (DOCS-STRATEGY §v0.1). Layered onto the ayu theme's variables. */

:root {
  --spt-accent: #4f7cf7;
  --spt-accent-high: #82a7ff;
  --spt-radius: 8px;
}

/* ---- base surface + type ---- */
.ayu {
  --bg: #17181c;
  --fg: #e7e9ee;
  --sidebar-bg: #1b1d23;
  --sidebar-fg: #b3b8c3;
  --sidebar-active: var(--spt-accent-high);
  --scrollbar: #2a2d35;
  --links: var(--spt-accent-high);
  --inline-code-color: #e7e9ee;
  --theme-popup-bg: #1b1d23;
  --theme-popup-border: #2a2d35;
  --theme-hover: #23262e;
  --quote-bg: #1d2027;
  --quote-border: var(--spt-accent);
  --table-border-color: #2a2d35;
  --table-header-bg: #1f2229;
  --table-alternate-bg: #1b1d23;
  --searchbar-bg: #1f2229;
  --searchbar-fg: #e7e9ee;
  --searchresults-header-fg: #8b91a0;
  --searchresults-border-color: #2a2d35;
  --color-scheme: dark;
}

html {
  font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
}

.content {
  line-height: 1.65;
}

.content main {
  max-width: 46rem;
}

/* ---- headings: Starlight's tight, confident scale ---- */
.content h1 {
  font-size: 2rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  margin-bottom: 0.6em;
}
.content h2 {
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-top: 2em;
  padding-bottom: 0.25em;
  border-bottom: 1px solid #2a2d35;
}
.content h3 {
  font-size: 1.1rem;
  font-weight: 600;
  margin-top: 1.6em;
}

/* ---- links ---- */
.content a {
  color: var(--spt-accent-high);
  text-decoration: none;
}
.content a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- code ---- */
:not(pre) > .hljs {
  background: #23262e;
  border-radius: 4px;
  padding: 0.1em 0.35em;
  font-size: 0.875em;
}
pre {
  border-radius: var(--spt-radius);
}
pre > .hljs {
  background: #1b1d23 !important;
  border: 1px solid #2a2d35;
  border-radius: var(--spt-radius);
  padding: 1rem 1.25rem;
  font-size: 0.875rem;
  line-height: 1.6;
}

/* ---- blockquotes as Starlight "note" asides ---- */
.content blockquote {
  background: var(--quote-bg);
  border: 1px solid #2a2d35;
  border-left: 3px solid var(--spt-accent);
  border-radius: 0 var(--spt-radius) var(--spt-radius) 0;
  padding: 0.75rem 1.1rem;
  margin: 1.25rem 0;
}
.content blockquote p {
  margin: 0.35rem 0;
}

/* ---- tables ---- */
.content table {
  border-collapse: collapse;
  border-radius: var(--spt-radius);
  overflow: hidden;
  width: 100%;
}
.content table th {
  text-align: left;
  font-weight: 600;
  background: var(--table-header-bg);
}
.content table th,
.content table td {
  border: 1px solid var(--table-border-color);
  padding: 0.45rem 0.75rem;
}

/* ---- sidebar: Starlight's quiet rail ---- */
.sidebar {
  font-size: 0.875rem;
}
.sidebar .part-title {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  font-weight: 650;
  color: #8b91a0;
  margin-top: 1.25rem;
}
.sidebar .chapter li.chapter-item {
  margin: 0.15rem 0;
}
.sidebar .chapter li > a {
  border-radius: 6px;
  padding: 0.25rem 0.5rem;
}
.sidebar .chapter li > a:hover {
  background: var(--theme-hover);
  text-decoration: none;
}
.sidebar .chapter li > a.active {
  background: rgba(79, 124, 247, 0.14);
  color: var(--spt-accent-high);
  font-weight: 600;
}

/* ---- top bar ---- */
#menu-bar {
  border-bottom: 1px solid #23262e;
}
.menu-title {
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* ---- light theme fallback keeps the same accent ---- */
.light,
.rust {
  --links: #2b5cd9;
  --quote-border: #2b5cd9;
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs-site\theme\spt.css (file state is current in your context — no need to Read it back)

/* SPT docs theme — Starlight-inspired skin over mdBook (M6-D4).
   North star: Astro Starlight's look (accent violet-blue, calm dark surface,
   tight typographic rhythm, soft-rounded panels) — its look, not its
   toolchain (DOCS-STRATEGY §v0.1). Layered onto the ayu theme's variables. */

:root {
  --spt-accent: #4f7cf7;
  --spt-accent-high: #82a7ff;
  --spt-radius: 8px;
}

/* ---- base surface + type ---- */
.ayu {
  --bg: #17181c;
  --fg: #e7e9ee;
  --sidebar-bg: #1b1d23;
  --sidebar-fg: #b3b8c3;
  --sidebar-active: var(--spt-accent-high);
  --scrollbar: #2a2d35;
  --links: var(--spt-accent-high);
  --inline-code-color: #e7e9ee;
  --theme-popup-bg: #1b1d23;
  --theme-popup-border: #2a2d35;
  --theme-hover: #23262e;
  --quote-bg: #1d2027;
  --quote-border: var(--spt-accent);
  --table-border-color: #2a2d35;
  --table-header-bg: #1f2229;
  --table-alternate-bg: #1b1d23;
  --searchbar-bg: #1f2229;
  --searchbar-fg: #e7e9ee;
  --searchresults-header-fg: #8b91a0;
  --searchresults-border-color: #2a2d35;
  --color-scheme: dark;
}

html {
  font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
}

.content {
  line-height: 1.65;
}

.content main {
  max-width: 46rem;
}

/* ---- headings: Starlight's tight, confident scale ---- */
.content h1 {
  font-size: 2rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  margin-bottom: 0.6em;
}
.content h2 {
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-top: 2em;
  padding-bottom: 0.25em;
  border-bottom: 1px solid #2a2d35;
}
.content h3 {
  font-size: 1.1rem;
  font-weight: 600;
  margin-top: 1.6em;
}

/* ---- links ---- */
.content a {
  color: var(--spt-accent-high);
  text-decoration: none;
}
.content a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- code ---- */
:not(pre) > .hljs {
  background: #23262e;
  border-radius: 4px;
  padding: 0.1em 0.35em;
  font-size: 0.875em;
}
pre {
  border-radius: var(--spt-radius);
}
pre > .hljs {
  background: #1b1d23 !important;
  border: 1px solid #2a2d35;
  border-radius: var(--spt-radius);
  padding: 1rem 1.25rem;
  font-size: 0.875rem;
  line-height: 1.6;
}

/* ---- blockquotes as Starlight "note" asides ---- */
.content blockquote {
  background: var(--quote-bg);
  border: 1px solid #2a2d35;
  border-left: 3px solid var(--spt-accent);
  border-radius: 0 var(--spt-radius) var(--spt-radius) 0;
  padding: 0.75rem 1.1rem;
  margin: 1.25rem 0;
}
.content blockquote p {
  margin: 0.35rem 0;
}

/* ---- tables ---- */
.content table {
  border-collapse: collapse;
  border-radius: var(--spt-radius);
  overflow: hidden;
  width: 100%;
}
.content table th {
  text-align: left;
  font-weight: 600;
  background: var(--table-header-bg);
}
.content table th,
.content table td {
  border: 1px solid var(--table-border-color);
  padding: 0.45rem 0.75rem;
}

/* ---- sidebar: Starlight's quiet rail ---- */
.sidebar {
  font-size: 0.875rem;
}
.sidebar .part-title {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  font-weight: 650;
  color: #8b91a0;
  margin-top: 1.25rem;
}
.sidebar .chapter li.chapter-item {
  margin: 0.15rem 0;
}
.sidebar .chapter li > a {
  border-radius: 6px;
  padding: 0.25rem 0.5rem;
}
.sidebar .chapter li > a:hover {
  background: var(--theme-hover);
  text-decoration: none;
}
.sidebar .chapter li > a.active {
  background: rgba(79, 124, 247, 0.14);
  color: var(--spt-accent-high);
  font-weight: 600;
}

/* ---- top bar ---- */
#menu-bar {
  border-bottom: 1px solid #23262e;
}
.menu-title {
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* ---- light theme fallback keeps the same accent ---- */
.light,
.rust {
  --links: #2b5cd9;
  --quote-border: #2b5cd9;
}


PowerShell

cd C:\Users\decid\Documents\projects\spt-core\docs-site; mdbook build 2>&1 | Select-Object -Last 6; Test-Path book\index.html

Build the docs site


 INFO Book building has started
 INFO Running the html backend
 INFO HTML book written to `C:\Users\decid\Documents\projects\spt-core\docs-site\book`
True


Build clean. Quick visual check of the theme:


Skill

open file:///C:/Users/decid/Documents/projects/spt-core/docs-site/book/index.html and take a screenshot


Launching skill: browse