in: 1. agree 2. need elaboration, i.e. how this would look for CC 3. "modle+fallback, flags, tool allowlist, [...] dir grants" is all baked into the command template. not explicit definitions. "fallback-binary chain" is kinda more of its own feature. under spt-core, ccs would become its own adapter separate from straight claude code/`spt-plugin`. so this feature is more like: other-adapter-fallback for psyche and echo commune invocations (in case of rate limiting under one adapter). defined per-endpoint? or as a node-wide setting? i'm leaning "node-wide setting." agree with the rest. 4. couldn't "injection" be done by way of just setting the env vars in the parent (broker) env, which would naturally pass into the harness? agree with the rest. 5. agree. iirc we also scoped a potential "history fetcher command template", for adapters that want to pass history into spt-core using their own binary/code? 6. agree with inject-input methods. block vocabulary like not configurable, will be documented for devs to reference. should mostly be i.e. <SPT_EVENT> (or whatever was the common event tag under modern spt). 7. filename patterns likely not configurable. statically `<endpoint_id>-commune.md` or `-signoff.md`. not sure what write mechanism is for? 8. agree 9. plugin-cache layout is none of SPT's concern. if the adapter accommodates a CC plugin, it should use the `claude plugin update` command. maybe adapter-update section also needs directives for version-parity-check pre-update? --- out: agree with all. config knobs are global settings. maybe could be overridden per-endpoint for endpoints with unique needs?


<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.


All refinements absorbed — they simplify the schema a lot. Key shifts: command templates bake in everything (no separate model/tools/flags fields — "SPT is not a harness"), cross-adapter fallback becomes a node-wide setting (since `ccs` is now its own adapter), env via broker-inheritance for spt-hosted, history gains a fetcher-command variant, block vocab + filedrop filenames are fixed constants, plugin-cache is out entirely.

Writing `docs/MANIFEST.md` with the refined schema + worked `claude-spt` example.


Write

# 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
```

---

## 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).


in: 1. agree 2. need elaboration, i.e. how this would look for CC 3. "modle+fallback, flags, tool allowlist, [...] dir grants" is all baked into the command template. not explicit definitions. "fallba…


Design spt-core binary and messaging library architecture


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

# 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
```

---

## 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).


Capturing the genuinely new model-level facts in CONTEXT (schema detail stays in MANIFEST.md).


Edit

There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model.

There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:

- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.
- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.
- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.
- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.


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)

There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:

- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.
- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.
- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.
- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.

**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.

-There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model.

+There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:

+- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.

+- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.

+- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.

+- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.

+- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.

+- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.

All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.


Updating PRD — §17.2 resolved.


- Manifest schema concretes; capability-declaration shape.

- ~~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.


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)

- ~~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; capability-declaration shape.

+- ~~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.


- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.

- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.
- [`docs/MANIFEST.md`](./docs/MANIFEST.md) — runtime manifest schema + worked `claude-spt` example.


- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.
- [`docs/MANIFEST.md`](./docs/MANIFEST.md) — runtime manifest schema + worked `claude-spt` example.

- [`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/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`.


§17.2 resolved → `docs/MANIFEST.md`. The schema came out much leaner than the extraction suggested, because most of the CC-coupling turned out to be **spt-core-owned, not adapter-config** — opaque command templates absorb all the model/tool/flag detail, sentinels/spool/event-vocab are core constants, and config knobs + cross-adapter fallback are spt-core settings. The worked `claude-spt` table shows today's exact behavior (7 hooks, sonnet/haiku invocations, `.claude/` file-drop, transcript path) expressed in it.

A couple of things that genuinely changed the model (now in CONTEXT): **`ccs` becomes its own adapter** and cross-adapter fallback is a node-wide setting; env injection is broker-inherited for spt-hosted / channel-declared for harness-hosted; config knobs are global with per-endpoint override.

§17 status: **17.1 ✓** (STORAGE) · **17.2 ✓** (MANIFEST, minus capability-declaration shape) · remaining:
- **17.3** — Psyche live/project authoring directives (content/prompt design — the thing that makes the two-tier sync actually hold)
- **17.4** — dormancy default policy + suspend command surface
- **17.5** — consent/security-gate UX (instantiate-anywhere + remote-exec)
- **17.6** — remote-control: formal Shell vs bare session-surface

17.3 is different in kind — it's prompt/authoring design, not architecture, and it's a *known live problem* (Psyche over-stuffing live context). Want to tackle that, or take 17.6 (it's quick and ties off the Shell/instance model cleanly)?


this is a good time to talk about Shells. i feel like we need to clarify what Shells are in the context of SPT's systems.

## conceptually, here's what a shell is:
- a surface for one agent to control
  - enables the agent to be embodied within new systems and environments
- a two-way channel that can be configured to transfer text and file payloads both ways
  - a channel for **one-way from-agent commands** for Shell manipulation and Shell-->environment interaction
  - a channel for **one-way to-agent sensory payloads** for images, sounds (maybe), and arbitrary descriptive "sensory" payloads, i.e. "movement instruction completed", "movement obstructed", "bumped", temperature change, energy level, etc.
- a binary that runs on any of the user's own nodes
- can be spun up by an agent endpoint-->both linked. only that endpoint id can control that shell
- optionally ephemeral. when link is broken: can tear down, or can transition to an offline state

## is a shell just a special type of adapter?
adapters:
- have manifests
- can spawn different endpoint types, i.e. ReadyAgent and LiveAgent
- help define harness behaviors and agent-related data surfaces
- do NOT have an agent control surface

shell adapters:
- should also have manifests?
- maybe "Shell" is the endpoint type, and i.e. "GameRobot" is the shell adapter name?
- broadcast their existence. options:
  - to all agents on the subnet
  - only to agents on the same node
  - not at all (agents only learn about it through the user's instruction)
- can only run on nodes where it advertises from/is installed
- choose how they receive commands from SPT. options:
  - HTTP REST
  - stdin-via-broker
  - child relay (basically `spt api poll`(?))

shells are only ever invoked through SPT, and *usually*/typically by an agent. they may have harness-hosted-style topology if they choose to communicate via child relay, but their binary doesn't get booted by anything but SPT.

all this said, what are shell-relevant SPT commands, and how do they integrate with the spt-core file layout?


This clarifies a lot — and yes, the cleanest framing is that a **Shell adapter is a second *kind* of adapter**. Let me reconcile, then answer your two questions.

## Reconciled model

**Two kinds of adapter, both manifested, both in `adapters/`:**
- **harness adapter** (`claude-spt`, `spt-codex`) — `kind = "harness"`, hosts *agent* endpoints (LiveAgent/ReadyAgent/…). No control surface.
- **shell adapter** (`GameRobot`, `phone-notify`) — `kind = "shell"`, provides a *Shell* endpoint. So **`Shell` is the endpoint type; the shell adapter name (`GameRobot`) is the provider**, and its manifest defines the capability surface.

**A Shell endpoint, refined:**
- **Two one-way channels** over the typed/binary messaging substrate (the seam we already have): **command channel** (agent→shell: the manifest's capability toolset, drives shell + environment) and **sensory channel** (shell→agent: images/sounds/descriptive events — "movement completed", "obstructed", "bumped", temp, energy). The manifest declares *both* vocabularies.
- **Owner-linked, exclusive:** spawned by an agent endpoint; both linked; **only that `endpoint_id` may control it** (`owner` recorded on the perch).
- **Always SPT-launched** (broker spawns the binary; never user-launched) → broker is the launcher → direct channel, env-inheritance, `api bind` (no seed-file). Command-*delivery* method is configurable (your three: HTTP REST / stdin-via-broker / child-relay-`api poll`) — the same inject-input modes agents use.
- **Lifecycle = active / offline / torn-down** (not dormant/suspended — those are agent-instance concepts). On link-break: ephemeral → teardown, or persist → offline (re-linkable).
- **Broadcast policy** (manifest): all-subnet / same-node / none (user-instruction-only).
- **No `tracked/` context** (no mind), logs in `history/` — as established.

## Q1 — Shell-relevant SPT commands

**Agent/user surface** (unprefixed — the controlling agent invokes):
- `spt shell spawn <shell_adapter> [--id <id>] [--ephemeral]` — boot + link (owner = caller).
- `spt shell list` — discover shells (filtered by broadcast policy).
- `spt shell relink <id>` / `spt shell teardown <id>` — owner-only.
- Commands *to* the shell ride ordinary `send` with a typed operation-command payload — with `spt shell cmd <id> <op…>` as sugar.

**Machinery `api` surface** (the broker-spawned shell binary):
- `api bind` — reused (type=Shell, owner, capability set; skeleton→live).
- `api poll` — reused, for the child-relay delivery mode.
- `api emit <id> <sensory-payload>` — **new**: the shell pushes a sensory payload back to its owner endpoint.

## Q2 — File-layout integration

- **`adapters/<GameRobot>/`** — the shell adapter manifest (`kind="shell"`; capability toolset, sensory types, broadcast scope, command-receipt method, spawn command).
- **`perches/<shell_id>/`** — node-local: `info.json` (`type=Shell`, `owner=<endpoint_id>`, `adapter_name=GameRobot`, capability set, status active/offline), `spool.db` (inbound commands + outbound sensory), `status`.
- **`history/`** — Shell logs (node-local).
- **No `tracked/`** entry. Shells sit in the same `perches/` ID namespace as agents (`info.json.type` discriminates; `owner` records the controller).

Two small design picks I'd flag:
1. **Sensory via `api emit` vs plain `send`-to-owner.** I lean `api emit` (the shell binary is machinery, and emit can attach the sensory-type tag the manifest declared) — agree?
2. **`shell_id` namespacing** — owner-derived for legibility (`<owner>-<adapter>-<n>`) vs a free id. Lean owner-derived. Agree?

Confirm those two + the overall reconciliation, and I'll write it into CONTEXT (Shell section), MANIFEST.md (the `kind="shell"` adapter fields), and STORAGE.md (shell perch fields).


-for shell endpoint: don't forget the 2-way text+files channel (which exists for harness adapters too, but in shells it's just a forwarder) -before i forget, a req for the files channel: it must enable the agent and the SPT binary (for GUI) to query a file transfer's progress at any time during the transfer. -i feel like the endpoint-to-shell linkage should be cryptographically secured. so messages between the SPT binary and the shell's broker/relay should be encrypted + require some sort of handshake to complete the transfer. agree? any additional thoughts about that point, either relating to shells or to the infra of SPT as a whole? -link-break event always closes the broker+shell binary, optionally with a pre-close instruction (to binary) + termination timeout. --- Q1: -`shell spawn`--ephemeral is an adapter manifest definition, never chosen by agent. -side note: shell perches should be nested under the agent's perch, i.e. `<endpoint_id>/shells/GameRobot-0/`. shell perches are *potentially* instantiable, so relationship one-agent:many-shell --> GameRobot-1, GameRobot-2 etc. shell perches are only addressed/communicated to between the agent and shell binary, so no messaging surface under traditional SPT. an agent and its shell should be able to link across nodes. -`shell list`--agreed, though ideally agent just "knows" the available shells (through i.e. CC's SessionStart additionalContext or `$LIVE start` equivalent). ALSO, this (and the hidden context versions) should show the agent's own shell instances and their online/offline states. -`shell relink/teardown` agree. -`shell cmd` agree. --- `api bind` agree. -`api poll` agree. -`api emit` shouldn't need <id>, as the shell defined its owner during `api bind`. unless you mean for `id` to represent a payload type id? --- what is the shell perch for? it's for the agent to send payloads to their shell. payloads from shell-to-agent go straight to the agent's perch. should the agent have per-shell spools for from-shell text+file payloads? sensory payloads should probably skip spools altogether (REST only), since their relevance is very temporal/ephemeral. --- agree with shell adapter manifests location. -shell perch location nested, as noted above. not sure if info.json needs the capability set? feel free to explain that. -how is history (logs) organized? -agreed no tracked entry. --- design picks: 1. agree on `api emit`. 2. noted above. folders nested under owner perch, named only after the shell adapter.


-for shell endpoint: don't forget the 2-way text+files channel (which exists for harness adapters too, but in shells it's just a forwarder) -before i forget, a req for the files channel: it must enabl…


Big refinements — all absorbed. Reconciling the channels, answering your three questions, then the security point.

## Channels (final)
A Shell endpoint has **three** channels over the typed/binary substrate:
- **command** — agent→shell, **durable** (spooled on the shell perch). The manifest's capability toolset.
- **text+file** — 2-way, **durable** (spooled). For shells it's a *forwarder*; harness adapters have it too. **Files are progress-queryable** (see below).
- **sensory** — shell→agent, **REST-only, never spooled** (relevance is fleeting; dropped if the agent isn't live). Goes straight to the agent's active session.

So: agent→shell (command + text/file) and durable shell→agent text/file live on the nested shell perch; **sensory bypasses spools entirely**. That reconciles "shell→agent goes straight to the agent" (sensory, live) with "per-shell spool for from-shell text+file" (durable, on the nested perch).

**File-transfer progress (general infra req):** every file transfer is addressable + **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI). Capturing as a messaging-substrate requirement, not shell-specific.

## Your questions answered
1. **`api emit`** — you're right, no `<id>` (owner is known from `api bind`). The optional arg is the **sensory-type tag**: `api emit --type <sensory_type> <payload>`.
2. **info.json capability set** — you're right to question it; **it doesn't belong there.** The capability toolset + sensory types live in the **shell adapter manifest** (single source of truth). The perch `info.json` just carries `type=Shell`, `owner`, `adapter_name`, `status` — spt-core resolves capabilities from the manifest by `adapter_name`. Removing it.
3. **History organization** — mirror the perch structure, node-local: `history/<endpoint_id>/sessions/<session_id>.jsonl` for agent Path-B logs, `history/<endpoint_id>/shells/<adapter>-<n>.jsonl` for a shell-link's command/text/sensory exchange log. Endpoint-owned, shells nested — same shape as `perches/`.

## Refinements captured
- **`--ephemeral` is a manifest property** of the shell adapter, not an agent spawn flag.
- **Shell perches nest under the owner:** `perches/<endpoint_id>/shells/<adapter>-<n>/` — one-agent:many-shell (`GameRobot-0`, `-1`…), named only after the adapter. **Not in the subnet registry** (private to the agent↔shell link, not a general messaging surface). Agent↔shell **can link across nodes** (shell perch lives on the shell's node, nested under the owner id as a label; commands ride Iroh).
- **Shell awareness is injected into agent context** (via SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — available shells *and* the agent's own shell instances with online/offline states. `shell list` exists but the agent ideally just "knows."
- **Link-break always closes the broker+shell binary** — with an optional **pre-close instruction** (to the binary) + **termination timeout** (graceful). Ephemeral(manifest) → perch also torn down; persistent → perch kept **offline** (re-linkable; binary re-spawned on relink). The binary never survives a link-break.

## Security — agree, and it generalizes
Yes — encrypt + handshake the endpoint↔shell link. Thoughts:
- **Cross-node** agent↔shell already rides Iroh → E2E-encrypted by node keypairs. Free.
- **Same-node** shell-binary↔broker (stdin/HTTP/relay) is the exposed surface — a local HTTP port is reachable by *other local processes*. So a **per-link handshake establishing a link token + local-channel encryption at `api bind`**, required on every subsequent message. This **generalizes to all local `api` channels** where potentially-untrusted code touches SPT (HTTP-to-harness-binary too) — a local-link auth capability.
- **Deeper item I'd flag:** a shell binary is the **least-trusted code in the system** (3rd-party, runs on your node, agent-controllable). The capability toolset bounds what the *agent* can ask, but the *binary* runs with whatever OS perms it has. **Shell-binary trust/sandboxing** (what a shell binary may access on the node) is a real security design item — recommend flag it **deferred**, gated like instantiate-anywhere/remote-exec.

Any additional infra-security thoughts of your own before I write this up? Otherwise I'll capture: the 3-channel Shell model, file-transfer progress req, local-link auth, shell-binary-trust (deferred), and the layout/history org into CONTEXT + MANIFEST + STORAGE + DEFERRED.


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 be said about harness binaries, so it should be a known and accepted risk by all users of spt-core.


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…


Capturing everything. Two final notes folded in: ephemeral shells leave no history after termination; shell-binary sandboxing deferred with the baseline stance that running *any* adapter/shell binary is a disclosed, accepted trust risk (same for harness binaries).


**Shells differ structurally from agent endpoints** (see `docs/STORAGE.md`): a Shell has a node-local perch (type=Shell + capability toolset + spool) but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic** (the adapter-hopping property is for agent endpoints only — a Shell's capabilities come from its providing adapter/platform); and its lifecycle is **instantiate/teardown** (by an agent or PresenceChannel `bind`), not the dormant/suspended resting model of agent instances.

**Shells differ structurally from agent endpoints** (full treatment below): a Shell has a node-local perch but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic**; and its lifecycle is link/teardown, not the dormant/suspended resting model of agent instances.

#### Shell model (detailed)

A Shell is a **surface one agent controls** — embodiment of an agent in a new system/environment.

**Provided by a shell adapter.** There are two kinds of adapter, both manifested, both under `adapters/`: a **harness adapter** (`kind="harness"`, e.g. `claude-spt`) hosts *agent* endpoints; a **shell adapter** (`kind="shell"`, e.g. `GameRobot`) provides a *Shell* endpoint. `Shell` is the endpoint type; the shell adapter name is the provider, and its manifest defines the Shell's capability surface. A shell binary is **only ever launched by SPT** (the broker spawns it; never user-launched).

**Three channels** over the typed/binary substrate:
- **command** (agent→shell, durable/spooled) — the manifest's capability toolset; drives the shell and shell→environment interaction.
- **text+file** (2-way, durable/spooled; per-shell) — general payloads; for a shell it is a *forwarder* (harness adapters have this channel too). File transfers are progress-queryable (below).
- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.

**Owner-linked, exclusive.** Spawned by an agent endpoint; both linked; **only that owner `endpoint_id` may control it**. An agent and its shell **can link across nodes** (the shell perch lives on the shell's node; commands ride Iroh). One agent : many shells (`GameRobot-0`, `GameRobot-1`, …).

**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.

**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.

**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.

**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).


**Shells differ structurally from agent endpoints** (full treatment below): a Shell has a node-local perch but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic**; and its lifecycle is link/teardown, not the dormant/suspended resting model of agent instances.

#### Shell model (detailed)

A Shell is a **surface one agent controls** — embodiment of an agent in a new system/environment.

**Provided by a shell adapter.** There are two kinds of adapter, both manifested, both under `adapters/`: a **harness adapter** (`kind="harness"`, e.g. `claude-spt`) hosts *agent* endpoints; a **shell adapter** (`kind="shell"`, e.g. `GameRobot`) provides a *Shell* endpoint. `Shell` is the endpoint type; the shell adapter name is the provider, and its manifest defines the Shell's capability surface. A shell binary is **only ever launched by SPT** (the broker spawns it; never user-launched).

**Three channels** over the typed/binary substrate:
- **command** (agent→shell, durable/spooled) — the manifest's capability toolset; drives the shell and shell→environment interaction.
- **text+file** (2-way, durable/spooled; per-shell) — general payloads; for a shell it is a *forwarder* (harness adapters have this channel too). File transfers are progress-queryable (below).
- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.

**Owner-linked, exclusive.** Spawned by an agent endpoint; both linked; **only that owner `endpoint_id` may control it**. An agent and its shell **can link across nodes** (the shell perch lives on the shell's node; commands ride Iroh). One agent : many shells (`GameRobot-0`, `GameRobot-1`, …).

**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.

**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.

**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.

**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).

All concrete Shells, the PresenceChannel implementation, and presence gossip are deferred.

-**Shells differ structurally from agent endpoints** (see `docs/STORAGE.md`): a Shell has a node-local perch (type=Shell + capability toolset + spool) but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic** (the adapter-hopping property is for agent endpoints only — a Shell's capabilities come from its providing adapter/platform); and its lifecycle is **instantiate/teardown** (by an agent or PresenceChannel `bind`), not the dormant/suspended resting model of agent instances.

+**Shells differ structurally from agent endpoints** (full treatment below): a Shell has a node-local perch but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic**; and its lifecycle is link/teardown, not the dormant/suspended resting model of agent instances.

+A Shell is a **surface one agent controls** — embodiment of an agent in a new system/environment.

+**Provided by a shell adapter.** There are two kinds of adapter, both manifested, both under `adapters/`: a **harness adapter** (`kind="harness"`, e.g. `claude-spt`) hosts *agent* endpoints; a **shell adapter** (`kind="shell"`, e.g. `GameRobot`) provides a *Shell* endpoint. `Shell` is the endpoint type; the shell adapter name is the provider, and its manifest defines the Shell's capability surface. A shell binary is **only ever launched by SPT** (the broker spawns it; never user-launched).

+**Three channels** over the typed/binary substrate:

+- **command** (agent→shell, durable/spooled) — the manifest's capability toolset; drives the shell and shell→environment interaction.

+- **text+file** (2-way, durable/spooled; per-shell) — general payloads; for a shell it is a *forwarder* (harness adapters have this channel too). File transfers are progress-queryable (below).

+- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.

+**Owner-linked, exclusive.** Spawned by an agent endpoint; both linked; **only that owner `endpoint_id` may control it**. An agent and its shell **can link across nodes** (the shell perch lives on the shell's node; commands ride Iroh). One agent : many shells (`GameRobot-0`, `GameRobot-1`, …).

+**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.

+**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.

+**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.

+**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).

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.


Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

**local-link authentication**:
Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

**binary-trust disclosure (accepted risk)**:
A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

### Messaging substrate

**file-transfer progress**:
Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).


Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

**local-link authentication**:
Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

**binary-trust disclosure (accepted risk)**:
A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

### Messaging substrate

**file-transfer progress**:
Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).

+Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

+**binary-trust disclosure (accepted risk)**:

+A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

+Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).

spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.


- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).

- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).
- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.


- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).
- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.

- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).

- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.

+- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.

**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.


Now MANIFEST.md — the shell-adapter manifest fields.


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

## 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)


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 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)

# 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
```

---

## 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).

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

+kind = "shell"                     # vs "harness"

+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"] }

+[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>`.


Now STORAGE.md — shell perch nesting + history org.


      spool.db                   SQLite spool (typed + binary payloads)
      status                     active | dormant | suspended    (agent endpoints)
      nested/<child_id>/         Worker / Psyche perches (own info.json, spool)
  history/                       node-local — Path-B native logs + Shell logs

      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


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)

      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

# $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)
  history/                       node-local — Path-B native logs + Shell logs
  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).

-  history/                       node-local — Path-B native logs + Shell logs

+      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)

+      sessions/<session_id>.jsonl    agent Path-B native logs

+      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log


## 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.

## 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.