**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):
A **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), and each always-on endpoint created from that option represents **another channel that one binary serves** — it binds via the existing `api bind`, so one connection fronts many `#`-endpoints. It is the **second class of third-party binary spt-core boot-launches** (the shell wake-watcher was the first); supervision **reuses the wake-watcher scaffolding** (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) **minus the offline-only flip** — always online, never resting (no dormant/suspended states). Addressed with a mandatory leading [[`#` always-on sigil]].
_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); "service" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).

The endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set.

Presence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.

+**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21 — core kind, first instance downstream `spt-discord`):

+A **resident, addressable endpoint that hosts no mind.** Its binary is daemon-**supervised continuously and runs independent of any agent's liveness** (up even when zero agents are online) — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Declared by an adapter's **`[always-on]`** manifest section; the supervised binary is **one per adapter-option** (`<adapter>[:profile]`), and each always-on endpoint created from that option represents **another channel that one binary serves** — it binds via the existing `api bind`, so one connection fronts many `#`-endpoints. It is the **second class of third-party binary spt-core boot-launches** (the shell wake-watcher was the first); supervision **reuses the wake-watcher scaffolding** (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) **minus the offline-only flip** — always online, never resting (no dormant/suspended states). Addressed with a mandatory leading [[`#` always-on sigil]].

+_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); "service" alone (it is addressable, not faceless); a sleep/wake resting model (it does not rest).

The endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.


Both terms + their cross-refs are in CONTEXT.md. One last core contour before the ADR — the lifecycle trigger and who creates a `#`-endpoint (this sets the ADR's lifecycle section and whether core needs a new create command):


AskUserQuestion

Question: When does the supervised binary run, and who creates/removes the `#`-endpoints it serves?

Choices:
Register-triggered + binary self-manages via api bind
Declaring [always-on] in a registered adapter-option = daemon keeps the binary resident (runs even with zero #-endpoints, so it's ready to receive the first channel join). The binary creates/removes #-endpoints itself via existing api bind / teardown. Most minimal core: NO new 'create #-endpoint' command. Cost: an idle bot connection when nothing's bridged (cheap for Discord).
registered + [always-on] -> binary resident
binary -> api bind #general (self-managed)
      -> api bind #dev
core: supervise only, key=adapter-option
NO new create command
idle bot ok
Binary starts when the option's FIRST #-endpoint is created, stops when its LAST is removed. Needs a core 'create/remove #-endpoint' command (the user/agent chooses which channels to bridge). No idle bot, but adds a core command + couples binary lifecycle to endpoint count.
spt endpoint create #general --from spt-discord:work
  -> first endpoint: start binary
remove last #-endpoint -> stop binary
core: + create/remove command
no idle bot
Read whether api bind + the perch teardown path (api session-end / endpoint purge) already support a non-shell, non-agent binary creating + removing its own addressable perches, before fixing the lifecycle model.
read: api bind (create perch)
  api session-end / purge (teardown)
can a resident binary own N perches
  + create/remove them itself?
then fix lifecycle


Your questions have been answered: "When does the supervised binary run, and who creates/removes the `#`-endpoints it serves?"="Register-triggered + binary self-manages via api bind" selected preview:
registered + [always-on] -> binary resident
binary -> api bind #general (self-managed)
      -> api bind #dev
core: supervise only, key=adapter-option
NO new create command
idle bot ok. You can now continue with these answers in mind.

Question: When does the supervised binary run, and who creates/removes the `#`-endpoints it serves?

Choices:
Register-triggered + binary self-manages via api bind
Declaring [always-on] in a registered adapter-option = daemon keeps the binary resident (runs even with zero #-endpoints, so it's ready to receive the first channel join). The binary creates/removes #-endpoints itself via existing api bind / teardown. Most minimal core: NO new 'create #-endpoint' command. Cost: an idle bot connection when nothing's bridged (cheap for Discord).
registered + [always-on] -> binary resident
binary -> api bind #general (self-managed)
      -> api bind #dev
core: supervise only, key=adapter-option
NO new create command
idle bot ok
Binary starts when the option's FIRST #-endpoint is created, stops when its LAST is removed. Needs a core 'create/remove #-endpoint' command (the user/agent chooses which channels to bridge). No idle bot, but adds a core command + couples binary lifecycle to endpoint count.
spt endpoint create #general --from spt-discord:work
  -> first endpoint: start binary
remove last #-endpoint -> stop binary
core: + create/remove command
no idle bot
Read whether api bind + the perch teardown path (api session-end / endpoint purge) already support a non-shell, non-agent binary creating + removing its own addressable perches, before fixing the lifecycle model.
read: api bind (create perch)
  api session-end / purge (teardown)
can a resident binary own N perches
  + create/remove them itself?
then fix lifecycle

Register-triggered + binary self-manages via api bind


yes we can switch. note that `spt-discord` info/docs should go in a new sister directory `../spt-discord/`. only information directly relevant to spt-core should go here.


Organize spt-discord documentation into sister directory


Locked — register-triggered, binary self-manages via `api bind`, no new create command. Full core design resolved. All three ADR triggers met (hard-to-reverse: new endpoint kind + sigil amending a hazard charset; surprising: "why `#`/why a 2nd boot-launched class"; real trade-off: vs generalize-watcher / always-online-shell / faceless-service). Writing the ADR + REQ. Let me get the formats + next ADR number.


1	# ADR Format
2
3	ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
4
5	Create the `docs/adr/` directory lazily — only when the first ADR is needed.
6
7	## Template
8
9	```md
10	# {Short title of the decision}
11
12	{1-3 sentences: what's the context, what did we decide, and why.}
13	```
14
15	That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
16
17	## Optional sections
18
19	Only include these when they add genuine value. Most ADRs won't need them.
20
21	- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
22	- **Considered Options** — only when the rejected alternatives are worth remembering
23	- **Consequences** — only when non-obvious downstream effects need to be called out
24
25	## Numbering
26
27	Scan `docs/adr/` for the highest existing number and increment by one.
28
29	## When to offer an ADR
30
31	All three of these must be true:
32
33	1. **Hard to reverse** — the cost of changing your mind later is meaningful
34	2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
35	3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
36
37	If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
38
39	### What qualifies
40
41	- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
42	- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
43	- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
44	- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
45	- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
46	- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
47	- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
48

# ADR Format

ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.

Create the `docs/adr/` directory lazily — only when the first ADR is needed.

## Template

```md
# {Short title of the decision}

{1-3 sentences: what's the context, what did we decide, and why.}
```

That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.

## Optional sections

Only include these when they add genuine value. Most ADRs won't need them.

- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
- **Considered Options** — only when the rejected alternatives are worth remembering
- **Consequences** — only when non-obvious downstream effects need to be called out

## Numbering

Scan `docs/adr/` for the highest existing number and increment by one.

## When to offer an ADR

All three of these must be true:

1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons

If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."

### What qualifies

- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.


docs\adr\0001-clean-room-fork-from-claude-skill-owl.md
docs\adr\0002-bake-networking-into-core-collapse-spt-node.md
docs\adr\0003-multi-instance-node-anchored-identity-model.md
docs\adr\0004-single-daemon-broker-brain-split-and-self-update.md
docs\adr\0005-totp-seeded-spake2-node-pairing.md
docs\adr\0006-multi-subnet-membership-model.md
docs\adr\0007-subnet-notification-primitive.md
docs\adr\0010-immutable-home-subnet-fork-not-rehome.md
docs\adr\0011-branchstore-git-kv-state-substrate.md
docs\adr\0013-psyche-reconciled-context-merge.md
docs\adr\0014-publish-target-repo-and-lapse-proof-canonical-url.md
docs\adr\0015-release-signing-two-key-trust-anchor.md
docs\adr\0016-platform-targeted-update-sets.md
docs\adr\0017-subnet-mesh-membership-seed-proof.md
docs\adr\0018-broker-brain-process-isolation-restoration.md
docs\adr\0008-live-activity-buffer-pty-digest.md
docs\adr\0019-digest-adapter-declared-extractor-session-spanning.md
docs\adr\0009-endpoint-access-control-model.md
docs\adr\0012-psyche-trust-boundary-stdout-event-outbound.md
docs\adr\0020-event-envelope-sole-arriving-format-reply-to-removed.md
docs\adr\0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md


19-# reqs add "int". Until a req is activated, its per-req `required_stages = []`
20-# keeps it out of the coverage gate.
21:required_stages = []
22-
23-# ───────────────────────────── Architecture / workspace ─────────────────────
24:[[requirements]]
25:id = "REQ-ARCH-1"
26-title = "Many small acyclically-layered crates"
27:required_stages = ["impl"]  # activated M0: spt-proto<-spt-store one-way layering (T14). impl-only: architecture is build-evidenced (cargo rejects cycles); grows as crates are added M1+.
28:[[requirements]]
29:id = "REQ-ARCH-2"
30-title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
31:required_stages = ["impl"]
32:[[requirements]]
33:id = "REQ-ARCH-3"
34-title = "Wire-protocol version independent of crate semver, N-1 compat window"
35:required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
36:[[requirements]]
37:id = "REQ-ARCH-4"
38-title = "Copy-verbatim the commodity layer from the sister project"
39:required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)
40-
41-# ───────────────────────────── Daemon ───────────────────────────────────────
42:[[requirements]]
43:id = "REQ-DAEMON-1"
44-title = "One per-machine spt-daemon owning all per-machine state"
45:required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) drives the consolidated daemon-hosted lifecycle through the real IPC
46:[[requirements]]
47:id = "REQ-DAEMON-2"
48-title = "Broker/brain split for seamless self-update"
49:[Omitted long matching line]
50:[[requirements]]
51:id = "REQ-DAEMON-3"
52-title = "Any api invocation auto-starts the daemon if absent"
53:required_stages = ["impl", "unit", "int"]
54:[[requirements]]
55:id = "REQ-DAEMON-4"
56-title = "Honor every KNOWN-HAZARDS invariant"
57:required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves graceful signoff runs the echo-before-teardown ordering (3.3) and removes the Self's ready marker in the consolidated daemon
58-

[Showing results with pagination = limit: 40]


Next ADR = **0023**. Note: the `#` sigil does **not** weaken `REQ-HAZARD-ID-CHARSET` — it sits *above* the id validator (the address parser strips the leading `#` before validating the bare id, which stays charset-clean). So no hazard amendment; just a new addressing layer. Let me find the REQ homes + file tail.


Grep

^# ─+ .+ ─+$|REQ-MANIFEST-8|REQ-SHELL-|REQ-EP-|REQ-INST-10|HAZARD-ID-CHARSET


96:id = "REQ-MANIFEST-8"
173:[Omitted long matching line]
178:id = "REQ-EP-1"
182:id = "REQ-EP-2"
186:id = "REQ-EP-3"
190:id = "REQ-EP-4"
194:id = "REQ-EP-5"
198:id = "REQ-EP-6"
202:id = "REQ-EP-7"
234:[Omitted long matching line]
243:[Omitted long matching line]
245:id = "REQ-INST-10"
785:id = "REQ-HAZARD-ID-CHARSET"
967:id = "REQ-SHELL-1"
971:id = "REQ-SHELL-2"
1117:# each wave lands evidence. Planned activations: REQ-SHELL-5 doc/impl at T0 (the
1118:# ownership audit — proof-not-change), REQ-CONSENT-3 at W1, REQ-SHELL-3 at W2,
1119:# REQ-SHELL-4 at W3, REQ-SHELL-5 unit/int at the W4 Gateway-owner capstone.) ---
1121:id = "REQ-SHELL-3"
1125:id = "REQ-SHELL-4"
1133:id = "REQ-SHELL-5"


171	[[requirements]]
172	id = "REQ-START-5"
173	title = "Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure \"a harness session exists at this pid\" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start <id>` → `$SPT listen <id>` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0)"
174	required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W1-W2 (LiveAgent/listen): doc = CONTEXT.md §171/§177-181 (adapter-agnostic seed + bind-time resolution) + docs-site harness-contract/api.md + docs/MANIFEST.md SessionStart hook. impl = adapter-agnostic Seed (seed.rs drops adapter) + cmd_seed + --adapter Option across the api group (api/mod.rs ApiArgs/Ctx/resolve_ctx_manifest) + bind-time resolution in cmd_listen (explicit override else resolve::resolve_bind_adapter) feeding both the info.json adapter stamp (bind_from_seed) and the live_capable manifest. unit = adapter_is_optional (api/mod) + resolve_from_basename/pointer tests (spt-runtime resolve). int (agnostic-seed listen bringup E2E) activates at the final LiveAgent wave. RESIDUAL (doyle ruling C, 2026-06-16): the ReadyAgent/`$SPT ready` path is a STACKED follow-on wave — Cmd::Ready bypasses seeds today, so its seed/resolve parity (CONTEXT §175 end-state intent) is staged separately (activate ready-int when built). api poll stays unchanged (operates on an already-stamped perch, no pid-resolution).
175
176	# ───────────────────────────── Endpoints / Shells ───────────────────────────
177	[[requirements]]
178	id = "REQ-EP-1"
179	title = "Day-one endpoint types; open type system"
180	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
181	[[requirements]]
182	id = "REQ-EP-2"
183	title = "Agent endpoints vs Shells distinction in the type model"
184	required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
185	[[requirements]]
186	id = "REQ-EP-3"
187	title = "Messaging payloads carry typed operation commands + file blobs"
188	required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
189	[[requirements]]
190	id = "REQ-EP-4"
191	title = "PresenceChannel broker endpoint (seam day-one)"
192	required_stages = ["impl", "unit"]  # activated M4-D4c: the broker-owned PresenceLog — a seq'd ring of {connected|disconnected, conn_id, remote_id_hex} events mirroring the StreamLog discipline. register_conn appends connected + spawns a closed-watcher (conn.closed() → conn row removed → disconnected appended), so the conn table finally reflects liveness; the brain subscribes via net-presence-subscribe {from_seq} and consumes net-presence-event under the same contiguous/dedup/gap cursor discipline as streams (brain-restart cursor-resume). The full PresenceChannel *endpoint* (agent-visible channel) builds on this broker seam later; int = two-host at D9.
193	[[requirements]]
194	id = "REQ-EP-5"
195	title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
196	required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
197	[[requirements]]
198	id = "REQ-EP-6"
199	title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6"
200	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)
201	[[requirements]]
202	id = "REQ-EP-7"
203	title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"
204	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)
205
206	# ───────────────────────────── Instances ────────────────────────────────────
207	[[requirements]]
208	id = "REQ-INST-1"
209	title = "endpoint ID vs instance split (adapter-agnostic ID)"
210	required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
211	[[requirements]]
212	id = "REQ-INST-2"
213	title = "Per-node files, synced Psyche mind"
214	required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
215	[[requirements]]
216	id = "REQ-INST-3"
217	title = "Dormant (warm) / suspended (cold) resting states"
218	required_stages = ["doc", "impl", "unit"]  # doc activated M4-D9-3: docs/DORMANCY-BUDGET.md — the measured warm/cold policy lock (ADR-0003 #9 closed: warm default confirmed, zero idle CPU, RSS-only cost; auto-suspend opt-in default OFF). impl/unit activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md
219	[[requirements]]
220	id = "REQ-INST-4"
221	title = "active to dormant/suspended fires a transition echo commune"
222	required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant
223	[[requirements]]
224	id = "REQ-INST-5"
225	title = "Two-tier context sync (live to all, project to same-project)"
226	required_stages = ["impl", "unit", "int"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both directions bootstrap-pull a mind the puller never held, registry-derived want-refs, gate served remotely, HFENDULEAM↔gravity). unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
227	[[requirements]]
228	id = "REQ-INST-6"
229	title = "Deferred messages not delivered to dormant/suspended instances"
230	required_stages = ["impl", "unit", "int"]  # activated M5-D5a/b: the gate is state-keyed at the hook-channel consumer sites — resting::deferred_held reads the owner's durable rest record at drain time; "held" = (deferred=1) ∧ (Dormant|Suspended); poll_drain + cmd_worker_poll gated (the KH 1.4 "all sites agree" sweep — shellchan peek ungated: no deferred writer targets shell perches; ring's ephemeral perch recordless by construction = legacy fallback). Release is by construction at the wake edge: the persisted Active state stops the narrowing, the unchanged delivered-mark discipline keeps it exactly-once. D5b adds the remote arm: StreamFamily::Rest + resthost request/serve (remote-drive trust class — access_check on the handshake-proven origin, no grant gate; refusal = finish-no-reply), cmd_rest qualified-form lift via wansend::wan_rest. unit = held-while-dormant+suspended / non-deferred-untouched / released-once-on-wake / recordless-flows + wire-record roundtrip + demux. int = loopback two-broker E2E (suspend cross-node, gate holds, replay NO_EDGE, wake cross-node releases exactly-once + wake effects, whitelist negative). doc = DEFERRED.md remote-fork note. Rig [twohost] leg waits for D9a.
231	[[requirements]]
232	id = "REQ-INST-7"
233	title = "Subnet registry + bare-id resolution policy"
234	required_stages = ["impl", "unit", "int"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10). Cross-node replication delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both sides' perch rows replicate into the other's gated registry over the real wire). unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
235	[[requirements]]
236	id = "REQ-INST-8"
237	title = "Remote-control mode distinct from local operation"
238	required_stages = ["impl", "unit", "int"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — A types into B's live PTY across the rig and reads the echo back)
239	# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
240	[[requirements]]
241	id = "REQ-INST-9"
242	title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
243	required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
244	[[requirements]]
245	id = "REQ-INST-10"
246	title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
247	required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live nodes [AcrossNodes, force @node] / multiple subnets [AcrossSubnets, force subnet:]; Offline never routed). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin/cross-subnet. Cross-node replication feeding the registry = D4; two-host = D9.
248	[[requirements]]
249	id = "REQ-INST-11"
250	title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"

[[requirements]]
id = "REQ-START-5"
title = "Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure \"a harness session exists at this pid\" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start <id>` → `$SPT listen <id>` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0)"
required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W1-W2 (LiveAgent/listen): doc = CONTEXT.md §171/§177-181 (adapter-agnostic seed + bind-time resolution) + docs-site harness-contract/api.md + docs/MANIFEST.md SessionStart hook. impl = adapter-agnostic Seed (seed.rs drops adapter) + cmd_seed + --adapter Option across the api group (api/mod.rs ApiArgs/Ctx/resolve_ctx_manifest) + bind-time resolution in cmd_listen (explicit override else resolve::resolve_bind_adapter) feeding both the info.json adapter stamp (bind_from_seed) and the live_capable manifest. unit = adapter_is_optional (api/mod) + resolve_from_basename/pointer tests (spt-runtime resolve). int (agnostic-seed listen bringup E2E) activates at the final LiveAgent wave. RESIDUAL (doyle ruling C, 2026-06-16): the ReadyAgent/`$SPT ready` path is a STACKED follow-on wave — Cmd::Ready bypasses seeds today, so its seed/resolve parity (CONTEXT §175 end-state intent) is staged separately (activate ready-int when built). api poll stays unchanged (operates on an already-stamped perch, no pid-resolution).

# ───────────────────────────── Endpoints / Shells ───────────────────────────
[[requirements]]
id = "REQ-EP-1"
title = "Day-one endpoint types; open type system"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-2"
title = "Agent endpoints vs Shells distinction in the type model"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-3"
title = "Messaging payloads carry typed operation commands + file blobs"
required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
[[requirements]]
id = "REQ-EP-4"
title = "PresenceChannel broker endpoint (seam day-one)"
required_stages = ["impl", "unit"]  # activated M4-D4c: the broker-owned PresenceLog — a seq'd ring of {connected|disconnected, conn_id, remote_id_hex} events mirroring the StreamLog discipline. register_conn appends connected + spawns a closed-watcher (conn.closed() → conn row removed → disconnected appended), so the conn table finally reflects liveness; the brain subscribes via net-presence-subscribe {from_seq} and consumes net-presence-event under the same contiguous/dedup/gap cursor discipline as streams (brain-restart cursor-resume). The full PresenceChannel *endpoint* (agent-visible channel) builds on this broker seam later; int = two-host at D9.
[[requirements]]
id = "REQ-EP-5"
title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
[[requirements]]
id = "REQ-EP-6"
title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)
[[requirements]]
id = "REQ-EP-7"
title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["doc", "impl", "unit"]  # doc activated M4-D9-3: docs/DORMANCY-BUDGET.md — the measured warm/cold policy lock (ADR-0003 #9 closed: warm default confirmed, zero idle CPU, RSS-only cost; auto-suspend opt-in default OFF). impl/unit activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = ["impl", "unit", "int"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both directions bootstrap-pull a mind the puller never held, registry-derived want-refs, gate served remotely, HFENDULEAM↔gravity). unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = ["impl", "unit", "int"]  # activated M5-D5a/b: the gate is state-keyed at the hook-channel consumer sites — resting::deferred_held reads the owner's durable rest record at drain time; "held" = (deferred=1) ∧ (Dormant|Suspended); poll_drain + cmd_worker_poll gated (the KH 1.4 "all sites agree" sweep — shellchan peek ungated: no deferred writer targets shell perches; ring's ephemeral perch recordless by construction = legacy fallback). Release is by construction at the wake edge: the persisted Active state stops the narrowing, the unchanged delivered-mark discipline keeps it exactly-once. D5b adds the remote arm: StreamFamily::Rest + resthost request/serve (remote-drive trust class — access_check on the handshake-proven origin, no grant gate; refusal = finish-no-reply), cmd_rest qualified-form lift via wansend::wan_rest. unit = held-while-dormant+suspended / non-deferred-untouched / released-once-on-wake / recordless-flows + wire-record roundtrip + demux. int = loopback two-broker E2E (suspend cross-node, gate holds, replay NO_EDGE, wake cross-node releases exactly-once + wake effects, whitelist negative). doc = DEFERRED.md remote-fork note. Rig [twohost] leg waits for D9a.
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = ["impl", "unit", "int"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10). Cross-node replication delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both sides' perch rows replicate into the other's gated registry over the real wire). unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = ["impl", "unit", "int"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — A types into B's live PTY across the rig and reads the echo back)
# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-INST-9"
title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
[[requirements]]
id = "REQ-INST-10"
title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live nodes [AcrossNodes, force @node] / multiple subnets [AcrossSubnets, force subnet:]; Offline never routed). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin/cross-subnet. Cross-node replication feeding the registry = D4; two-host = D9.
[[requirements]]
id = "REQ-INST-11"
title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"


Found the homes: new endpoint REQs go after `REQ-EP-7`; the `#` sigil extends `REQ-INST-10`'s grammar. Writing ADR 0023 + registering `REQ-EP-8`/`REQ-EP-9` (inactive per rule 5 — design ratified, unbuilt).


Edit

M9-WAVE2-ROLE-WRITERS.md survey)

# ───────────────────────────── Instances ────────────────────────────────────

M9-WAVE2-ROLE-WRITERS.md survey)
# ── Always-on endpoints (ADR-0023, ratified 2026-06-21; first consumer downstream spt-discord) ──
[[requirements]]
id = "REQ-EP-8"
title = "AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `<adapter>[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake <id>`, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord."
required_stages = []  # rule 5: design ratified 2026-06-21 (grill-with-docs, ADR-0023); UNBUILT — activate when the milestone delivering [always-on] supervision lands. Reuses shellwake.rs scaffolding (REQ-SHELL-2) minus the offline flip; first consumer is the downstream spt-discord adapter (own repo).
[[requirements]]
id = "REQ-EP-9"
title = "`#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged)."
required_stages = []  # rule 5: design ratified 2026-06-21 (ADR-0023); UNBUILT — activate with REQ-EP-8's milestone (spt_proto::addr::Address::parse sigil extension + class-discriminated resolve in spt_net registry). REQ-HAZARD-ID-CHARSET is NOT amended (sigil is an address-layer concern).

# ───────────────────────────── Instances ────────────────────────────────────


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

M9-WAVE2-ROLE-WRITERS.md survey)

# ───────────────────────────── Instances ────────────────────────────────────

M9-WAVE2-ROLE-WRITERS.md survey)
# ── Always-on endpoints (ADR-0023, ratified 2026-06-21; first consumer downstream spt-discord) ──
[[requirements]]
id = "REQ-EP-8"
title = "AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `<adapter>[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake <id>`, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord."
required_stages = []  # rule 5: design ratified 2026-06-21 (grill-with-docs, ADR-0023); UNBUILT — activate when the milestone delivering [always-on] supervision lands. Reuses shellwake.rs scaffolding (REQ-SHELL-2) minus the offline flip; first consumer is the downstream spt-discord adapter (own repo).
[[requirements]]
id = "REQ-EP-9"
title = "`#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged)."
required_stages = []  # rule 5: design ratified 2026-06-21 (ADR-0023); UNBUILT — activate with REQ-EP-8's milestone (spt_proto::addr::Address::parse sigil extension + class-discriminated resolve in spt_net registry). REQ-HAZARD-ID-CHARSET is NOT amended (sigil is an address-layer concern).

# ───────────────────────────── Instances ────────────────────────────────────

title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)

+# ── Always-on endpoints (ADR-0023, ratified 2026-06-21; first consumer downstream spt-discord) ──

+title = "AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `<adapter>[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake <id>`, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord."

+required_stages = []  # rule 5: design ratified 2026-06-21 (grill-with-docs, ADR-0023); UNBUILT — activate when the milestone delivering [always-on] supervision lands. Reuses shellwake.rs scaffolding (REQ-SHELL-2) minus the offline flip; first consumer is the downstream spt-discord adapter (own repo).

+title = "`#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged)."

+required_stages = []  # rule 5: design ratified 2026-06-21 (ADR-0023); UNBUILT — activate with REQ-EP-8's milestone (spt_proto::addr::Address::parse sigil extension + class-discriminated resolve in spt_net registry). REQ-HAZARD-ID-CHARSET is NOT amended (sigil is an address-layer concern).


Write

# Always-on endpoints: resident supervised adapter binaries, `#`-sigil addressed

Status: accepted (2026-06-21)

<!-- [doc->REQ-EP-8] [doc->REQ-EP-9] -->

## Context & decision

We needed a way for an adapter to bridge an external multi-party substrate — first case: a Discord "forum" where many agents converse in one channel — into the SPT subnet, *including* drawing an **offline** agent online when it is addressed there. The substrate itself must never enter spt-core (it is substrate-independent), so the core gains only a generic mechanism.

We introduce the **always-on endpoint**: a resident, addressable endpoint that hosts **no mind**, declared by an adapter's `[always-on]` manifest section. The daemon boot-launches and supervises **one such binary per registered adapter-option** (`<adapter>[:profile]`), running it continuously and **independent of any agent's liveness** (so it is up to receive the first interaction even when zero agents are online). The binary **self-manages its channel endpoints via the existing `api bind`** — one connection fronts many `#`-addressed endpoints, no new "create endpoint" command. It may call `endpoint wake <id>` to revive an offline target; authorization is **target-side** (`resolve_wake` / `forward_wake` — "the target's access gate decides" — plus the access whitelist and the `shell_wake_spawn_anywhere` pre-consent), so **no caller-ownership gate is added**.

Always-on endpoints are addressed with a **mandatory leading `#` sigil**, `[subnet:]#id[@node]`, **bijective** with the class (`#name` ⟺ always-on, bare `name` ⟺ agent endpoint) so the router resolves the endpoint class from the address alone, before any registry lookup.

## Considered options

- **Make it a Shell.** Rejected: Shells are single-owner, *driven*, and control-exclusive (`REQ-SHELL-5`). A multi-party bridge with N peer participants inverts every one of those invariants.
- **Generalize the shell wake-watcher (`shellwake.rs`) into a persistent binary.** Rejected: the watcher is intrinsically the *offline half* of a mutual-exclusivity flip — keyed `(owner, shell_id)`, gated on `SHELL_STATUS_OFFLINE`, killed when the thing comes online. An always-on, owner-less binary fights that design. We **reuse its supervision scaffolding** (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) but as a **distinct kind**, not by overloading the watcher.
- **Faceless supervised service (no perch/identity).** Rejected: agents must address it two-way to drive the bridged channel, so it needs a real, addressable identity — not a faceless process.
- **Bake the substrate (Discord) into core / the daemon.** Rejected: spt-core is substrate-independent. Discord lives entirely in the downstream `spt-discord` adapter; core gains only the generic always-on mechanism.

## Consequences

- There are now **two** classes of third-party binary spt-core boot-launches (previously one — the shell wake-watcher). `CONTEXT.md` §Gateway revival updated from "the one class" accordingly.
- The `#` sigil is reserved at the **address-grammar layer only**; `REQ-HAZARD-ID-CHARSET` is **unchanged** — the parser strips a single leading `#` before id validation, so bare/stored ids stay charset-clean and a mid-id `#` is still rejected.
- An always-on binary is **least-trusted third-party code**, the same accepted-risk posture as shells and harness binaries (`CONTEXT.md` §binary-trust disclosure). The existing endpoint access whitelist and target-side wake gating apply unchanged.
- New tracked requirements `REQ-EP-8` (the kind + supervision) and `REQ-EP-9` (the `#` sigil), both **inactive** (`required_stages = []`) until the delivering milestone.
- All Discord-specific design (webhooks, per-agent threads, inbound routing, hybrid delivery, echo-loop prevention, secrets) is recorded in the sister repo **`../spt-discord/`**, deliberately not here.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\docs\adr\0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md (file state is current in your context — no need to Read it back)

# Always-on endpoints: resident supervised adapter binaries, `#`-sigil addressed

Status: accepted (2026-06-21)

<!-- [doc->REQ-EP-8] [doc->REQ-EP-9] -->

## Context & decision

We needed a way for an adapter to bridge an external multi-party substrate — first case: a Discord "forum" where many agents converse in one channel — into the SPT subnet, *including* drawing an **offline** agent online when it is addressed there. The substrate itself must never enter spt-core (it is substrate-independent), so the core gains only a generic mechanism.

We introduce the **always-on endpoint**: a resident, addressable endpoint that hosts **no mind**, declared by an adapter's `[always-on]` manifest section. The daemon boot-launches and supervises **one such binary per registered adapter-option** (`<adapter>[:profile]`), running it continuously and **independent of any agent's liveness** (so it is up to receive the first interaction even when zero agents are online). The binary **self-manages its channel endpoints via the existing `api bind`** — one connection fronts many `#`-addressed endpoints, no new "create endpoint" command. It may call `endpoint wake <id>` to revive an offline target; authorization is **target-side** (`resolve_wake` / `forward_wake` — "the target's access gate decides" — plus the access whitelist and the `shell_wake_spawn_anywhere` pre-consent), so **no caller-ownership gate is added**.

Always-on endpoints are addressed with a **mandatory leading `#` sigil**, `[subnet:]#id[@node]`, **bijective** with the class (`#name` ⟺ always-on, bare `name` ⟺ agent endpoint) so the router resolves the endpoint class from the address alone, before any registry lookup.

## Considered options

- **Make it a Shell.** Rejected: Shells are single-owner, *driven*, and control-exclusive (`REQ-SHELL-5`). A multi-party bridge with N peer participants inverts every one of those invariants.
- **Generalize the shell wake-watcher (`shellwake.rs`) into a persistent binary.** Rejected: the watcher is intrinsically the *offline half* of a mutual-exclusivity flip — keyed `(owner, shell_id)`, gated on `SHELL_STATUS_OFFLINE`, killed when the thing comes online. An always-on, owner-less binary fights that design. We **reuse its supervision scaffolding** (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) but as a **distinct kind**, not by overloading the watcher.
- **Faceless supervised service (no perch/identity).** Rejected: agents must address it two-way to drive the bridged channel, so it needs a real, addressable identity — not a faceless process.
- **Bake the substrate (Discord) into core / the daemon.** Rejected: spt-core is substrate-independent. Discord lives entirely in the downstream `spt-discord` adapter; core gains only the generic always-on mechanism.

## Consequences

- There are now **two** classes of third-party binary spt-core boot-launches (previously one — the shell wake-watcher). `CONTEXT.md` §Gateway revival updated from "the one class" accordingly.
- The `#` sigil is reserved at the **address-grammar layer only**; `REQ-HAZARD-ID-CHARSET` is **unchanged** — the parser strips a single leading `#` before id validation, so bare/stored ids stay charset-clean and a mid-id `#` is still rejected.
- An always-on binary is **least-trusted third-party code**, the same accepted-risk posture as shells and harness binaries (`CONTEXT.md` §binary-trust disclosure). The existing endpoint access whitelist and target-side wake gating apply unchanged.
- New tracked requirements `REQ-EP-8` (the kind + supervision) and `REQ-EP-9` (the `#` sigil), both **inactive** (`required_stages = []`) until the delivering milestone.
- All Discord-specific design (webhooks, per-agent threads, inbound routing, hybrid delivery, echo-loop prevention, secrets) is recorded in the sister repo **`../spt-discord/`**, deliberately not here.


ADR 0023 + REQs registered. Validating the toml against the gate, then creating the `../spt-discord/` sister dir for the adapter-side design.


Bash

Run traceable-reqs check


  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).


traceable-reqs check >/tmp/trcheck.txt 2>&1; echo "EXIT=$?"; grep -iE "REQ-EP-8|REQ-EP-9|fail|error|missing|invalid" /tmp/trcheck.txt | head -20; echo "---tail---"; tail -3 /tmp/trcheck.txt

Confirm check exit + new reqs status