- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).

- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.


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

- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.

# TOTP-seeded SPAKE2 node pairing

## Status

accepted (2026-05-29)

## Context

"Zero-config WAN that just works" still requires a first-contact ceremony by which a user's new node is authorized into their existing subnet (key exchange + mutual trust), with no account and no central operator. The research brief vetted Ed25519 identity, Noise, Magic-Wormhole-style PAKE, and TOFU. The product question is the day-one pairing UX for a single user's own machines.

A natural-seeming option is TOTP: the user provisions a seed into an authenticator app and pairs future nodes with the rotating 6-digit code. Used the obvious way (a new node sends the code to a trusted node for verification), this is insecure: the code is a ~20-bit bearer token, replayable within its 30s window, not bound to the key being exchanged — a real-time MITM can replay the code with their own pubkey and join the subnet. It also performs no key exchange, requiring a separate unbound handshake.

## Decision

Adopt **TOTP-seeded SPAKE2 pairing**: keep the familiar TOTP UX but use the 6-digit code as the **password for a SPAKE2 (PAKE) handshake**, not as a verified bearer token.

- The TOTP **seed** is the durable subnet secret: generated on the first node, shown as a QR for the user's authenticator app, and replicated to every already-trusted node.
- To pair a new node, a trusted node (online, no human — it holds the seed) computes the current code as its SPAKE2 password; the user reads the code off their phone and types it into the new node as its password. Matching codes → PAKE succeeds → pubkeys exchanged + bound, MITM-resistant encrypted channel.

PAKE is the correct primitive for a low-entropy human-transcribed secret: it resists offline brute force (one online guess per attempt) and binds the code to the established key. Cryptographic strength comes from the PAKE construction, not from the 6 digits being secret.

Rejected alternatives:
- **TOTP-as-bearer-token** — replayable, unbound, MITM-vulnerable (above).
- **Plain Magic Wormhole / fresh-code-each-time** — secure, but needs a human/relay at *both* ends each pairing. TOTP-seeded PAKE keeps the persistent seed on online trusted nodes so the user interacts only with the new node.
- **Paste-the-key** — too error-prone for "just works."
- **Account-anchored auto-trust** — reintroduces the account dependency the product is shedding.

Trust model: local per-node trust store of authorized peer pubkeys; TOFU after pairing (accept on first post-ceremony connect, warn on key change). Cross-subnet (two different users) is out of scope for v1.

## Consequences

- A seed-holder node must be online and reachable by the untrusted new node at pairing time; the relay/discovery layer must permit pre-trust contact on a dedicated pairing ALPN.
- Operational details to implement: ±1 TOTP window tolerance, handshake-attempt rate limiting, QR re-provisioning for recovery from any still-trusted node.
- Losing all nodes *and* the authenticator seed makes the subnet unrecoverable — must be documented.
- Revocation of a paired node is a separate trust-store delete; TOTP gates joining only, not continued membership.
- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).

+- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.


Audit done. Results:

**Genuine staleness fixed:**
- **ADR-0002** — "WAN transport becomes a later *milestone*" **contradicted** the final decision that networking is first-class v1. Corrected to "first-class v1 capability, not a post-v1 milestone." Also added a forward-ref from its single-point-of-failure consequence to ADR-0004's broker/brain answer.

**Clarity gaps closed:**
- **DEFERRED.md** — the deferred "OS-level input-activity presence (7a-A)" row could be confused with the v1 broker-PTY-input-*sensing* we added later. Added a note distinguishing them (broker sensing on a held PTY is v1; OS-wide monitoring is deferred).
- **ADR-0005** — added a consequence line for the later pairing-UX additions (fetch-code-from-any-node, subnet naming).

**Checked, no changes needed:**
- **ADR-0001** — clean; freeze + mitigations still hold.
- **ADR-0003** — already amended for native-instances/dormancy/two-tier/remote-control.
- **ADR-0004** — consistent with final.
- **KNOWN-HAZARDS** — written *after* the broker/brain ADRs, so it already references ADR-0002/0003/0004, registry, daemon model. It's a sister-project-harvested checklist; the new spt-core gates that *extend* a hazard (deferred-message gate on dormant instances, transition echo-commune, two-tier precedence) live in CONTEXT and are gestured at in the mapping notes — I left it untouched rather than scope-creep a harvest doc into a forward-spec.

Doc set is now internally consistent with the final model. Good moment to review, or want me to take on a §17 open item next (`$SPT_HOME` layout or manifest schema)?


we can touch on the open items


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


Let's work them one at a time, same as before. Starting with the most foundational — **§17.1 `$SPT_HOME` on-disk layout** (it gates migration, backup, and the single-source-of-truth hazard).

I'll hold the resolved layout in a new `docs/STORAGE.md` (it's implementation detail — neither glossary nor decision). Proposed tree first:

```
$SPT_HOME/                      default: %LOCALAPPDATA%\spt  |  ~/.spt   (override: $SPT_HOME)
  identity/                     ── durable, back-up/migrate ──
    node.key                    Ed25519 node identity
    trust/                      authorized peer pubkeys (trust store)
    subnet.json                 subnet name + TOTP seed (the durable subnet secret)
  agents/                       ── durable ──
    <adapter_name>/
      <endpoint_id>/
        info.json               perch metadata (identity, type, status, adapter_name)
        spool.db                SQLite spool
        status                  active | dormant | suspended
        live-context.md         per-agent live context (synced to ALL instances)
        projects/<project-key>/
          project-context.md    per-(agent,project) context (synced to same-project only)
        nested/<child_id>/      Worker / Psyche perches
  history/                      ── durable-ish ── Path-B native logs + Shell logs
  registry/                     ── ephemeral, rebuildable ── local cache of subnet registry
  daemon/                       ── ephemeral ── brain rehydration state, broker handle metadata
  updates/                      ── ephemeral ── staged binaries pending handoff
  manifests/                    registered adapter manifests (or pointers)
```

Five real decision forks:

**A — Root: single dir vs XDG split.** Single root (`~/.spt`) for everything vs Linux XDG split (config/data/state in three XDG dirs). **Recommend single root** — relocatable, one dir to back up/move (matches the marketplace/handheld portability goal), `$SPT_HOME` override. XDG-correct splitting fights the relocatable-single-dir goal and complicates migration. Agree?

**B — Perch keying: `<adapter_name>/<endpoint_id>`.** Namespace perches under adapter (not flat endpoint_id). Rationale: `(adapter_name, endpoint_id)` is exactly the cross-node instance identity (same harness+adapter), and it prevents collision if two adapters host the same id — `ling` under `claude-spt` ≠ `ling` under `spt-codex` (different agents). **Recommend yes.** Agree, or should bare `ling` be adapter-agnostic?

**C — Config vs ephemeral separation.** `identity/` + `agents/` + `history/` are durable (the backup/migrate set); `registry/` + `daemon/` + `updates/` are regenerable. The split makes "what to back up / what migration carries" obvious. **Recommend keep the durable/ephemeral boundary explicit.** Agree?

**D — `project-key` encoding.** project context is per-(agent, project). Key the project dir by: a hash of the absolute project path, or a sanitized path-slug? Sister used a project-hash. **Recommend hash** (stable, collision-resistant, no path-length/charset issues) + store the human path inside for legibility. But note: the *same project* on two machines may sit at *different absolute paths* (`/home/x/proj` vs `C:\Users\x\proj`) — for project-context sync to match across instances, the key must be **path-independent** (e.g. a project identity from git remote URL / a project marker file), not the local path. Which project-identity do you want — git-remote-derived, a generated `.spt-project` marker id, or local-path-hash (breaks cross-machine sync)?

**E — Single path resolver.** One function maps `(adapter, endpoint_id[, child][, project])` → path; nothing constructs paths ad-hoc (KNOWN-HAZARDS 6.1, no flat/nested ambiguity). **Recommend mandate it.** Agree?

D is the one with a real cross-machine subtlety — your call there especially.


Design spt-core binary and messaging library architecture


rejections: 1. categorizing endpoints under the adapter name. all adapters will share identical functionality/the same subset of manifest capabilities as far as SPT is concerned, so any given endpoint should be able to run under any adapter. for instance, we might run ling on claude code at first, but later want to revive her on codex or pi. 2. project context under endpoints. there's a very good reason projects are not stored in the `tracked/agents/` folder on modern spt: it's so any given live agent can synthesize a broader context from other agent contexts within the same project. it could also be used for one agent to look up which endpoint is most responsible for a specific task, to facilitate query routing. this is also why the git structure creates a branch for both agents `a-<agent_id>` and projects `p-<project_id>`. all contexts should still be tracked as a git repo just like they are today under modern spt. 3. perches are mixed with tracked context and live agent details. that's MAYBE ok--i hadn't considered it before--but in this model, we have to ensure the perch-related files are never committed to/tracked by the context+details git repo. otherwise, SPT will end up with clobbered spools and perch statuses across nodes. --- A. single root B. disagree as stated above. IDs are adapter-agnostic. attempts to run more than one instance of any given ID under a single node will be rejected, regardless of which adapter it's on. C. agree. D. git-remote-derived first, and if there's no git repo in the project, then just use the project's folder name. E. agree. no paths should be hardcoded, all with centralized idempotent resolution so any future path changes carry to all dependent functions --- with all this taken into account, please propose an updated on-disk layout


All three rejections land — and they sharpen the model meaningfully. Absorbing:

1. **IDs are adapter-agnostic** — `ling` can move claude→codex→pi. So perches key on bare `endpoint_id`; `adapter_name` is a *property of the running instance* (in `info.json`), not a path segment. One instance per ID per node, enforced regardless of adapter.
2. **Projects are a separate tree, not under agents** — so any agent can synthesize cross-agent context within a project + do query-routing ("which endpoint owns this task"). Mirrors modern spt's `a-<id>` / `p-<project_id>` git branches. Context stays a **git repo**.
3. **Perch runtime must never be git-tracked** — else spools/statuses clobber across nodes when context syncs. Cleanest fix: **fully separate** node-local perch runtime from the synced tracked-context git repo (no gitignore tightrope).

Key clarification this surfaces: **P2P sync replaces the git *remote* (GitHub), not the local git tracking.** Context stays a local git repo with the `a-`/`p-` branch views; sync moves its data over P2P. Capturing.

**Updated layout:**

```
$SPT_HOME/                       default %LOCALAPPDATA%\spt | ~/.spt   (override $SPT_HOME)
  identity/                      ── node-local, durable, NEVER git-tracked ──
    node.key                     Ed25519 node identity (per-node; must not sync)
    trust/                       authorized peer pubkeys
    subnet.json                  subnet name + TOTP seed (replicated via pairing, not git)
  tracked/                       ── git repo; synced across nodes over P2P (replaces GitHub remote) ──
    agents/<endpoint_id>/
      live-context.md            per-agent; syncs to ALL instances   (branch a-<endpoint_id>)
    projects/<project_id>/
      <endpoint_id>/
        project-context.md       per-(agent,project); per-project tree enables cross-agent
                                 synthesis + query routing            (branch p-<project_id>)
  perches/                       ── node-local runtime, NEVER tracked/synced (clobber-safe) ──
    <endpoint_id>/
      info.json                  identity, type, status, CURRENT adapter_name
      spool.db
      status                     active | dormant | suspended
      nested/<child_id>/         Worker / Psyche perches
  history/                       node-local — Path-B logs + Shell logs
  registry/                      ── ephemeral, rebuildable ── subnet-registry cache
  daemon/                        ── ephemeral ── brain rehydration + broker handle metadata
  updates/                       ── ephemeral ── staged binaries pending handoff
  manifests/                     registered adapter manifests / pointers
```

Resolved: A single-root ✓ · B id-keyed (adapter-agnostic) ✓ · C durable/ephemeral split ✓ · D `project_id` = git-remote-derived, else folder name ✓ · E one central path resolver, no hardcoded paths ✓.

The big structural change from my first pass: **two separate trees** — `tracked/` (git, synced, the *mind*: live + project context) vs `perches/` (node-local, never synced, the *plumbing*: spool/status/info). That eliminates the clobber risk entirely and matches the agent/project dual-view. Writing it up.


Write

# $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>/
      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
      spool.db                   SQLite spool
      status                     active | dormant | suspended
      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
  manifests/                     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.

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


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs\STORAGE.md (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>/
      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
      spool.db                   SQLite spool
      status                     active | dormant | suspended
      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
  manifests/                     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.

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


rejections: 1. categorizing endpoints under the adapter name. all adapters will share identical functionality/the same subset of manifest capabilities as far as SPT is concerned, so any given endpoint…


Updating CONTEXT for the model-level facts these surfaced.


Edit

### Cross-node Psyche sync

Today's modern SPT backs up `psyches/tracked/` via a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core this is **replaced** by free, no-registration, no-setup cross-node sync over the built-in P2P networking. Psyche context replicates between a user's nodes as ordinary file/blob payloads on the messaging substrate (the same typed/binary payload seam Shells need). The GitHub-repo sync mechanism is retired.

### Cross-node Psyche sync

Today's modern SPT backs up its tracked-context **git repo** to a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core, **only the remote is replaced** — the context stays a local git repo with its `a-<endpoint_id>` (per-agent) and `p-<project_id>` (per-project) branch views; the *data* syncs between a user's nodes over the built-in P2P networking instead of being pushed to GitHub (no `gh`/account/setup). Concurrent multi-node writes are arbitrated by the precedence/freshness guard (KNOWN-HAZARDS 6.5, node identity in the marker). The per-project branch view exists so any agent can synthesize broader context from *other* agents' contexts in the same project and support query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances. On-disk layout: `docs/STORAGE.md`.


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)

### Cross-node Psyche sync

Today's modern SPT backs up `psyches/tracked/` via a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core this is **replaced** by free, no-registration, no-setup cross-node sync over the built-in P2P networking. Psyche context replicates between a user's nodes as ordinary file/blob payloads on the messaging substrate (the same typed/binary payload seam Shells need). The GitHub-repo sync mechanism is retired.

### Cross-node Psyche sync

Today's modern SPT backs up its tracked-context **git repo** to a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core, **only the remote is replaced** — the context stays a local git repo with its `a-<endpoint_id>` (per-agent) and `p-<project_id>` (per-project) branch views; the *data* syncs between a user's nodes over the built-in P2P networking instead of being pushed to GitHub (no `gh`/account/setup). Concurrent multi-node writes are arbitrated by the precedence/freshness guard (KNOWN-HAZARDS 6.5, node identity in the marker). The per-project branch view exists so any agent can synthesize broader context from *other* agents' contexts in the same project and support query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances. On-disk layout: `docs/STORAGE.md`.

-Today's modern SPT backs up `psyches/tracked/` via a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core this is **replaced** by free, no-registration, no-setup cross-node sync over the built-in P2P networking. Psyche context replicates between a user's nodes as ordinary file/blob payloads on the messaging substrate (the same typed/binary payload seam Shells need). The GitHub-repo sync mechanism is retired.

+Today's modern SPT backs up its tracked-context **git repo** to a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core, **only the remote is replaced** — the context stays a local git repo with its `a-<endpoint_id>` (per-agent) and `p-<project_id>` (per-project) branch views; the *data* syncs between a user's nodes over the built-in P2P networking instead of being pushed to GitHub (no `gh`/account/setup). Concurrent multi-node writes are arbitrated by the precedence/freshness guard (KNOWN-HAZARDS 6.5, node identity in the marker). The per-project branch view exists so any agent can synthesize broader context from *other* agents' contexts in the same project and support query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances. On-disk layout: `docs/STORAGE.md`.


**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address.

**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected.


**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected.

The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

-The logical identity (`ling`). Shared subnet-wide. The thing you address.

+The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected.

The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:


$SPT_HOME layout resolved → `docs/STORAGE.md`. Updating PRD to reflect it.


## 17. Open design-phase questions

- Exact `$SPT_HOME` on-disk layout (single source of truth; no flat/nested-sibling ambiguity — KNOWN-HAZARDS 6.1).
- Manifest schema concretes; capability-declaration shape.

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


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)

## 17. Open design-phase questions

- Exact `$SPT_HOME` on-disk layout (single source of truth; no flat/nested-sibling ambiguity — KNOWN-HAZARDS 6.1).
- Manifest schema concretes; capability-declaration shape.

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

-- Exact `$SPT_HOME` on-disk layout (single source of truth; no flat/nested-sibling ambiguity — KNOWN-HAZARDS 6.1).

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

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


- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.

- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.


- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
- [`docs/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.

- [`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/STORAGE.md`](./docs/STORAGE.md) — `$SPT_HOME` on-disk layout.

- Sister project networking research: `claude_skill_owl/docs/research/SPT Networked Messaging Research Brief.md`.


§17.1 resolved → `docs/STORAGE.md`, CONTEXT + PRD updated. The structural win: **`tracked/` (git repo, synced, the mind) fully separate from `perches/` (node-local, never synced, the plumbing)** — kills the cross-node clobber risk, and the adapter-agnostic ID keying means `ling` can hop harnesses.

Remaining open items:
- **§17.2 — Manifest schema concretes + capability-declaration shape** (the actual TOML structure for the seams we enumerated)
- **§17.3 — Refining Psyche live/project authoring directives** (so the two-tier split holds — content/prompt design, not architecture)
- **§17.4 — Warm/cold dormancy default policy + suspend-from-anywhere command surface**
- **§17.5 — Consent/security-gate UX** (instantiate-anywhere + remote-exec)
- **§17.6 — Remote-control: formally a Shell, or bare network session-surface?**

§17.2 (manifest schema) is the most load-bearing — it's the concrete artifact every adapter author writes, and it pins down all the seams. Want to take that next, or pick another?


note about endpoints being adapter-agnostic: they should still "remember" their adapter history for the sake of the resume feature. so the latest adapter would be the default resume, next latest would be 2nd in the adapters list for resume, oldest would be 2nd to last, and "choose a different adapter" would be the final offering. --- maybe change "manifests" folder name to "adapters" folder name? --- how to Shells fit into all this? --- after those, we can move to 17.2. initial schema proposal should be informed by the functional requirements of modern spt as it exists today.


note about endpoints being adapter-agnostic: they should still "remember" their adapter history for the sake of the resume feature. so the latest adapter would be the default resume, next latest would…


Three good points. Taking them in order, then I'll tee up 17.2.

**1. Adapter history per endpoint** — endpoints remember an ordered adapter history (recency-sorted) for the resume UX: latest adapter = default resume, then 2nd-latest, … oldest, then "choose a different adapter" as the final option. This is **endpoint identity metadata**, so it belongs in `tracked/` (synced — a fresh node then knows the resume order), distinct from the *current* running adapter in node-local `perches/info.json`.

**2. `manifests/` → `adapters/`** — agreed, clearer (a manifest *is* an adapter's contract).

**3. How Shells fit** — they're endpoints, but a different shape:
- **Perch, yes** (node-local `perches/<shell_id>/` — type=Shell, capability toolset, spool for inbound op-commands + outbound user responses/files, status). Same plumbing as agents.
- **No `tracked/` context** — a Shell has no mind (no live/project context, no Psyche), so nothing to sync. Shells never appear in `tracked/`.
- **Logs** in `history/` (node-local — already planned for Shell logs).
- **Adapter/platform-bound, NOT adapter-agnostic** — the adapter-hopping property is for *agent* endpoints (interchangeable harnesses). A GameRobot only exists where its game runs; its capability surface comes from its providing adapter. So adapter-agnosticism applies to agent endpoints only.
- **Lifecycle = instantiate/teardown** (by an agent or PresenceChannel `bind`), not the dormant/suspended resting model of agent instances.

Capturing all three.


  tracked/                       ── git repo; synced across nodes over P2P ──
    agents/<endpoint_id>/
      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
      spool.db                   SQLite spool
      status                     active | dormant | suspended
      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
  manifests/                     registered adapter manifests / pointers

  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


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)

  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

# $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>/
      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
      spool.db                   SQLite spool
      status                     active | dormant | suspended
      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
  manifests/                     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.

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

+      meta.json                  endpoint identity metadata: type, ordered adapter history, created

-      info.json                  identity, type, status, current adapter_name

-      spool.db                   SQLite spool

-      status                     active | dormant | suspended

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

-  manifests/                     registered adapter manifests / pointers

+  adapters/                      registered adapter manifests / pointers


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

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


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

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

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

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


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

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.


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

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.


The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected.

The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)


The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

-The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected.

+The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)

The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:


All three captured (adapter history in synced `meta.json`, `manifests/`→`adapters/`, Shells fully placed).

Now **§17.2 — manifest schema**, grounded in modern spt's actual functional requirements as you asked. Launching a background extraction of the sister project's full functional surface (every `$OWL`/`$LIVE` subcommand, hook, config knob, and the wrapper/Psyche operations) so the schema covers real needs, not guesses.


Agent

Extract modern spt functional surface

Prompt:
Explore the sister project at C:\Users\decid\Documents\projects\claude_skill_owl (a Rust workspace — "modern SPT" / owl, an inter-agent messaging + live-agent-lifecycle tool for Claude Code). I am designing a declarative "runtime manifest" schema for a rebuild (spt-core) that abstracts all Claude-Code-specific behavior behind config, so other harnesses can implement the same contract. To design it well I need a complete inventory of what the current system actually DOES that is harness-specific. Produce a structured functional-requirements inventory.

Search thoroughly across src/ (cli.rs, owl/, live/, common/), the plugin/ directory (skills, hooks, plugin.json, settings), build.rs, and any docs/.planning. Enumerate concretely:

1. **CLI surface** — every `owl.exe` and `owl.exe live` subcommand and its key flags/args. Group by purpose (messaging, lifecycle, listing, hooks, internal). Cite cli.rs.
2. **Claude-Code hooks used** — every hook the plugin registers (SessionStart, SubagentStart/Stop, PreToolUse, Stop, etc.), what each fires (which owl subcommand), and why. Cite the plugin settings/hook configs.
3. **Live-agent lifecycle operations** — what the Psyche wrapper does: how it spawns/resumes the Psyche (the exact `claude` invocation incl. flags like `-p`, `--resume`, `--session-id`, `--append-system-prompt`, model selection), how echo-communes are fired (the haiku invocation), pulse cadence, signoff, orphan handling. Cite live/wrapper.
4. **Conversation-history access** — how/where it reads Claude Code session transcripts (paths, format) for echo-communes and resume briefs.
5. **Context model** — the live-context vs project-context split, the psyches/tracked/ git repo structure (branches a-<id>, p-<id>), commune/signoff file-drop flow (file names, watched dirs).
6. **Env vars** — every env var the system injects or reads ($OWL, $LIVE, OWL_SESSION_ID, SPT_HOME, etc.) and its purpose.
7. **Config/state files** — info.json fields, wrapper-state.json fields, any config.json knobs, registry format.
8. **Anything else harness-specific** — model names, billing-relevant flags, activity/idle detection mechanism, message injection mechanism.

For each item give: the operation, what's harness-specific about it (what would differ for Codex/another harness), and a file path citation. The goal is a checklist a manifest schema must be able to express. Return a structured markdown report; do not write any files.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: a9b036e2dad234d87 (internal ID - do not mention to user. Use SendMessage with to: 'a9b036e2dad234d87' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\a9b036e2dad234d87.output
Do NOT Read or tail this file via the shell tool — it is the full sub-agent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Extract modern spt functional surface

Prompt:
Explore the sister project at C:\Users\decid\Documents\projects\claude_skill_owl (a Rust workspace — "modern SPT" / owl, an inter-agent messaging + live-agent-lifecycle tool for Claude Code). I am designing a declarative "runtime manifest" schema for a rebuild (spt-core) that abstracts all Claude-Code-specific behavior behind config, so other harnesses can implement the same contract. To design it well I need a complete inventory of what the current system actually DOES that is harness-specific. Produce a structured functional-requirements inventory.

Search thoroughly across src/ (cli.rs, owl/, live/, common/), the plugin/ directory (skills, hooks, plugin.json, settings), build.rs, and any docs/.planning. Enumerate concretely:

1. **CLI surface** — every `owl.exe` and `owl.exe live` subcommand and its key flags/args. Group by purpose (messaging, lifecycle, listing, hooks, internal). Cite cli.rs.
2. **Claude-Code hooks used** — every hook the plugin registers (SessionStart, SubagentStart/Stop, PreToolUse, Stop, etc.), what each fires (which owl subcommand), and why. Cite the plugin settings/hook configs.
3. **Live-agent lifecycle operations** — what the Psyche wrapper does: how it spawns/resumes the Psyche (the exact `claude` invocation incl. flags like `-p`, `--resume`, `--session-id`, `--append-system-prompt`, model selection), how echo-communes are fired (the haiku invocation), pulse cadence, signoff, orphan handling. Cite live/wrapper.
4. **Conversation-history access** — how/where it reads Claude Code session transcripts (paths, format) for echo-communes and resume briefs.
5. **Context model** — the live-context vs project-context split, the psyches/tracked/ git repo structure (branches a-<id>, p-<id>), commune/signoff file-drop flow (file names, watched dirs).
6. **Env vars** — every env var the system injects or reads ($OWL, $LIVE, OWL_SESSION_ID, SPT_HOME, etc.) and its purpose.
7. **Config/state files** — info.json fields, wrapper-state.json fields, any config.json knobs, registry format.
8. **Anything else harness-specific** — model names, billing-relevant flags, activity/idle detection mechanism, message injection mechanism.

For each item give: the operation, what's harness-specific about it (what would differ for Codex/another harness), and a file path citation. The goal is a checklist a manifest schema must be able to express. Return a structured markdown report; do not write any files.


Extraction running in background (it's launched with `run_in_background: true`, so I stay reachable). I'll propose the §17.2 manifest schema when it returns — grounded in the sister project's real CLI surface, hooks, lifecycle invocations, context model, env vars, and config files.

While it works: §17 status — **17.1 resolved** (STORAGE.md), **17.2 in progress** (schema, pending extraction). Still queued after: 17.3 (Psyche live/project authoring directives), 17.4 (dormancy default policy + suspend command surface), 17.5 (consent/security-gate UX), 17.6 (remote-control: formal Shell vs bare session-surface).

I'll pick back up on 17.2 the moment the extraction lands.


<task-notification>
<task-id>a9b036e2dad234d87</task-id>
<tool-use-id>toolu_01WPvt2qaBMnXQ1nkdX6AkYJ</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\a9b036e2dad234d87.output</output-file>
<status>completed</status>
<summary>Agent "Extract modern spt functional surface" completed</summary>
<result>I now have a complete and detailed picture of the entire system. I have all the harness-specific operations across CLI, hooks, Claude invocations, transcript access, context model, env vars, config/state, and the file-drop flow. Let me compile the structured functional-requirements inventory.

---

# SPT/owl Harness-Specific Functional Inventory

This inventory enumerates every Claude-Code-specific behavior the current `owl`/`spt` system performs. Each item gives the operation, what is harness-specific (would differ for Codex/another harness), and a file-path citation. This is the checklist a declarative runtime-manifest schema must be able to express.

The plugin ships as a Claude Code plugin named `spt` (`plugin/spt/.claude-plugin/plugin.json`), v1.11.22, with a single bundled binary `owl.exe`, 12 hook-less skills, and a hooks.json registering 7 hooks. All harness coupling is concentrated in: hook wiring, the `claude`/`ccs`/`haiku` subprocess invocations, transcript (jsonl) reads, and the env-var injection pipeline.

---

## 1. CLI Surface (`src/cli.rs`)

Two binaries: `owl.exe <cmd>` and `owl.exe live <cmd>` (the latter exposed to skills as `$LIVE`). Grouped by purpose. Harness-specific notes flagged per group.

### Messaging
- `owl send <target> [from] [--reply-to <id>] [--deferred]` (alias `deliver`) — fire-and-forget message; `--deferred` = spool-only, no TCP wake (cli.rs:54-65).
- `owl ring <target> [from]` (alias `ask`) — send and wait for reply (cli.rs:69-72).
- `owl reply <sender> [my_id]` — deprecated/hidden (cli.rs:133-137).
- `live commune [self_id]` — reads a communal-update body from stdin, delivers to `{self_id}-psyche` via TCP/spool (cli.rs:323-326).

*Harness-specific:* message bodies are surfaced to the agent as `<owl_messages>` XML injected via hook `additionalContext` (Claude-Code-specific injection channel). The transport itself (TCP + file spool) is harness-agnostic.

### Lifecycle (live agents / Psyche)
- `live start <id> [--period <secs>] [--pulse-psyche]` (cli.rs:285-294).
- `live stop [id] [--all]` / `owl stop [id] [--all]` (cli.rs:99-103, 296-301).
- `live revive <id> [--period] [--pulse-psyche]` (cli.rs:354-362).
- `live fork <src> <new_id> [--period] [--pulse-psyche]` — copies stored context to a new id and starts it (cli.rs:385-398).
- `live signoff <id> [has_psyche] [-m <msg>]` — graceful shutdown w/ INIT_SIGNOFF (cli.rs:369-376).
- `live amend-signoff <self_id> <message...>` (cli.rs:379-383).
- `live boot-spine <id>` — Spine supervisor (cli.rs:365-367).
- `owl reboot [id]` (cli.rs:114-116); `owl signoff`/`owl stash` — deprecated (cli.rs:119-130).
- `new-alarm <self_id> <time_spec> [--message-file] [msg...]` — one-shot timed pulse (cli.rs:173-184).

### Context / Psyche state
- `live psyche-download [self_id]` — emit summarized stored context (cli.rs:336-339).
- `live clear-psyche [self_id]` (cli.rs:342-344).
- `live suppress-drift <self_id> <project>` (cli.rs:349-352).
- `live pulse-wait <seconds>` (cli.rs:329-331).

### Listing
- `owl list [--all|--offline] [--here] [--verbose]` (cli.rs:79-93).
- `live list [--all|--offline] [--here] [--verbose]` (cli.rs:307-321).
- `live list-psyches` (cli.rs:334).
- `owl list-working [self_id]` (cli.rs:208-210).
- `owl whoami` (cli.rs:96).
- `live pick-spec [--resolve <name>]` — builds an **AskUserQuestion-ready JSON spec** for the no-arg picker (cli.rs:404-409). *Harness-specific:* AskUserQuestion is a Claude-Code tool; the spec format is tailored to it.

### Hooks (hidden subcommands — each maps to a hooks.json entry)
- `plugin-session-start` (SessionStart), `cleanup-session` (SessionEnd), `hook-check` (PreToolUse), `hook-idle` (Stop), `hook-prompt` (UserPromptSubmit), `hook-subagent-start` (SubagentStart), `hook-subagent-stop` (SubagentStop) (cli.rs:106-108, 142-144, 199-250).
- `session-resume` (cli.rs:140); `sync-pull-async` (UserPromptSubmit detached child, cli.rs:148-154).

### Internal (hidden)
- `_psyche-wrapper <self_id> <period> <gen> <session_name> [pulse_psyche]` — the Psyche event loop (cli.rs:213-224).
- `_spine-wrapper`, `_touch-wrapper` (cli.rs:227-238).
- `_echo-commune [--force] [--forward-to-self] <session_uuid> <self_id> <psyche_id>` (cli.rs:260-278).
- `doctor [--fix]`, `gsd-setup`, `psyche-sync-setup [--disable]`, `version-remind <old>` (hidden), `setup <id> [mode]`, `poll <id> [mode] [flags...]` (cli.rs:14-52, 156-163, 191-197, 252-257).

---

## 2. Claude-Code Hooks (`plugin/spt/hooks/hooks.json`)

Every hook command is `$CLAUDE_PLUGIN_ROOT/owl.exe <subcmd>`. The 7 registered hooks:

| Hook event | Fires | Why | Harness-specific aspect |
|---|---|---|---|
| **SessionStart** | `plugin-session-start` | Injects `$OWL`/`$LIVE` env vars via `CLAUDE_ENV_FILE`; writes `OWL_SESSION_ID`; version trampoline; syncs settings.json; re-orientation on clear/compact; auto-pick block; session resume | Reads Claude stdin `session_id`/`source`/`agent_type`; writes `CLAUDE_ENV_FILE`; emits `hookSpecificOutput.additionalContext` (hooks.json:3-12, plugin_session_start.rs:164-229) |
| **SessionEnd** | `cleanup-session` | Tears down the session's perch | Uses `OWL_SESSION_ID` (hooks.json:13-22) |
| **PreToolUse** | `hook-check` | Drains spooled messages into `additionalContext`; clears `.idle-ready`; nudges `run_in_background` when Agent tool detected; first-install primer | Reads `tool_name`/`agent_id` from stdin; emits `hookSpecificOutput` PreToolUse envelope (hooks.json:23-32, hook_check.rs) |
| **Stop** | `hook-idle` | Sets `.idle-ready`; writes `.more-done` echo-commune sentinel if live+psyche; version-change changelog | Stop hook has **no** `hookSpecificOutput` schema, so it can't inject — it writes filesystem sentinels instead (hooks.json:33-42, hook_idle.rs:1-93) |
| **UserPromptSubmit** | `hook-prompt` | Clears `.idle-ready`; detects wake sentinels; drains spool; injects reachability notice | Detects capsule wake-sentinel prompt text `--- [INCOMING]` (hooks.json:43-52, hook_prompt.rs) |
| **SubagentStart** | `hook-subagent-start` | Creates a working perch `{parent}-w{N}`; injects `<owl_working_perch>` context into the child; notifies parent | Reads `agent_id`/`agent_type`; emits child `additionalContext` (hooks.json:53-62, hook_subagent_start.rs) |
| **SubagentStop** | `hook-subagent-stop` | Tears down the working perch | (hooks.json:63-72) |

*Manifest requirement:* a hook table mapping {harness event name → command + which stdin fields it consumes → what output channel it writes (env file / additionalContext / filesystem sentinel)}. Note the Stop-hook constraint (no injection schema) drives the entire `.more-done` sentinel + wrapper-poll architecture — the manifest must express **per-event output capabilities**.

---

## 3. Live-Agent Lifecycle / Psyche Wrapper (`src/live/wrapper/claude.rs`, `start.rs`)

The Psyche wrapper is a detached process spawned by `live start` that drives a hidden `claude` subagent (the "Psyche"). All of this is deeply Claude-CLI-specific.

### Psyche spawn (init) — `claude.rs:42-216`
`Command::new(cli_binary)` (default `"claude"`), `current_dir = psyche_dir()`, with args:
```
-p --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
```
- The `--agents` JSON embeds `psyche.md` (compile-time `include_str!`, claude.rs:13, build.rs) with `{{self_id}}`/`{{psyche_id}}`/`{{period}}` substituted, restricting Psyche tools to `["Read","Write","Edit"]` (claude.rs:16-38).
- init message written to **stdin**; session UUID parsed from stdout JSON (`"session_id"` key or UUID regex) (claude.rs:107-148, 694-724).
- `env_remove("OWL_SESSION_ID")` before every spawn (claude.rs:57).
- Windows `CREATE_NO_WINDOW` flag (claude.rs:82-87).

### Psyche resume — `claude.rs:251-389`
```
-p --resume <session_uuid> --model sonnet --fallback-model opus
--effort medium --dangerously-skip-permissions --disable-slash-commands
--tools Read,Edit,Write
```
(no `--output-format` — plain-text for readable logs). Prompt = `[Current time: …]` + envelope-decoded message + passive context, written to stdin. Response parsed by `extract_claude_response` handling 3 output shapes: whole-buffer `result` JSON, stream-json JSONL aggregation, plain text (claude.rs:628-681).

### `final_session` (signoff) — `claude.rs:473-569`
Same `--resume` shape, fires a last context-save turn, returns true to break the loop.

### PULSE_TRIGGER 3-tier recovery — `claude.rs:434-469`
Tier 1: fresh `claude` init (no `--resume`). **Tier 2: swaps `cli_binary` to `"ccs"`** and latches it on success. Tier 3: wait for next pulse. *Harness-specific:* `ccs` is an alternate Claude-compatible CLI fallback.

### Echo-commune (the "haiku invocation") — `echo_commune.rs:983-1033` (`build_haiku_cmd`)
A separate `claude -p` fired fire-and-forget by the wrapper sentinel:
```
claude -p --model haiku --effort low --output-format json
--disable-slash-commands
--allowed-tools "Read Grep Glob Bash(git log:*) Bash(git diff:*) Bash(git status)"
--add-dir <psyche_dir_abs> --add-dir <self_jsonl_parent_abs>
```
- `current_dir = psyche_dir`; `env_remove("OWL_SESSION_ID")`; **`env("OWL_ECHO_COMMUNE","1")`** as recursion guard so the haiku child's own Stop hook bails (echo_commune.rs:990-995).
- `--no-session-persistence` **deliberately omitted** (forensic resume affordance); freshness via absence of `--resume` (echo_commune.rs:1007-1011).

### Pulse cadence — `start.rs`, `cli.rs`, `echo_fire.rs`
- `--period <secs>` (default behavior set in start.rs; floor 60s, echo_fire.rs:170); `pulse_psyche` gate decides whether cadence wakes resume the Psyche LLM vs. only fire the echo-commune gate (wrapper/mod.rs:1078-1085).
- Echo-commune gate: 15-min `.more-done` sentinel-age window, hardcoded (echo_fire.rs:66, `ECHO_COMMUNE_WINDOW`).
- 24h daily refresh forces a fresh init re-injecting psyche.md (wrapper/mod.rs:1179-1192).

### Wrapper spawn — `start.rs:444-520`
Detached, `bInheritHandles=FALSE` + `CREATE_BREAKAWAY_FROM_JOB` on Windows (`win_spawn::spawn_detached_no_inherit`), `setsid()` on Unix. *Harness-specific reason:* avoids inheriting Claude Code's Bash-tool pipe handles / job object, which otherwise `TerminateProcess` the wrapper after ~5 min (start.rs:435-443).

### Orphan handling — `wrapper/orphan.rs`, `wrapper/mod.rs:1145-1148`
The wrapper polls its Self listener's liveness (parent_pid / claude.exe ancestor walk via Toolhelp32, types.rs:197-313); after a grace period with Self gone, breaks the loop. INIT_SIGNOFF envelope is the primary exit; orphan check is the backstop.

*Manifest requirements:* full subprocess spec per role (init/resume/final/haiku/recovery) — binary name, model, fallback-model, effort, tool allowlist, permission flag, slash-command flag, output-format, session-resume mechanism, `--agents`/`--agent`/`--name` system-prompt injection, `--add-dir` grants, env injects/removes, working dir, detachment flags, and the recursion-guard env var.

---

## 4. Conversation-History (Transcript) Access (`src/owl/echo_commune.rs`, `src/common/owlery.rs`)

Echo-commune and resume briefs read **Claude Code session transcript jsonl files**:
- Path = `claude_projects_root() / encode_project_path(wrapper_cwd) / {session_uuid}.jsonl` (echo_commune.rs:1070-1078).
- `claude_projects_root()` = `$CLAUDE_CONFIG_DIR/projects` or `$HOME|$USERPROFILE/.claude/projects` (owlery.rs:439-467). *Harness-specific:* `.claude/projects` layout + `CLAUDE_CONFIG_DIR` honoring (ccs support).
- `encode_project_path`: every non-`[A-Za-z0-9-]` char → `-` (the Claude-Code dir-name encoding, reverse-engineered) (owlery.rs:410-419).
- Excerpt extraction: filters jsonl entries by timestamp > `.last-commune-epoch` cursor and **by entry type** — keeps user text + assistant text/thinking, drops `tool_use`/`tool_result`/`system`; 64KB tail cap (echo_commune.rs lead comment D-07..D-11, EXCERPT_BYTE_CAP=64KB at :964).

*Manifest requirements:* transcript root path template, the project-path encoding function, transcript filename pattern (`{session_uuid}.jsonl`), and the JSONL entry schema (`type`, `message.content[].type=="text"`, `result`, `session_id`) — all of which a Codex harness would shape differently.

---

## 5. Context Model & Git Repo Structure

### live-context vs project-context split (`echo_commune.rs`, `common/envelope.rs`)
Two-slice envelope: `<live-context>` (agent-personal) and `<project-context>` (project-bound), routed independently. Project name resolved from the perch's `info.json.cwd` via a strict git-ancestor resolver (echo_commune.rs:916-959, `build_current_context_blocks`). Precedence markers `<!-- spt:source=direct|llm spt:routed_at=ISO -->` guard against stale LLM overwrites within a window (echo_commune.rs:287-450).

### tracked git repo (`src/common/tracked.rs:1-39`)
`$SPT_HOME/psyches/tracked/`:
```
seed/             bare git repo
agents/{id}/      linked worktree, branch a-{id}    → live_context.md, memformat.xml, sessions.log
projects/{name}/  linked worktree, branch p-{name}  → {self_id}.md
```
Shells out to system `git` (D-01, no git2-rs); branch naming `a-{agent_id}`/`p-{project_name}`; bootstrap via `commit-tree`/`update-ref`; commits carry `-c user.name=spt-bootstrap -c user.email=spt@local`; Windows `core.autocrlf=false`. *Harness-agnostic* but tied to SPT's own storage, not Claude.

### Commune/signoff file-drop flow (`src/owl/poll.rs`, `commune` SKILL.md)
The skills write a **file drop** that the Self listener watches:
- Watched dir: `<cwd>/.claude/`. Files: `{self_id}-commune.md` and `{self_id}-signoff.md` (poll.rs:1292-1304 `scan_drop_files`, commune SKILL.md:27-39).
- On each poll iteration the listener stats those files; on hit composes `<EVENT type="file_drop" kind="commune|signoff" path="<abs>" from="<id>">` and delivers to `{self_id}-psyche` (poll.rs:407-457, 1320-1323).
- Wrapper reads the file, routes the two-slice body, resumes the Psyche, and **deletes on exit-code 0** / retains on error (wrapper/mod.rs:295-460; SKILL.md:36-39 last-write-wins).
- *Harness-specific:* `.claude/` is chosen because Write auto-creates it and the skill uses the Claude **Write tool** (no shell escaping). A Codex harness would pick a different watched dir / tool.

---

## 6. Environment Variables (grep across `src/`)

**Injected (written to `CLAUDE_ENV_FILE` / settings.json / child env):**
- `OWL` = path to owl.exe; `LIVE` = `"<owl.exe> live"` — written to `CLAUDE_ENV_FILE` by SessionStart and mirrored into `~/.claude/settings.json` `env` (plugin_session_start.rs:502-524, 543-617). Also passed to wrapper/spine children (start.rs:465-466, boot_spine.rs:83-84).
- `OWL_SESSION_ID` = Claude `session_id` from hook stdin, written to `CLAUDE_ENV_FILE` (plugin_session_start.rs:464-498).
- `OWL_ECHO_COMMUNE=1` — recursion guard set on the haiku child (echo_commune.rs:995).
- `GIT_AUTHOR_*`/`GIT_COMMITTER_*` on tracked commits (tracked.rs:277-284).

**Read (gates / resolution):**
- `OWL_SESSION_ID` — perch resolution in every hook (hook_check.rs:23, hook_idle.rs:34, hook_prompt.rs:26, hook_subagent_start.rs:45, cleanup.rs:14, poll.rs:177, ring.rs:63).
- `CLAUDE_ENV_FILE` — the plugin env-injection channel (plugin_session_start.rs:487,509,545,722). *Claude-Code-specific.*
- `CLAUDE_PLUGIN_ROOT` — binary location (main.rs:31, version_changelog.rs:262). *Claude-Code-specific.*
- `CLAUDE_CONFIG_DIR` — relocates transcript root for ccs (owlery.rs:440). *Claude-Code-specific.*
- `CLAUDE_CODE_TEAM_NAME` — skips re-orientation/resume for team-member sessions (plugin_session_start.rs:46,321; resume.rs:22). *Claude-Code-specific.*
- `OWL_ECHO_COMMUNE`, `OWL_HANDOFF_CHILD`, `SPT_TRAMPOLINE_GUARD`, `OWL_SKIP_RESUME` — auto-pick/resume gates (plugin_session_start.rs:42-46; resume.rs:27; primer.rs:74).
- `SPT_HOME` — runtime root override (owlery.rs:19; pervasive). `LOCALAPPDATA`/`USERPROFILE`/`HOME` — platform default root (owlery.rs:37-49).
- `SPT_TRACE`, `SPT_OVERRIDE_PKG_VERSION` — diagnostics/version override (trace.rs:13, version_changelog.rs:69).
- `OWL_UNDER_WRAPPER`, `SPT_WRAPPER_HANDOFF` — wrapper handoff signaling (wrapper_state.rs:5, wrapper/mod.rs).

*Manifest requirement:* a declared env-var table with direction (inject/read), value template, and consumer — and which env file/channel the harness uses to surface them.

---

## 7. Config / State Files

- **Perch `info.json`** (`common/types.rs:90-131`): `owl_id, started, mode, pid (numeric|"BUSY"), session_id, state (listener|capsule|live|psyche|spine|touch|working), parent_pid?, parent_id?, agent_id?, cwd?, project_history[]`. The `session_id` field holds the **Claude session UUID** (harness-coupled). Layout: flat `owlery/<id>/` or nested `owlery/<self>/nested/<psyche>/`.
- **`wrapper-state.json`** (`common/wrapper_state.rs:35-50`): `session_uuid, gen, last_fresh_launch_epoch, pulse_psyche`. Carries the Claude session UUID across wrapper handoffs; written atomically (tmp+rename), delete-on-load.
- **`status/{self_id}.json`** (`context.rs:62-86`): `generation, session_name, last_started`.
- **`TrackedAgentInfo` info.json** (`types.rs:76-87`): `agent_id, last_started, last_machine_name, last_project_name?, machine_history[], project_history[]` (per-agent cross-session index at `psyches/tracked/agents/{id}/info.json`).
- **`sessions.log`** (per-agent, in tracked worktree): JSONL rows `{trigger: boot|pulse|commune|signoff, session_uuid, ts}`, sealed/rotated per generation via git (claude.rs:414-428).
- **Sentinels (filesystem)**: `.more-done` (Stop→wrapper echo gate), `.idle-ready`, `.has-messages`, `.owl-aware`, `.last-commune-epoch` cursor, `.orphaned_at`, wrapper pid file (hook_idle.rs, owlery.rs:390, inbox.rs).
- **settings.json sync**: rewrites `~/.claude/settings.json` `env.OWL`/`env.LIVE` on drift (plugin_session_start.rs:543-617). *Claude-Code-specific.*
- **Spool / registry**: file-backed message spool under owlery (`common/spool.rs`, `registry.rs`).
- **Route-guard window**: `route_guard_window_secs` config knob (echo_commune.rs:398).
- `last-seen-version.json` (first-install primer gate), `config.json` knobs read via owlery helpers.

---

## 8. Other Harness-Specific Items

- **Model names / billing flags:** Psyche = `sonnet` (fallback `opus`), echo-commune summarizer = `haiku`; `--effort medium/low`; `--dangerously-skip-permissions`, `--disable-slash-commands`, `--no-session-persistence` (omitted) (claude.rs:64-78, echo_commune.rs:996-1022). All Claude-CLI flags.
- **Version trampoline** (`plugin_session_start.rs:666-773`): scans sibling plugin-cache version dirs `.../spt/<semver>/owl.exe`, rewrites `CLAUDE_ENV_FILE`, backfills binary + hooks.json into the old version dir. Coupled to Claude Code's plugin-cache layout and `.orphaned_at` GC marker.
- **Activity / idle detection:** driven entirely by hook events — `Stop` = "turn ended" (writes `.more-done`/`.idle-ready`), `PreToolUse`/`UserPromptSubmit` = "busy" (clears `.idle-ready`). The 15-min echo gate measures `.more-done` sentinel age (echo_fire.rs:96-120). No process-CPU idle detection.
- **Message injection mechanism:** spooled messages are injected only via hook `hookSpecificOutput.additionalContext` as `<owl_messages>` / `<owl_working_perch>` / `<spacetime-reorientation>` / `<spt-live-auto-pick>` XML blocks (hook_check.rs:120, hook_output.rs, plugin_session_start.rs:139-155). Stop-hook can't inject (no schema) → sentinel architecture. TCP "wake" + capsule `--- [INCOMING]` sendkeys is the secondary push channel (hook_prompt.rs:13-20).
- **Parent-pid resolution** walks the process tree for a `claude.exe`/`claude` ancestor (types.rs:194-313) — the fallback identity resolver when `OWL_SESSION_ID` is absent. Harness-specific process name.
- **AskUserQuestion picker spec** (`live pick-spec`, `pick_spec.rs`) and the auto-pick block emission target Claude's AskUserQuestion tool.
- **`compose_llm_prompt_from_envelope`** (wrapper/mod.rs:863-931): translates `<EVENT>` wire envelopes into natural-language prompts before the LLM sees them — a harness-neutral seam, but the prompt-shaping assumes a single stdin prompt channel.

---

### Manifest schema checklist (synthesized)
A runtime manifest must be able to declare, per harness: (1) **hook event table** (event name → command → consumed stdin fields → output channel + that channel's capabilities, incl. the "can/can't inject" distinction); (2) **subprocess role specs** (init/resume/final/summarizer/recovery: binary, model+fallback, effort, tool allowlist, permission & slash-command flags, output-format, session-resume flag, system-prompt-injection mechanism, dir grants, env inject/remove, cwd, detachment flags, recursion guard); (3) **transcript access** (root path template, project-path encoding, filename pattern, JSONL entry schema + type filter, cursor); (4) **env-var table** (direction, template, channel); (5) **identity model** (session-id source, parent-process ancestor name, perch info.json schema); (6) **state-file schemas** (info.json, wrapper-state.json, status, sessions.log, sentinels); (7) **file-drop convention** (watched dir, filename pattern, write tool, consume/delete semantics); (8) **config knobs** (period, echo-window, route-guard window, 24h refresh); (9) **plugin-cache/version-trampoline layout**; (10) **injection block vocabulary** (the XML tags surfaced to the agent).

Key load-bearing file paths: `src/cli.rs`, `plugin/spt/hooks/hooks.json`, `src/live/wrapper/claude.rs`, `src/owl/echo_commune.rs`, `src/owl/plugin_session_start.rs`, `src/owl/hook_idle.rs`, `src/common/types.rs`, `src/common/wrapper_state.rs`, `src/common/owlery.rs`, `src/common/tracked.rs`, `src/owl/poll.rs`.</result>
<usage><subagent_tokens>213494</subagent_tokens><tool_uses>31</tool_uses><duration_ms>188072</duration_ms></usage>
</task-notification>


<owl_messages>
<owl_message from="doyle-w327" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: doyle-w327
Agent type: general-purpose
To message it: $OWL send doyle-w327 doyle</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.

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


Extraction's back — comprehensive (213k tokens of sister-project surface). The headline finding for schema design: **the sharpest harness-varying fact is per-hook output capability** — CC's Stop hook *can't* inject (no `hookSpecificOutput` schema), which is the single reason the whole `.more-done` sentinel + wrapper-poll architecture exists. The manifest must express that per event.

Before I write `docs/MANIFEST.md`, one **design decision dominates** — the **manifest-vs-core boundary**. The extraction lists ~10 categories of CC-specific behavior, but a lot of it is *spt-core's internal mechanism*, not adapter config. The manifest should declare only what genuinely **varies per harness**. My proposed split:

**In the manifest (varies per harness):**
1. **Header** — `adapter_name`, version, `min_spt_core_version`, hostable endpoint types.
2. **Hook table** — per harness-event: the `api` subcommand it fires, which stdin fields it reads, and **its output capability** (can-inject-context? yes/no → if no, spt-core falls back to sentinel/relay). ← the load-bearing finding.
3. **Session role templates** — per role (psyche-init, psyche-resume, echo-summarizer, signoff, recovery): command template with substitution keys, model+fallback, flags, tool allowlist, output-format, resume mechanism, system-prompt-injection method, dir grants, env inject/remove, cwd, detachment flags, **recursion-guard env var**, and an optional **fallback-binary chain** (the real `claude → ccs → wait` 3-tier).
4. **Env-var table** — direction (inject/read), value template, and the harness's **injection channel** (e.g. `CLAUDE_ENV_FILE`).
5. **History access** (Path A) — transcript root template, project-path encoding, filename pattern, JSONL entry schema + type-filter — *or* a flag electing Path B (native store).
6. **Injection methods** — inject-input method(s) per activity-state + the **block vocabulary** (the XML tags surfaced to the agent: `<owl_messages>` etc.).
7. **File-drop** — watched dir(s), filename patterns, the write mechanism (Write tool vs shell).
8. **Identity** — session-id source, parent-process ancestor name (`claude.exe`), the post-spawn/UUID choice.
9. **Adapter-update** — file-pull / delegated-command (+ the plugin-cache layout it needs).

**NOT in the manifest (spt-core-internal — adapters never configure these):**
- Sentinels (`.more-done`, `.idle-ready`) — managed by `api state`/`api echo-gate`; the adapter just *calls* them.
- The spool/registry/perch `info.json`/`wrapper-state` schemas — spt-core owns these.
- The version-trampoline *mechanism*, settings.json drift-sync, parent-pid ancestor-walk *logic* — core behaviors (the manifest only supplies the harness-specific *names/paths* they need, e.g. the ancestor process name).
- Config knobs (period, echo-window, route-guard, 24h refresh) — these are **spt-core settings**, not harness-specific. (Period is already a `start` flag.) Unless you want any of them adapter-overridable?

**The key question:** does that manifest-vs-core boundary match your intent — specifically, are the **config knobs** (period, echo-window, route-guard, refresh) spt-core-global settings (my rec), or should an adapter be able to override them? And anything I've put "in core" that you think an adapter must control?

Once you confirm the boundary, I'll write `docs/MANIFEST.md` — the full TOML schema **plus a worked `claude-spt` example** showing today's exact behavior (the `sonnet`/`haiku` invocations, the 7 hooks, the `.claude/` file-drop, the transcript path) expressed in it.