## Shells in the layout

Shells are endpoints but a different shape from agent endpoints:
- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.
- **Not in the subnet registry** — private to the agent↔shell link, not a general messaging surface.
- **No `tracked/` context** — a Shell has no mind; never synced.
- **Logs** in node-local `history/<endpoint_id>/shells/<adapter>-<n>.jsonl`.
- **Adapter/platform-bound, not adapter-agnostic** — capability surface comes from the providing shell adapter; it does not move harnesses.
- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.
- **Cross-node links:** the shell perch lives on the shell's node (nested under the owner id as a label there); commands ride Iroh from the owner's node.

# $SPT_HOME on-disk layout

> Draft design doc (2026-05-30). Implementation detail — neither glossary ([`../CONTEXT.md`](../CONTEXT.md)) nor decision ([`adr/`](./adr/)). Resolves PRD §17.1.

## Principles

1. **Single relocatable root.** One `$SPT_HOME` dir holds everything (default `%LOCALAPPDATA%\spt` / `~/.spt`; override via `$SPT_HOME`). One dir to back up, move, or repackage for a marketplace/handheld. No XDG three-way split.
2. **Endpoint IDs are adapter-agnostic.** `ling` can run under `claude-spt`, then later under `spt-codex` or `spt-pi`. Perches and context key on bare `endpoint_id`; the currently-running `adapter_name` is a *property of the instance* recorded in `info.json`, never a path segment. **At most one instance of an ID per node** — a second attempt (any adapter) is rejected.
3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.
4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.
5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).

## Layout

```
$SPT_HOME/
  identity/                      ── node-local, durable, NEVER git-tracked ──
    node.key                     Ed25519 node identity (per-node; must not sync)
    trust/                       authorized peer pubkeys (trust store)
    subnet.json                  subnet name + TOTP seed (replicated via pairing, not git)
  tracked/                       ── git repo; synced across nodes over P2P ──
    agents/<endpoint_id>/
      meta.json                  endpoint identity metadata: type, ordered adapter history, created
      live-context.md            per-agent live context           (branch a-<endpoint_id>)
    projects/<project_id>/
      <endpoint_id>/
        project-context.md       per-(agent,project) context      (branch p-<project_id>)
  perches/                       ── node-local runtime, NEVER tracked/synced ──
    <endpoint_id>/
      info.json                  identity, type, status, CURRENT adapter_name (+ capability set if Shell)
      spool.db                   SQLite spool (typed + binary payloads)
      status                     active | dormant | suspended    (agent endpoints)
      nested/<child_id>/         Worker / Psyche perches (own info.json, spool)
      shells/<adapter>-<n>/      Shell perches owned by this endpoint (GameRobot-0, GameRobot-1…)
        info.json                type=Shell, owner, adapter_name, status (active|offline)
        spool.db                 command + durable text/file (sensory is REST-only, unspooled)
  history/                       node-local logs (mirrors perch structure)
    <endpoint_id>/
      sessions/<session_id>.jsonl    agent Path-B native logs
      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log
  registry/                      ── ephemeral ── local cache of the subnet registry
  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata
  updates/                       ── ephemeral ── staged binaries pending handoff
  adapters/                      registered adapter manifests / pointers
```

## tracked/ — the context git repo

- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing ("which endpoint is most responsible for this task").
- **Two-tier sync maps onto the structure:** `agents/<id>/live-context.md` (the `a-` view) syncs to **all** instances of the endpoint; `projects/<project_id>/<id>/project-context.md` (the `p-` view) syncs only to instances sharing that project.
- **P2P replaces the *remote*, not the local git.** Modern SPT pushed this repo to a private GitHub remote; spt-core keeps the local git tracking + branch views and moves the data over the built-in P2P layer instead (no `gh`/account/setup). The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — arbitrates concurrent writes from multiple nodes/instances.

## project_id

Derived **from the git remote URL** of the project if it has one (stable + path-independent, so the *same* project at different absolute paths on different machines — `/home/x/proj` vs `C:\Users\x\proj` — resolves to the *same* `project_id`, which is what makes project-context sync match across instances). If the project has no git repo, fall back to the project **folder name**. (Folder-name fallback is not guaranteed path-independent across machines — a known limitation for non-git projects.)

## perches/ — node-local runtime

- Holds the clobber-sensitive, node-specific state: `info.json` (identity, type, current `adapter_name`, status), the SQLite `spool.db`, the `status` marker, and `nested/` Worker/Psyche child perches.
- **Never** part of `tracked/` and never synced — each node owns its own perch runtime. This is the clean alternative to mixing perch files into the context repo and excluding them via gitignore.

## Endpoint metadata & adapter history (`tracked/agents/<id>/meta.json`)

- Records the endpoint's durable identity: type, creation, and an **ordered adapter history** (most-recent first). Synced, so any node knows it.
- Drives the **resume UX**: the latest adapter is the default resume offer, the next-latest is second, … the oldest is second-to-last, and "choose a different adapter" is the final option.
- The *currently-running* `adapter_name` lives in node-local `perches/<id>/info.json`; the *history* is endpoint identity in `tracked/`.

## Shells in the layout

Shells are endpoints but a different shape from agent endpoints:
- **Perch, yes:** `perches/<shell_id>/` — `info.json` with `type=Shell` + its capability toolset, a `spool.db` carrying typed operation-commands inbound and user responses/files outbound, a status.
- **No `tracked/` context:** a Shell has no mind (no live/project context, no Psyche), so it never appears under `tracked/` and is never context-synced.
- **Logs** go in node-local `history/` (Shell logs).
- **Adapter/platform-bound, not adapter-agnostic:** the adapter-hopping property is for *agent* endpoints only. A Shell's capability surface comes from its providing adapter/platform; it does not move harnesses.
- **Lifecycle = instantiate / teardown** (by an agent or a PresenceChannel `bind`), not the dormant/suspended resting model of agent instances.

## Migration & backup

- **Back up / migrate:** `identity/` + `tracked/` (+ `history/` if wanted). That's identity, trust, subnet, and the full agent/project mind.
- **Regenerate on the target:** `perches/`, `registry/`, `daemon/`, `updates/` rebuild from the durable set + the live subnet.
- Legacy `claude_skill_owl` migration maps its `psyches/tracked/` git repo into `tracked/` and its node identity/agents into `identity/` + `perches/`.

## Open sub-items

- Exact carry-over of modern SPT's git branch mechanics (worktrees vs branch-checkout) into `tracked/`.
- Whether `history/` ever needs cross-node fetch (today node-local; history-lookup over the network would serve remote reads).

-- **Perch, yes:** `perches/<shell_id>/` — `info.json` with `type=Shell` + its capability toolset, a `spool.db` carrying typed operation-commands inbound and user responses/files outbound, a status.

-- **No `tracked/` context:** a Shell has no mind (no live/project context, no Psyche), so it never appears under `tracked/` and is never context-synced.

-- **Logs** go in node-local `history/` (Shell logs).

-- **Adapter/platform-bound, not adapter-agnostic:** the adapter-hopping property is for *agent* endpoints only. A Shell's capability surface comes from its providing adapter/platform; it does not move harnesses.

-- **Lifecycle = instantiate / teardown** (by an agent or a PresenceChannel `bind`), not the dormant/suspended resting model of agent instances.

+- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.

+- **Not in the subnet registry** — private to the agent↔shell link, not a general messaging surface.

+- **No `tracked/` context** — a Shell has no mind; never synced.

+- **Logs** in node-local `history/<endpoint_id>/shells/<adapter>-<n>.jsonl`.

+- **Adapter/platform-bound, not adapter-agnostic** — capability surface comes from the providing shell adapter; it does not move harnesses.

+- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.

+- **Cross-node links:** the shell perch lives on the shell's node (nested under the owner id as a label there); commands ride Iroh from the owner's node.


Now DEFERRED.md — shell-binary sandboxing.


Edit

| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |

| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |


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

| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works; consent UX is its own design | After remote-drive ships and consent model is designed |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |

+| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |

+| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |


All four docs updated. The Shell deep-dive also effectively answers **§17.6** — let me close it: remote-control and Shells are *opposite directions*. A Shell is **agent→surface** (the agent drives an embodiment and receives sensory). Remote-control is **user→agent** (a viewport onto a running agent instance). So remote-control is **not** a formal Shell — it's the bare network session-surface. Updating PRD.


- Refining Psyche live/project authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

- Refining Psyche live/project authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- ~~Remote-control: formal Shell or bare session-surface?~~ — **resolved**: opposite directions. A **Shell is agent→surface** (agent drives embodiment, receives sensory); **remote-control is user→agent** (a viewport onto a running instance). Remote-control is the bare network session-surface, *not* a Shell. (Shell model fully specified in CONTEXT §Shell model, `docs/MANIFEST.md` `kind="shell"`, `docs/STORAGE.md`.)


<tool_use_error>String to replace not found in file.
String: - Refining Psyche live/project authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.</tool_use_error>

Error: String to replace not found in file.
String: - Refining Psyche live/project authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.


please proceed to capture. --- note that ephemeral shells also mean they are removed from the agent's shell history after they terminate. -agree, defer shell-binary trust/sandboxing. same thing could…


Design spt-core binary and messaging library architecture


<system-reminder>Warning: the file exists but is shorter than the provided offset (232). The file has 196 lines.</system-reminder>


170
171	1. A non-CC harness (or a headless spike) drives the full live-agent lifecycle through manifest + `api` alone — proving harness independence.
172	2. The rebuilt spt-plugin reaches **feature parity** with frozen `claude_skill_owl`.
173	3. Two of a user's machines, freshly paired via one TOTP code, exchange messages and the user drives an agent on machine A from machine B — zero config beyond pairing.
174	4. A self-update rolls across the subnet **without terminating any endpoint**.
175	5. The same agent's mind follows the user across machines (two-tier sync); switching machines leaves the prior instance dormant and catches up the new active one.
176	6. Every `KNOWN-HAZARDS` invariant has a corresponding passing test.
177
178	## 17. Open design-phase questions
179
180	- ~~Exact `$SPT_HOME` on-disk layout~~ — **resolved**, see [`docs/STORAGE.md`](./docs/STORAGE.md) (single relocatable root; adapter-agnostic ID keying; separate `tracked/` git repo vs node-local `perches/`; one path resolver).
181	- ~~Manifest schema concretes~~ — **resolved**, see [`docs/MANIFEST.md`](./docs/MANIFEST.md) (opaque command templates; per-hook `can_inject`; node-wide cross-adapter fallback; global+override config knobs; worked `claude-spt` example). Capability-declaration shape beyond `hostable_types` remains open.
182	- Refining Psyche authoring directives so the live/project context split holds cleanly (R-INST-5).
183	- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
184	- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
185	- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.
186
187	## 18. References
188
189	- [`CONTEXT.md`](./CONTEXT.md) — glossary & model (authoritative for meaning).
190	- [`docs/adr/0001`](./docs/adr/0001-clean-room-fork-from-claude-skill-owl.md) … [`0005`](./docs/adr/0005-totp-seeded-spake2-node-pairing.md) — decisions.
191	- [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) — invariants harvested from the sister project (test checklist).
192	- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
193	- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.
194	- [`docs/MANIFEST.md`](./docs/MANIFEST.md) — runtime manifest schema + worked `claude-spt` example.
195	- Sister project networking research: `claude_skill_owl/docs/research/SPT Networked Messaging Research Brief.md`.

1. A non-CC harness (or a headless spike) drives the full live-agent lifecycle through manifest + `api` alone — proving harness independence.
2. The rebuilt spt-plugin reaches **feature parity** with frozen `claude_skill_owl`.
3. Two of a user's machines, freshly paired via one TOTP code, exchange messages and the user drives an agent on machine A from machine B — zero config beyond pairing.
4. A self-update rolls across the subnet **without terminating any endpoint**.
5. The same agent's mind follows the user across machines (two-tier sync); switching machines leaves the prior instance dormant and catches up the new active one.
6. Every `KNOWN-HAZARDS` invariant has a corresponding passing test.

## 17. Open design-phase questions

- ~~Exact `$SPT_HOME` on-disk layout~~ — **resolved**, see [`docs/STORAGE.md`](./docs/STORAGE.md) (single relocatable root; adapter-agnostic ID keying; separate `tracked/` git repo vs node-local `perches/`; one path resolver).
- ~~Manifest schema concretes~~ — **resolved**, see [`docs/MANIFEST.md`](./docs/MANIFEST.md) (opaque command templates; per-hook `can_inject`; node-wide cross-adapter fallback; global+override config knobs; worked `claude-spt` example). Capability-declaration shape beyond `hostable_types` remains open.
- Refining Psyche authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

## 18. References

- [`CONTEXT.md`](./CONTEXT.md) — glossary & model (authoritative for meaning).
- [`docs/adr/0001`](./docs/adr/0001-clean-room-fork-from-claude-skill-owl.md) … [`0005`](./docs/adr/0005-totp-seeded-spake2-node-pairing.md) — decisions.
- [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) — invariants harvested from the sister project (test checklist).
- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.
- [`docs/MANIFEST.md`](./docs/MANIFEST.md) — runtime manifest schema + worked `claude-spt` example.
- Sister project networking research: `claude_skill_owl/docs/research/SPT Networked Messaging Research Brief.md`.


- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

- ~~Remote-control: formal Shell or bare session-surface?~~ — **resolved**: opposite directions. A **Shell is agent→surface** (agent drives embodiment, receives sensory); **remote-control is user→agent** (a viewport onto a running instance). Remote-control is the bare network session-surface, *not* a Shell. (Shell model now fully specified — CONTEXT §Shell model, `docs/MANIFEST.md` `kind="shell"`, `docs/STORAGE.md`.)


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

- Refining Psyche authoring directives so the live/project context split holds cleanly (R-INST-5).

- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.

- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.

-- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

+- ~~Remote-control: formal Shell or bare session-surface?~~ — **resolved**: opposite directions. A **Shell is agent→surface** (agent drives embodiment, receives sensory); **remote-control is user→agent** (a viewport onto a running instance). Remote-control is the bare network session-surface, *not* a Shell. (Shell model now fully specified — CONTEXT §Shell model, `docs/MANIFEST.md` `kind="shell"`, `docs/STORAGE.md`.)


Shell model fully captured across CONTEXT (§Shell model), MANIFEST (`kind="shell"` adapter), STORAGE (nested perch + history org), DEFERRED (sandboxing + concrete shells), and §17.6 resolved in PRD.

**What landed:**
- **Two adapter kinds** — harness (`claude-spt`) vs shell (`GameRobot`); both manifested, both in `adapters/`.
- **Three channels** — command (durable), text+file (durable, 2-way forwarder, progress-queryable), sensory (REST-only, never spooled).
- **Owner-linked exclusive**, cross-node-capable, nested perch `perches/<id>/shells/<adapter>-<n>/`, not in the registry, capability set resolved from the manifest (not the perch).
- **Lifecycle** active/offline/torn-down; link-break always closes the binary (+ pre-close instruction + timeout); ephemeral ⇒ torn down + dropped from shell history; persistent ⇒ offline + relinkable.
- **Security** — local-link auth (handshake + encryption on same-node channels), binary-trust as a disclosed accepted risk (sandboxing deferred for shells *and* harnesses).
- **Commands** — agent surface `shell spawn|list|relink|teardown|cmd`; `api emit --type` for sensory.
- **§17.6 resolved** — remote-control is user→agent (bare session-surface), Shells are agent→surface; opposite directions, distinct concepts.

§17 remaining: **17.3** (Psyche live/project authoring directives — the live problem that makes two-tier sync hold), **17.4** (dormancy default + suspend surface), **17.5** (consent/security-gate UX). Which next?


for shells, please change lifecycle nomenclature "active"-->"online". --- before we do more, please use `gh` cli to start the `spt-node` repo here, initial commit, and publish it as private to github.


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Renaming shell lifecycle "active"→"online" (agent-instance `active/dormant/suspended/offline` stays as-is — you only asked for shells).


**Lifecycle = active / offline / torn-down.** A **link-break always closes the broker + shell binary**

**Lifecycle = online / offline / torn-down.** A **link-break always closes the broker + shell binary**


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

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

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

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

**Broadcast policy** (manifest): the shell advertises its availability to all agents on the subnet, only same-node agents, or not at all (agents learn of it only by user instruction). Shells run only on nodes where the shell adapter is installed/advertised.


The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` — the capability set is resolved

The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — the capability set is resolved


**Agent awareness.** Available shells *and* the agent's own shell instances (with online/offline state) are injected into the agent's context (SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — the agent ideally just "knows." A `shell list` command also exists.

-**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.

+**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.

**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; no id needed — owner known from bind).


info.json                type=Shell, owner, adapter_name, status (active|offline)

info.json                type=Shell, owner, adapter_name, status (online|offline)


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

# $SPT_HOME on-disk layout

> Draft design doc (2026-05-30). Implementation detail — neither glossary ([`../CONTEXT.md`](../CONTEXT.md)) nor decision ([`adr/`](./adr/)). Resolves PRD §17.1.

## Principles

1. **Single relocatable root.** One `$SPT_HOME` dir holds everything (default `%LOCALAPPDATA%\spt` / `~/.spt`; override via `$SPT_HOME`). One dir to back up, move, or repackage for a marketplace/handheld. No XDG three-way split.
2. **Endpoint IDs are adapter-agnostic.** `ling` can run under `claude-spt`, then later under `spt-codex` or `spt-pi`. Perches and context key on bare `endpoint_id`; the currently-running `adapter_name` is a *property of the instance* recorded in `info.json`, never a path segment. **At most one instance of an ID per node** — a second attempt (any adapter) is rejected.
3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.
4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.
5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).

## Layout

```
$SPT_HOME/
  identity/                      ── node-local, durable, NEVER git-tracked ──
    node.key                     Ed25519 node identity (per-node; must not sync)
    trust/                       authorized peer pubkeys (trust store)
    subnet.json                  subnet name + TOTP seed (replicated via pairing, not git)
  tracked/                       ── git repo; synced across nodes over P2P ──
    agents/<endpoint_id>/
      meta.json                  endpoint identity metadata: type, ordered adapter history, created
      live-context.md            per-agent live context           (branch a-<endpoint_id>)
    projects/<project_id>/
      <endpoint_id>/
        project-context.md       per-(agent,project) context      (branch p-<project_id>)
  perches/                       ── node-local runtime, NEVER tracked/synced ──
    <endpoint_id>/
      info.json                  identity, type, status, CURRENT adapter_name (+ capability set if Shell)
      spool.db                   SQLite spool (typed + binary payloads)
      status                     active | dormant | suspended    (agent endpoints)
      nested/<child_id>/         Worker / Psyche perches (own info.json, spool)
      shells/<adapter>-<n>/      Shell perches owned by this endpoint (GameRobot-0, GameRobot-1…)
        info.json                type=Shell, owner, adapter_name, status (active|offline)
        spool.db                 command + durable text/file (sensory is REST-only, unspooled)
  history/                       node-local logs (mirrors perch structure)
    <endpoint_id>/
      sessions/<session_id>.jsonl    agent Path-B native logs
      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log
  registry/                      ── ephemeral ── local cache of the subnet registry
  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata
  updates/                       ── ephemeral ── staged binaries pending handoff
  adapters/                      registered adapter manifests / pointers
```

## tracked/ — the context git repo

- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing ("which endpoint is most responsible for this task").
- **Two-tier sync maps onto the structure:** `agents/<id>/live-context.md` (the `a-` view) syncs to **all** instances of the endpoint; `projects/<project_id>/<id>/project-context.md` (the `p-` view) syncs only to instances sharing that project.
- **P2P replaces the *remote*, not the local git.** Modern SPT pushed this repo to a private GitHub remote; spt-core keeps the local git tracking + branch views and moves the data over the built-in P2P layer instead (no `gh`/account/setup). The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — arbitrates concurrent writes from multiple nodes/instances.

## project_id

Derived **from the git remote URL** of the project if it has one (stable + path-independent, so the *same* project at different absolute paths on different machines — `/home/x/proj` vs `C:\Users\x\proj` — resolves to the *same* `project_id`, which is what makes project-context sync match across instances). If the project has no git repo, fall back to the project **folder name**. (Folder-name fallback is not guaranteed path-independent across machines — a known limitation for non-git projects.)

## perches/ — node-local runtime

- Holds the clobber-sensitive, node-specific state: `info.json` (identity, type, current `adapter_name`, status), the SQLite `spool.db`, the `status` marker, and `nested/` Worker/Psyche child perches.
- **Never** part of `tracked/` and never synced — each node owns its own perch runtime. This is the clean alternative to mixing perch files into the context repo and excluding them via gitignore.

## Endpoint metadata & adapter history (`tracked/agents/<id>/meta.json`)

- Records the endpoint's durable identity: type, creation, and an **ordered adapter history** (most-recent first). Synced, so any node knows it.
- Drives the **resume UX**: the latest adapter is the default resume offer, the next-latest is second, … the oldest is second-to-last, and "choose a different adapter" is the final option.
- The *currently-running* `adapter_name` lives in node-local `perches/<id>/info.json`; the *history* is endpoint identity in `tracked/`.

## Shells in the layout

Shells are endpoints but a different shape from agent endpoints:
- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.
- **Not in the subnet registry** — private to the agent↔shell link, not a general messaging surface.
- **No `tracked/` context** — a Shell has no mind; never synced.
- **Logs** in node-local `history/<endpoint_id>/shells/<adapter>-<n>.jsonl`.
- **Adapter/platform-bound, not adapter-agnostic** — capability surface comes from the providing shell adapter; it does not move harnesses.
- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.
- **Cross-node links:** the shell perch lives on the shell's node (nested under the owner id as a label there); commands ride Iroh from the owner's node.

## Migration & backup

- **Back up / migrate:** `identity/` + `tracked/` (+ `history/` if wanted). That's identity, trust, subnet, and the full agent/project mind.
- **Regenerate on the target:** `perches/`, `registry/`, `daemon/`, `updates/` rebuild from the durable set + the live subnet.
- Legacy `claude_skill_owl` migration maps its `psyches/tracked/` git repo into `tracked/` and its node identity/agents into `identity/` + `perches/`.

## Open sub-items

- Exact carry-over of modern SPT's git branch mechanics (worktrees vs branch-checkout) into `tracked/`.
- Whether `history/` ever needs cross-node fetch (today node-local; history-lookup over the network would serve remote reads).

-        info.json                type=Shell, owner, adapter_name, status (active|offline)

+        info.json                type=Shell, owner, adapter_name, status (online|offline)


- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set

- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — **not** the capability set


# $SPT_HOME on-disk layout

> Draft design doc (2026-05-30). Implementation detail — neither glossary ([`../CONTEXT.md`](../CONTEXT.md)) nor decision ([`adr/`](./adr/)). Resolves PRD §17.1.

## Principles

1. **Single relocatable root.** One `$SPT_HOME` dir holds everything (default `%LOCALAPPDATA%\spt` / `~/.spt`; override via `$SPT_HOME`). One dir to back up, move, or repackage for a marketplace/handheld. No XDG three-way split.
2. **Endpoint IDs are adapter-agnostic.** `ling` can run under `claude-spt`, then later under `spt-codex` or `spt-pi`. Perches and context key on bare `endpoint_id`; the currently-running `adapter_name` is a *property of the instance* recorded in `info.json`, never a path segment. **At most one instance of an ID per node** — a second attempt (any adapter) is rejected.
3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.
4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.
5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).

## Layout

```
$SPT_HOME/
  identity/                      ── node-local, durable, NEVER git-tracked ──
    node.key                     Ed25519 node identity (per-node; must not sync)
    trust/                       authorized peer pubkeys (trust store)
    subnet.json                  subnet name + TOTP seed (replicated via pairing, not git)
  tracked/                       ── git repo; synced across nodes over P2P ──
    agents/<endpoint_id>/
      meta.json                  endpoint identity metadata: type, ordered adapter history, created
      live-context.md            per-agent live context           (branch a-<endpoint_id>)
    projects/<project_id>/
      <endpoint_id>/
        project-context.md       per-(agent,project) context      (branch p-<project_id>)
  perches/                       ── node-local runtime, NEVER tracked/synced ──
    <endpoint_id>/
      info.json                  identity, type, status, CURRENT adapter_name (+ capability set if Shell)
      spool.db                   SQLite spool (typed + binary payloads)
      status                     active | dormant | suspended    (agent endpoints)
      nested/<child_id>/         Worker / Psyche perches (own info.json, spool)
      shells/<adapter>-<n>/      Shell perches owned by this endpoint (GameRobot-0, GameRobot-1…)
        info.json                type=Shell, owner, adapter_name, status (online|offline)
        spool.db                 command + durable text/file (sensory is REST-only, unspooled)
  history/                       node-local logs (mirrors perch structure)
    <endpoint_id>/
      sessions/<session_id>.jsonl    agent Path-B native logs
      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log
  registry/                      ── ephemeral ── local cache of the subnet registry
  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata
  updates/                       ── ephemeral ── staged binaries pending handoff
  adapters/                      registered adapter manifests / pointers
```

## tracked/ — the context git repo

- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing ("which endpoint is most responsible for this task").
- **Two-tier sync maps onto the structure:** `agents/<id>/live-context.md` (the `a-` view) syncs to **all** instances of the endpoint; `projects/<project_id>/<id>/project-context.md` (the `p-` view) syncs only to instances sharing that project.
- **P2P replaces the *remote*, not the local git.** Modern SPT pushed this repo to a private GitHub remote; spt-core keeps the local git tracking + branch views and moves the data over the built-in P2P layer instead (no `gh`/account/setup). The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — arbitrates concurrent writes from multiple nodes/instances.

## project_id

Derived **from the git remote URL** of the project if it has one (stable + path-independent, so the *same* project at different absolute paths on different machines — `/home/x/proj` vs `C:\Users\x\proj` — resolves to the *same* `project_id`, which is what makes project-context sync match across instances). If the project has no git repo, fall back to the project **folder name**. (Folder-name fallback is not guaranteed path-independent across machines — a known limitation for non-git projects.)

## perches/ — node-local runtime

- Holds the clobber-sensitive, node-specific state: `info.json` (identity, type, current `adapter_name`, status), the SQLite `spool.db`, the `status` marker, and `nested/` Worker/Psyche child perches.
- **Never** part of `tracked/` and never synced — each node owns its own perch runtime. This is the clean alternative to mixing perch files into the context repo and excluding them via gitignore.

## Endpoint metadata & adapter history (`tracked/agents/<id>/meta.json`)

- Records the endpoint's durable identity: type, creation, and an **ordered adapter history** (most-recent first). Synced, so any node knows it.
- Drives the **resume UX**: the latest adapter is the default resume offer, the next-latest is second, … the oldest is second-to-last, and "choose a different adapter" is the final option.
- The *currently-running* `adapter_name` lives in node-local `perches/<id>/info.json`; the *history* is endpoint identity in `tracked/`.

## Shells in the layout

Shells are endpoints but a different shape from agent endpoints:
- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.
- **Not in the subnet registry** — private to the agent↔shell link, not a general messaging surface.
- **No `tracked/` context** — a Shell has no mind; never synced.
- **Logs** in node-local `history/<endpoint_id>/shells/<adapter>-<n>.jsonl`.
- **Adapter/platform-bound, not adapter-agnostic** — capability surface comes from the providing shell adapter; it does not move harnesses.
- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.
- **Cross-node links:** the shell perch lives on the shell's node (nested under the owner id as a label there); commands ride Iroh from the owner's node.

## Migration & backup

- **Back up / migrate:** `identity/` + `tracked/` (+ `history/` if wanted). That's identity, trust, subnet, and the full agent/project mind.
- **Regenerate on the target:** `perches/`, `registry/`, `daemon/`, `updates/` rebuild from the durable set + the live subnet.
- Legacy `claude_skill_owl` migration maps its `psyches/tracked/` git repo into `tracked/` and its node identity/agents into `identity/` + `perches/`.

## Open sub-items

- Exact carry-over of modern SPT's git branch mechanics (worktrees vs branch-checkout) into `tracked/`.
- Whether `history/` ever needs cross-node fetch (today node-local; history-lookup over the network would serve remote reads).

-- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (active|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.

+- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.


- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary

- **Lifecycle = online / offline / torn-down.** Link-break always closes the binary


# $SPT_HOME on-disk layout

> Draft design doc (2026-05-30). Implementation detail — neither glossary ([`../CONTEXT.md`](../CONTEXT.md)) nor decision ([`adr/`](./adr/)). Resolves PRD §17.1.

## Principles

1. **Single relocatable root.** One `$SPT_HOME` dir holds everything (default `%LOCALAPPDATA%\spt` / `~/.spt`; override via `$SPT_HOME`). One dir to back up, move, or repackage for a marketplace/handheld. No XDG three-way split.
2. **Endpoint IDs are adapter-agnostic.** `ling` can run under `claude-spt`, then later under `spt-codex` or `spt-pi`. Perches and context key on bare `endpoint_id`; the currently-running `adapter_name` is a *property of the instance* recorded in `info.json`, never a path segment. **At most one instance of an ID per node** — a second attempt (any adapter) is rejected.
3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.
4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.
5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).

## Layout

```
$SPT_HOME/
  identity/                      ── node-local, durable, NEVER git-tracked ──
    node.key                     Ed25519 node identity (per-node; must not sync)
    trust/                       authorized peer pubkeys (trust store)
    subnet.json                  subnet name + TOTP seed (replicated via pairing, not git)
  tracked/                       ── git repo; synced across nodes over P2P ──
    agents/<endpoint_id>/
      meta.json                  endpoint identity metadata: type, ordered adapter history, created
      live-context.md            per-agent live context           (branch a-<endpoint_id>)
    projects/<project_id>/
      <endpoint_id>/
        project-context.md       per-(agent,project) context      (branch p-<project_id>)
  perches/                       ── node-local runtime, NEVER tracked/synced ──
    <endpoint_id>/
      info.json                  identity, type, status, CURRENT adapter_name (+ capability set if Shell)
      spool.db                   SQLite spool (typed + binary payloads)
      status                     active | dormant | suspended    (agent endpoints)
      nested/<child_id>/         Worker / Psyche perches (own info.json, spool)
      shells/<adapter>-<n>/      Shell perches owned by this endpoint (GameRobot-0, GameRobot-1…)
        info.json                type=Shell, owner, adapter_name, status (online|offline)
        spool.db                 command + durable text/file (sensory is REST-only, unspooled)
  history/                       node-local logs (mirrors perch structure)
    <endpoint_id>/
      sessions/<session_id>.jsonl    agent Path-B native logs
      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log
  registry/                      ── ephemeral ── local cache of the subnet registry
  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata
  updates/                       ── ephemeral ── staged binaries pending handoff
  adapters/                      registered adapter manifests / pointers
```

## tracked/ — the context git repo

- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing ("which endpoint is most responsible for this task").
- **Two-tier sync maps onto the structure:** `agents/<id>/live-context.md` (the `a-` view) syncs to **all** instances of the endpoint; `projects/<project_id>/<id>/project-context.md` (the `p-` view) syncs only to instances sharing that project.
- **P2P replaces the *remote*, not the local git.** Modern SPT pushed this repo to a private GitHub remote; spt-core keeps the local git tracking + branch views and moves the data over the built-in P2P layer instead (no `gh`/account/setup). The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — arbitrates concurrent writes from multiple nodes/instances.

## project_id

Derived **from the git remote URL** of the project if it has one (stable + path-independent, so the *same* project at different absolute paths on different machines — `/home/x/proj` vs `C:\Users\x\proj` — resolves to the *same* `project_id`, which is what makes project-context sync match across instances). If the project has no git repo, fall back to the project **folder name**. (Folder-name fallback is not guaranteed path-independent across machines — a known limitation for non-git projects.)

## perches/ — node-local runtime

- Holds the clobber-sensitive, node-specific state: `info.json` (identity, type, current `adapter_name`, status), the SQLite `spool.db`, the `status` marker, and `nested/` Worker/Psyche child perches.
- **Never** part of `tracked/` and never synced — each node owns its own perch runtime. This is the clean alternative to mixing perch files into the context repo and excluding them via gitignore.

## Endpoint metadata & adapter history (`tracked/agents/<id>/meta.json`)

- Records the endpoint's durable identity: type, creation, and an **ordered adapter history** (most-recent first). Synced, so any node knows it.
- Drives the **resume UX**: the latest adapter is the default resume offer, the next-latest is second, … the oldest is second-to-last, and "choose a different adapter" is the final option.
- The *currently-running* `adapter_name` lives in node-local `perches/<id>/info.json`; the *history* is endpoint identity in `tracked/`.

## Shells in the layout

Shells are endpoints but a different shape from agent endpoints:
- **Nested perch under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one agent owns many shells. `info.json` carries `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — **not** the capability set (that lives in the shell adapter manifest, resolved by `adapter_name`). `spool.db` holds the durable command + text/file channels; the sensory channel is REST-only and never spooled.
- **Not in the subnet registry** — private to the agent↔shell link, not a general messaging surface.
- **No `tracked/` context** — a Shell has no mind; never synced.
- **Logs** in node-local `history/<endpoint_id>/shells/<adapter>-<n>.jsonl`.
- **Adapter/platform-bound, not adapter-agnostic** — capability surface comes from the providing shell adapter; it does not move harnesses.
- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.
- **Cross-node links:** the shell perch lives on the shell's node (nested under the owner id as a label there); commands ride Iroh from the owner's node.

## Migration & backup

- **Back up / migrate:** `identity/` + `tracked/` (+ `history/` if wanted). That's identity, trust, subnet, and the full agent/project mind.
- **Regenerate on the target:** `perches/`, `registry/`, `daemon/`, `updates/` rebuild from the durable set + the live subnet.
- Legacy `claude_skill_owl` migration maps its `psyches/tracked/` git repo into `tracked/` and its node identity/agents into `identity/` + `perches/`.

## Open sub-items

- Exact carry-over of modern SPT's git branch mechanics (worktrees vs branch-checkout) into `tracked/`.
- Whether `history/` ever needs cross-node fetch (today node-local; history-lookup over the network would serve remote reads).

-- **Lifecycle = active / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.

+- **Lifecycle = online / offline / torn-down.** Link-break always closes the binary (optional pre-close instruction + termination timeout). **Ephemeral** (manifest property) ⇒ perch torn down *and removed from the agent's shell history*; **persistent** ⇒ perch kept offline for relink.


Nomenclature fixed (shells: `online/offline/torn-down`).

On the repo — quick catch before I publish, since this is outward-facing and hard to walk back: you said **`spt-node`**, but this directory and the entire session's work is **`spt-core`**, and we explicitly *retired* `spt-node` as a name in ADR-0002. Want to confirm which name the GitHub repo should carry.


AskUserQuestion

Question: What should the private GitHub repo be named?

Choices:
Matches the project, this directory, and all docs (CONTEXT/PRD/ADRs). spt-node was retired as a name in ADR-0002. Most likely intended.
As literally typed. Contradicts the established project name — only choose if you genuinely want the repo named spt-node despite the directory being spt-core.


Your questions have been answered: "What should the private GitHub repo be named?"="spt-core". You can now continue with these answers in mind.

Question: What should the private GitHub repo be named?

Choices:
Matches the project, this directory, and all docs (CONTEXT/PRD/ADRs). spt-node was retired as a name in ADR-0002. Most likely intended.
As literally typed. Contradicts the established project name — only choose if you genuinely want the repo named spt-node despite the directory being spt-core.


Confirmed `spt-core`. Checking git/gh state and what's in the directory before committing.


Bash

git --version && gh --version | head -1 && echo "--- auth ---" && gh auth status 2>&1 | head -5

Check git, gh, auth status


git version 2.43.0.windows.1
gh version 2.89.0 (2026-03-26)
--- auth ---
github.com
  ✓ Logged in to github.com account SaberMage (keyring)
  - Active account: true
  - Git operations protocol: https
  - Token: gho_************************************


ls -la && echo "--- .claude ---" && ls -la .claude 2>/dev/null && echo "--- docs ---" && ls docs && echo "--- adr ---" && ls docs/adr

List working dir contents incl .claude and docs


total 96
drwxr-xr-x 1 decid 197609     0 May 31 03:12 .
drwxr-xr-x 1 decid 197609     0 May 27 23:14 ..
drwxr-xr-x 1 decid 197609     0 May 31 02:44 .claude
-rw-r--r-- 1 decid 197609 60917 May 31 03:12 CONTEXT.md
-rw-r--r-- 1 decid 197609 19489 May 31 03:08 PRD.md
drwxr-xr-x 1 decid 197609     0 May 31 03:12 docs
--- .claude ---
total 5
drwxr-xr-x 1 decid 197609   0 May 31 02:44 .
drwxr-xr-x 1 decid 197609   0 May 31 03:12 ..
-rw-r--r-- 1 decid 197609 135 May 31 02:44 LIVE_AGENT_IDS.json
--- docs ---
DEFERRED.md
KNOWN-HAZARDS.md
MANIFEST.md
STORAGE.md
adr
--- adr ---
0001-clean-room-fork-from-claude-skill-owl.md
0002-bake-networking-into-core-collapse-spt-node.md
0003-multi-instance-node-anchored-identity-model.md
0004-single-daemon-broker-brain-split-and-self-update.md
0005-totp-seeded-spake2-node-pairing.md


gh authed as `SaberMage`, git ready. The only non-deliverable is `.claude/LIVE_AGENT_IDS.json` (harness-local agent state) — gitignoring it. Writing `.gitignore`, then init + commit + publish.


Write

# Rust build output
/target
**/*.rs.bk

# Harness / SPT local state (machine-specific)
.claude/

# OS junk
Thumbs.db
.DS_Store


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

# Rust build output
/target
**/*.rs.bk

# Harness / SPT local state (machine-specific)
.claude/

# OS junk
Thumbs.db
.DS_Store


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && git init -b main && git add -A && git status --short

Init repo, stage files, show status


Initialized empty Git repository in C:/Users/decid/Documents/projects/spt-core/.git/
warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'PRD.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/KNOWN-HAZARDS.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/MANIFEST.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/STORAGE.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0001-clean-room-fork-from-claude-skill-owl.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0002-bake-networking-into-core-collapse-spt-node.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0003-multi-instance-node-anchored-identity-model.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0005-totp-seeded-spake2-node-pairing.md', LF will be replaced by CRLF the next time Git touches it
A  .gitignore
A  CONTEXT.md
A  PRD.md
A  docs/DEFERRED.md
A  docs/KNOWN-HAZARDS.md
A  docs/MANIFEST.md
A  docs/STORAGE.md
A  docs/adr/0001-clean-room-fork-from-claude-skill-owl.md
A  docs/adr/0002-bake-networking-into-core-collapse-spt-node.md
A  docs/adr/0003-multi-instance-node-anchored-identity-model.md
A  docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md
A  docs/adr/0005-totp-seeded-spake2-node-pairing.md


Staged correctly (`.claude/` excluded). Committing.


cd "C:/Users/decid/Documents/projects/spt-core" && git commit -q -F - <<'EOF'
docs: initial spt-core planning artifacts

Foundational planning for spt-core — the harness-independent rebuild of
claude_skill_owl. Captures the full decision tree from a grilling session.

- CONTEXT.md: glossary + model (deliverable shape, runtime/harness contract,
  endpoints, Shells, terminal wrapper, networking, self-update, instances,
  pairing, install, workspace/versioning)
- PRD.md: product requirements, v1 scope, success criteria, open questions
- docs/adr/0001-0005: clean-room fork; bake networking / collapse spt-node;
  multi-instance node-anchored model; broker/brain daemon + self-update;
  TOTP-seeded SPAKE2 pairing
- docs/KNOWN-HAZARDS.md: invariants harvested from the sister project
- docs/DEFERRED.md: parked-but-committed features
- docs/STORAGE.md: $SPT_HOME on-disk layout
- docs/MANIFEST.md: runtime manifest schema + worked claude-spt example

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -1

Create initial commit


9515e2e docs: initial spt-core planning artifacts


Committed. Publishing as private to GitHub.


cd "C:/Users/decid/Documents/projects/spt-core" && gh repo create spt-core --private --source=. --remote=origin --push 2>&1 && echo "--- remote ---" && git remote -v

Create private GitHub repo and push


https://github.com/SaberMage/spt-core
branch 'main' set up to track 'origin/main'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      HEAD -> main
--- remote ---
origin	https://github.com/SaberMage/spt-core.git (fetch)
origin	https://github.com/SaberMage/spt-core.git (push)


Published. **https://github.com/SaberMage/spt-core** — private, `main` pushed, `origin` tracking set.

- Initial commit `9515e2e` — 12 files: CONTEXT.md, PRD.md, `docs/` (KNOWN-HAZARDS, DEFERRED, STORAGE, MANIFEST + 5 ADRs), `.gitignore`.
- `.claude/LIVE_AGENT_IDS.json` excluded (machine-local harness state).
- Shell lifecycle rename (`online/offline/torn-down`) is in the working tree but **not yet committed** — I made those edits after... actually no, I made them before `git add -A`, so they're in the commit. Confirmed: the rename edits were saved before staging, so they're included.

Back to the open items whenever you want: **17.3** (Psyche live/project authoring directives), **17.4** (dormancy default + suspend surface), **17.5** (consent/security-gate UX). Which next?


i want the following hypothetical to be possible: the `GameRobot` Shell for `ling` has a corresponding avatar. that avatar looks "shut down" if the Shell is offline. Shells automatically go offline when their owner's endpoint goes offline. but i want to be able to "wake" the agent-->their endpoint-->their shell by interacting with the avatar in a certain way. ideas? here are mine:
- give Shell adapter manifests the option to be "persistent." the Shell is automatically brought online alongside its owner's endpoint
- allow the manifest to define a "daemon command template."
  - this is a long-running service
  - spt-core then ensures the shell daemon is running whenever the Shell itself is offline
  - one process per shell instance. so if an agent has 2 instances of GameRobot, there will be at most 2 daemons running
  - the daemon can do exactly one thing: terminate with a certain opcode-->triggers spt-core to revive the shell (and the agent endpoint, if it's offline)

so this way, the daemon can be listening on the shell developer's platform for a wakeup event, and revive the shell and agent to bring them back online.


Love this — it's a clean inversion (the environment waking the agent, the mirror of PresenceChannel reaching the user). Endorsing both your ideas with refinements:

**Refinements:**

1. **Naming** — avoid "daemon" (collides with `spt-daemon`). Call it the **wake-watcher**. The `persistent` option stays as named.

2. **The two options are complementary, not redundant:**
   - **`persistent`** (manifest bool) — shell auto-online *whenever its owner endpoint is online*. Handles "owner already up."
   - **`wake_command`** (manifest template) — the long-running wake-watcher spt-core runs *while the shell is offline*. Handles "owner is **down**, wake it from outside." This is the one that powers your avatar-interaction.
   They compose: persistent covers owner-up; wake-watcher covers owner-down.

3. **Lifecycle = mutually-exclusive processes.** Shell **online** ⇒ shell binary runs, no watcher. Shell **offline** (+ `wake_command` defined) ⇒ wake-watcher runs, no shell binary. spt-core flips between them. The watcher runs on the **shell's node** (where the platform wake-event originates).

4. **Exit-code discrimination** (this makes crash-recovery fall out cleanly): the watcher exiting with the **wake opcode** → spt-core revives (owner endpoint, then shell online). Any *other* exit (crash) → spt-core **respawns the watcher**. So "ensure the watcher runs while offline" and "wake on opcode" are the same supervision loop, distinguished by exit code. One watcher per offline shell instance (your 2-GameRobot → 2-watcher cap holds).

**One scope boundary to flag** — waking the *owner endpoint* depends on where it is:
- Owner has a **dormant/suspended instance on the shell's node** (or another *reachable* node) → wake-watcher wakes it. **v1.**
- Owner's only instance is on an **offline (unreachable) node** → can't revive a powered-off machine; nothing to do until that node returns.
- Owner has no instance on the shell's node and you want to *spawn* one there to wake → that's **instantiate-anywhere** (deferred, consent-gated).

So **v1 = wake-watcher revives a reachable dormant/suspended owner + brings the shell online; cross-node *spawn*-to-wake rides the deferred gate.** Agree with that boundary?

Recommend capturing: `persistent` + `wake_command` manifest fields, the online/offline process mutual-exclusion, exit-opcode supervision, watcher-on-shell-node, and the v1/deferred wake scope. Confirm the boundary and I'll write it in.


i want the following hypothetical to be possible: the `GameRobot` Shell for `ling` has a corresponding avatar. that avatar looks "shut down" if the Shell is offline. Shells automatically go offline wh…


1. agreed. the process has exactly one goal: fire a wake. name makes sense. 2. agree. 3. agree. 4. agree. watcher respawn should have exponential backoff (and eventual give-up until next shell activity) in case the watcher has a crash bug. -"dormant" instance means the endpoint is still running, so the watcher-exit-response wouldn't need to do anything to the endpoint. "suspended", yes, endpoint needs revived. -offline node-only no-op: agree, UNLESS that endpoint has a special settings flag which enables shells to fresh-spawn it "on any available node". boundary ok. -missing scenario: owner has no instance on the shell's node, but has one on another subnet node that should revive (or is active, so just attach) to wake-->ok, do it. --- in the same vein as shell-based wake, there should be 1. agent-facing command to shut down their own endpoint 2. a shell adapter manifest option to mark shutdown intent i.e. `can-shutdown: true`. enables the shell to bypass agent comms and directly shut down the linked endpoint. what is the channel for this from-shell shutdown signal?


Answering your question first, then the refinements.

**The from-shell shutdown channel** — it's a **machinery `api` command**, not any of the three content channels (command/sensory/text are *content*; shutdown is a *control signal to spt-core about the owner*). So:

- **`api owner-shutdown`** — fired by the shell binary, routed shell-binary→broker→spt-core (local-link auth), **gated by `can_shutdown = true`** in the shell adapter manifest. It bypasses agent comms entirely — doesn't touch the agent's perch/spool; it's a direct lifecycle op spt-core performs on the owner. Symmetric with wake: wake = watcher exit-opcode (revives owner); shutdown = `api owner-shutdown` (suspends owner). Both shell-driven owner-lifecycle control, each gated by its manifest flag (`wake_command` / `can_shutdown`).

So the shell machinery `api` set is now: `api bind`, `api poll`, `api emit`, `api owner-shutdown`.

**Agent-facing self-shutdown:** `spt shutdown` — an agent takes *its own* endpoint down (graceful: fires the transition echo-commune, → **suspended**, wakeable). Cascade: endpoint suspended ⇒ its persistent shells go offline ⇒ their wake-watchers start. The full sleep/wake cycle, driven from either end (agent self-shutdown, or shell `api owner-shutdown`, or the platform's avatar interaction → wake-watcher).

**Wake refinements — all agreed, with your sharpenings:**
- **Watcher respawn = exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. ✓
- **Wake-response keys on owner state:** **dormant** (still running) → do nothing to the endpoint, just bring the shell online; **suspended** → revive owner + online shell; **offline node** → no-op.
- **Cross-node wake of an *existing* instance = v1:** if the owner has no instance on the shell's node but has one on another reachable subnet node, wake/attach/revive *that* one (dormant→attach, suspended→revive). Only a **fresh-spawn where no instance exists** stays gated.
- **Spawn-anywhere flag = pre-consent:** a per-endpoint settings flag lets shells **fresh-spawn the owner on any available node** to wake it when its home node is offline. The flag *is* the consent (pre-authorizes the otherwise-deferred instantiate-anywhere for this wake path).

So the v1 wake-resolution: find any reachable instance of the owner → act per its state; none reachable → no-op, *unless* the spawn-anywhere flag → fresh-spawn on any available node.

Capturing: `persistent` + `wake_command` + `can_shutdown` (shell manifest), the `spt shutdown` agent command + `api owner-shutdown`, the spawn-anywhere endpoint flag, online/offline process mutual-exclusion + backoff supervision, and the state-keyed wake resolution. Writing it in.


Edit

**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; no id needed — owner known from bind).

**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content channels).

#### Shell sleep/wake (offline ↔ online)

A shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:

- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers "owner already up."
- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs *while the shell is offline*. Its sole job is to fire a wake. It runs on the **shell's node** (where the platform wake-event originates — e.g. interacting with a "shut-down" avatar). Covers "owner is down, wake it from outside."

**Online/offline are mutually-exclusive processes:** online ⇒ the shell binary runs (no watcher); offline ⇒ the wake-watcher runs (no shell binary). spt-core flips between them.

**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.

**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
- owner **suspended** → revive the owner, then online the shell;
- owner **active** elsewhere → attach/online the shell;
- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).

The same `spt shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.


**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content channels).

#### Shell sleep/wake (offline ↔ online)

A shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:

- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers "owner already up."
- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs *while the shell is offline*. Its sole job is to fire a wake. It runs on the **shell's node** (where the platform wake-event originates — e.g. interacting with a "shut-down" avatar). Covers "owner is down, wake it from outside."

**Online/offline are mutually-exclusive processes:** online ⇒ the shell binary runs (no watcher); offline ⇒ the wake-watcher runs (no shell binary). spt-core flips between them.

**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.

**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
- owner **suspended** → revive the owner, then online the shell;
- owner **active** elsewhere → attach/online the shell;
- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).

The same `spt shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.

**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline) — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.

-**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; no id needed — owner known from bind).

+**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content channels).

+#### Shell sleep/wake (offline ↔ online)

+A shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:

+- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers "owner already up."

+- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs *while the shell is offline*. Its sole job is to fire a wake. It runs on the **shell's node** (where the platform wake-event originates — e.g. interacting with a "shut-down" avatar). Covers "owner is down, wake it from outside."

+**Online/offline are mutually-exclusive processes:** online ⇒ the shell binary runs (no watcher); offline ⇒ the wake-watcher runs (no shell binary). spt-core flips between them.

+**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.

+**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —

+- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;

+- owner **suspended** → revive the owner, then online the shell;

+- owner **active** elsewhere → attach/online the shell;

+- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).

+The same `spt shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.

The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface.


1. agreed. the process has exactly one goal: fire a wake. name makes sense. 2. agree. 3. agree. 4. agree. watcher respawn should have exponential backoff (and eventual give-up until next shell activit…


[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close
persistent = true                  # auto-online whenever the owner endpoint is online
wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly


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

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close
persistent = true                  # auto-online whenever the owner endpoint is online
wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly

# Runtime manifest schema

> Draft design doc (2026-05-31). Resolves PRD §17.2. Implementation detail — see [`../CONTEXT.md`](../CONTEXT.md) for the model. Schema grounded in the sister project's actual functional surface.

## Principle

**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.

## What is NOT in the manifest (spt-core-owned)

- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.
- **Spool / registry / perch `info.json` / daemon-state schemas** — core.
- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. Adapters pass spt-core's output through unchanged.
- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.
- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.
- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).

## Manifest sections

### `[adapter]` — header
```toml
[adapter]
name = "claude-spt"
version = "2.0.0"
min_spt_core_version = "1.0.0"     # readable before any update; compat gate
hostable_types = ["LiveAgent", "ReadyAgent", "Psyche", "Worker"]
```

### `[hooks.<event>]` — inbound hook table
Per harness event: the `api` command it fires, the stdin fields it maps in, and its **output capability** — `can_inject` (whether this hook can surface context to the agent). When `can_inject = false`, spt-core falls back to a sentinel + relay/poll path instead of expecting injection. This is the load-bearing harness-varying fact (CC's Stop hook can't inject).

```toml
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "source", "agent_type"]
can_inject = true                  # via additionalContext

[hooks.Stop]
fires = "api state idle"
reads = []
can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback
```

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

```toml
[session.psyche_init]
command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
cwd = "{psyche_dir}"
env_remove = ["OWL_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
detach = true
keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
```
(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

### `[env]` — env-var table
Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.

```toml
[env.OWL_SESSION_ID]
direction = "inject"
value = "{session_id}"
channel = "CLAUDE_ENV_FILE"        # harness-hosted only; spt-hosted inherits from broker
```

### `[history]` — transcript access
Three strategies; pick one. Pull-based (spt-core asks) or push-based (adapter sends).
```toml
[history]
strategy = "fetcher"               # fetcher | locate_normalize | native
fetcher = "claude-spt-history --session {session_id}"   # adapter binary emits normalized history
# locate_normalize: locate_template + normalize_command
# native: adapter pushes via `api history-log`; spt-core stores (also used for Shells)
```

### `[inject]` — inject-input methods
Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.
```toml
[inject]
activity = ["hook"]                # non-disruptive during work
idle     = ["pty", "hook"]
```

### `[session]` watched dirs (file-drop)
```toml
[session]
commune_dir = ".claude"            # watched for <id>-commune.md (filename fixed by spt-core)
signoff_dir = ".claude"            # watched for <id>-signoff.md
```

### `[identity]`
```toml
[identity]
session_id_source = "post_spawn"   # post_spawn | uuid_inject
parent_ancestor_name = "claude"    # process-tree anchor when session_id absent
```

### `[update]` — adapter self-update
```toml
[update]
avenue = "delegated"               # delegated | file_pull
command = "claude plugin update spt"          # delegated
# file_pull: repo + path_regex
version_check = true               # verify spt-core satisfies min_spt_core_version before/after
```

---

## Shell adapters (`kind = "shell"`)

A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.

```toml
[adapter]
name = "GameRobot"
kind = "shell"                     # vs "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close

[shell.capabilities]               # the command vocabulary (agent→shell) — the toolset
move      = { args = ["direction", "distance"] }
gesture   = { args = ["name"] }
alert     = { args = ["symbol"] }
screenshot = {}

[shell.sensory]                    # the sensory vocabulary (shell→agent, REST-only)
types = ["image", "sound", "event"]   # "event" = arbitrary descriptive payloads
```

The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.

## Worked example: `claude-spt` (today's behavior, expressed)

Maps the sister project's actual surface onto the schema.

| Concern | Today (modern SPT) | Manifest expression |
|---|---|---|
| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |
| Env | `$OWL`/`$LIVE`/`OWL_SESSION_ID` via `CLAUDE_ENV_FILE` | `[env]`; PATH removes most; channel only harness-hosted |
| Identity | `OWL_SESSION_ID` + `claude.exe` ancestor walk | `[identity]` |
| Update | (today: cplugs) | `[update] avenue="delegated" command="claude plugin update"` |
| period / echo-window / route-guard / 24h refresh | hardcoded/flags | spt-core **global knobs** (per-endpoint override), not manifest |

## Open sub-items
- Exact substitution-key catalog (which keys spt-core guarantees to fill per role).
- TOML vs YAML final choice; schema-validation + helpful errors for adapter authors.
- The fixed event-block vocabulary's final tag names (`<SPT_EVENT>` etc.).
- Capability-declaration shape beyond `hostable_types` (per-type operation advertisement).

+persistent = true                  # auto-online whenever the owner endpoint is online

+wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive

+can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly


The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.

The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.

**Sleep/wake:** when offline (+ `wake_command` set), spt-core runs the wake-watcher (mutually exclusive with the shell binary); `exit(wake-opcode)` → wake resolution; crash-exit → respawn with exponential backoff + give-up. `persistent` auto-onlines the shell with its owner. `can_shutdown` authorizes `api owner-shutdown`. Cross-node *fresh-spawn* to wake an owner requires the owner's `shell_wake_spawn_anywhere` settings flag (pre-consent).