The file C:\Users\decid\Documents\projects\spt-core\docs\adr\0004-single-daemon-broker-brain-split-and-self-update.md has been updated successfully. (file state is current in your context — no need to Read it back)

- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.

## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)

Codex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:

### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)
R-UPD-3's "no endpoint terminates during update" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:
- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**
- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.
- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.

### B. Ownership table (resolves FATAL #4 — "any live stream owned by brain breaks the invariant")
The boundary is normative, not "broker holds fds, brain holds logic." A resource is broker-owned **iff** a live consumer would lose continuity on brain restart.

| Resource | Owner | Spiked? |
|---|---|---|
| PTY master read+write handle | **broker** | ✅ #1 |
| Spawned harness child process | **broker** | ✅ #1 |
| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |
| Iroh endpoint + accepted QUIC streams | **broker** | ❌ open (FATAL #2) |
| mDNS socket / relay session | **broker** | ❌ open |
| Listening sockets | **broker** | ✅ #1 |
| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |

Consequence codex forced into the open: pushing live Iroh/QUIC ownership into the broker means networking stream-state lives in the "stable kernel," enlarging it. Accepted as the cost of the invariant; the broker is "stable" in *update cadence*, not in *narrowness*.

### C. ConPTY DSR gotcha (new hazard, discovered by Spike #1)
ConPTY withholds **all** child stdout until the terminal answers its startup cursor-position query (`ESC [ 6 n`). A broker reading a ConPTY master that does not reply `ESC [ <r>;<c> R` sees only the 4-byte query and then nothing — looks like a hung/silent child. Spike got 0 bytes of program output until the reader answered DSR, then full output. → `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR. Added to KNOWN-HAZARDS §5.

### D. Self-update delivery hardening (resolves SERIOUS #5)
Signature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by "binary signature"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.

### E. Open spike gaps (must close before M3 builds the daemon)
1. **Live Iroh + file-transfer stream survival across brain restart** (FATAL #2) — Spike #1 only proved PTY + plain TCP. Extend with an active QUIC stream.
2. **Linux `forkpty` parity** — Spike #1 was Windows-only (the high-risk path); rerun on Linux.
3. **100× restart + resize-under-load stress** (codex #3) — Spike #1 did one restart; harden before trusting.
4. **Idempotent/exactly-once delivery across brain restart** (FATAL-adjacent #14) — durable IDs + replay rules at every broker↔brain side-effect boundary. `REQ-HAZARD-RESTART-IDEMPOTENT`.

# Single consolidated daemon with broker/brain split; peer-propagated gated self-update

## Status

accepted (2026-05-29)

## Context

ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:

1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.

2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive "drain + restart the daemon" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).

## Decision

**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.

**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:
- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.
- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.

Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.

Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).

**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.

**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.

## Consequences

- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.
- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of "literally one process," preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.
- Peer-propagated updates make release signing mandatory, not optional.
- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.
- The "deliver to most-recently-active session" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.
- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.

+## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)

+Codex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:

+### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)

+R-UPD-3's "no endpoint terminates during update" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:

+- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**

+- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.

+- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.

+### B. Ownership table (resolves FATAL #4 — "any live stream owned by brain breaks the invariant")

+The boundary is normative, not "broker holds fds, brain holds logic." A resource is broker-owned **iff** a live consumer would lose continuity on brain restart.

+| PTY master read+write handle | **broker** | ✅ #1 |

+| Spawned harness child process | **broker** | ✅ #1 |

+| Accepted local client socket (`api listen`, relay) | **broker** | ✅ #1 (plain TCP) |

+| Iroh endpoint + accepted QUIC streams | **broker** | ❌ open (FATAL #2) |

+| mDNS socket / relay session | **broker** | ❌ open |

+| Listening sockets | **broker** | ✅ #1 |

+| Routing, registry state, pulse/psyche loops, manifest parse, update orchestration | brain (rehydrate from disk) | n/a |

+Consequence codex forced into the open: pushing live Iroh/QUIC ownership into the broker means networking stream-state lives in the "stable kernel," enlarging it. Accepted as the cost of the invariant; the broker is "stable" in *update cadence*, not in *narrowness*.

+### C. ConPTY DSR gotcha (new hazard, discovered by Spike #1)

+ConPTY withholds **all** child stdout until the terminal answers its startup cursor-position query (`ESC [ 6 n`). A broker reading a ConPTY master that does not reply `ESC [ <r>;<c> R` sees only the 4-byte query and then nothing — looks like a hung/silent child. Spike got 0 bytes of program output until the reader answered DSR, then full output. → `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR. Added to KNOWN-HAZARDS §5.

+### D. Self-update delivery hardening (resolves SERIOUS #5)

+Signature-verify alone is insufficient. v1 adds: monotonic version field (rollback rejection), release-metadata expiry, channel pinning, key rotation/revocation list, and **adapter content signing** (adapter file-pull / delegated-command updates were uncovered by "binary signature"). Track as `REQ-HAZARD-UPDATE-ROLLBACK`.

+### E. Open spike gaps (must close before M3 builds the daemon)

+1. **Live Iroh + file-transfer stream survival across brain restart** (FATAL #2) — Spike #1 only proved PTY + plain TCP. Extend with an active QUIC stream.

+2. **Linux `forkpty` parity** — Spike #1 was Windows-only (the high-risk path); rerun on Linux.

+3. **100× restart + resize-under-load stress** (codex #3) — Spike #1 did one restart; harden before trusting.

+4. **Idempotent/exactly-once delivery across brain restart** (FATAL-adjacent #14) — durable IDs + replay rules at every broker↔brain side-effect boundary. `REQ-HAZARD-RESTART-IDEMPOTENT`.


Writing the spike record doc.


Write

# Spike #1 — broker/brain no-terminate handoff

> Date: 2026-05-31. Status: **PASS (core thesis, Windows ConPTY)** with named open gaps.
> Throwaway code: `../spt-spikes/spike-01-broker-handoff` (outside the spt-core repo, not shipped).
> Drives ROADMAP Stage B Spike #1 + codex red-team FATAL #2/#3 (`docs/reviews/STAGE-A-codex-redteam.md`).

## Question

ADR-0004 claims a PTY child + an open client connection survive a daemon-logic ("brain") restart because a stable "broker" holds the load-bearing handles. Codex called this the single most-likely redesign trigger. **Does it actually hold on Windows ConPTY — the high-risk path?**

## Method

One Rust binary (`portable-pty` + std net), five modes: `broker`, `brain <ver>`, `child`, `client`, `run`.
- **broker** owns the *sole* PTY master reader+writer, the spawned child, the listening sockets, and every accepted client socket. Forwards PTY output to all sinks; routes brain `INPUT:` control lines to the PTY writer it owns. Never restarts.
- **brain** holds only a forwarding control socket — no PTY handle, no client fd. Killable/relaunchable.
- **client** = an external TCP consumer that must survive brain churn.
- **run** orchestrates: start broker+child, attach client, start brain v1, kill it, start brain v2 (which injects input post-restart), tear down, then assert from logs.

Invariants: **A** child alive whole run (no PTY EOF) · **B** `tick N` counter contiguous · **C** input reaches child after brain restart · **D** client stream contiguous across restart.

## Result

```
child pid 33880 survived | pty ticks 0..39 contiguous, no EOF
post-restart input echoed | client: 34 ticks, no gap across the brain kill/restart window
A PASS  B PASS  C PASS  D PASS  →  OVERALL PASS ✅
```

The ownership model works: brain death is just a closed forwarding socket; the broker's held handles keep the child and the live client stream intact, and the broker-owned writer still delivers input after the brain is replaced. This is the *brain-only* update class — the one ADR-0004 must preserve, and the routine case.

## Key finding — ConPTY DSR stall (not in any doc before)

ConPTY emits a cursor-position query `ESC [ 6 n` at startup and **withholds all child stdout until the terminal answers it**. First runs captured exactly 4 bytes (`\e[6n`) and zero program output — the child looked dead but was ticking fine standalone. Replying `ESC [ 1;1 R` from the reader immediately unblocked full output (`bytes=4` → `bytes=93`, HELLO/DONE present). A `cmd.exe` probe reproduced it identically, proving it's a ConPTY plumbing requirement, not a workload bug. → new hazard `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR.

Secondary gotcha: a ConPTY master does **not** EOF while the broker holds the writer, so any read loop must drain on a thread and never gate exit on a blocking `read()` returning. (Cost a build cycle — three hung `spike.exe` instances locked the binary against relink.)

## What this does NOT yet prove (feeds ADR-0004 §E before M3)

1. **Live Iroh/QUIC + file-transfer stream survival** across brain restart (codex FATAL #2). Only PTY + plain TCP tested. Plain-TCP survival is a weak proxy — QUIC carries process-local crypto/stream state. **Highest remaining risk.**
2. **Linux `forkpty` parity** — Windows-only run.
3. **100× restart + resize-under-load** — single restart only.
4. **Idempotent delivery** across the broker↔brain boundary (codex #14).

## Verdict

ADR-0004's broker/brain split is **not falsified** — the hardest OS path (ConPTY) passes, and the DSR stall that would have bitten the real terminal layer is now caught and documented. The split survives Stage A. Promote the four open gaps into the M3 daemon plan rather than re-opening the decision.


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

# Spike #1 — broker/brain no-terminate handoff

> Date: 2026-05-31. Status: **PASS (core thesis, Windows ConPTY)** with named open gaps.
> Throwaway code: `../spt-spikes/spike-01-broker-handoff` (outside the spt-core repo, not shipped).
> Drives ROADMAP Stage B Spike #1 + codex red-team FATAL #2/#3 (`docs/reviews/STAGE-A-codex-redteam.md`).

## Question

ADR-0004 claims a PTY child + an open client connection survive a daemon-logic ("brain") restart because a stable "broker" holds the load-bearing handles. Codex called this the single most-likely redesign trigger. **Does it actually hold on Windows ConPTY — the high-risk path?**

## Method

One Rust binary (`portable-pty` + std net), five modes: `broker`, `brain <ver>`, `child`, `client`, `run`.
- **broker** owns the *sole* PTY master reader+writer, the spawned child, the listening sockets, and every accepted client socket. Forwards PTY output to all sinks; routes brain `INPUT:` control lines to the PTY writer it owns. Never restarts.
- **brain** holds only a forwarding control socket — no PTY handle, no client fd. Killable/relaunchable.
- **client** = an external TCP consumer that must survive brain churn.
- **run** orchestrates: start broker+child, attach client, start brain v1, kill it, start brain v2 (which injects input post-restart), tear down, then assert from logs.

Invariants: **A** child alive whole run (no PTY EOF) · **B** `tick N` counter contiguous · **C** input reaches child after brain restart · **D** client stream contiguous across restart.

## Result

```
child pid 33880 survived | pty ticks 0..39 contiguous, no EOF
post-restart input echoed | client: 34 ticks, no gap across the brain kill/restart window
A PASS  B PASS  C PASS  D PASS  →  OVERALL PASS ✅
```

The ownership model works: brain death is just a closed forwarding socket; the broker's held handles keep the child and the live client stream intact, and the broker-owned writer still delivers input after the brain is replaced. This is the *brain-only* update class — the one ADR-0004 must preserve, and the routine case.

## Key finding — ConPTY DSR stall (not in any doc before)

ConPTY emits a cursor-position query `ESC [ 6 n` at startup and **withholds all child stdout until the terminal answers it**. First runs captured exactly 4 bytes (`\e[6n`) and zero program output — the child looked dead but was ticking fine standalone. Replying `ESC [ 1;1 R` from the reader immediately unblocked full output (`bytes=4` → `bytes=93`, HELLO/DONE present). A `cmd.exe` probe reproduced it identically, proving it's a ConPTY plumbing requirement, not a workload bug. → new hazard `REQ-HAZARD-CONPTY-DSR`; every ConPTY reader must auto-answer DSR.

Secondary gotcha: a ConPTY master does **not** EOF while the broker holds the writer, so any read loop must drain on a thread and never gate exit on a blocking `read()` returning. (Cost a build cycle — three hung `spike.exe` instances locked the binary against relink.)

## What this does NOT yet prove (feeds ADR-0004 §E before M3)

1. **Live Iroh/QUIC + file-transfer stream survival** across brain restart (codex FATAL #2). Only PTY + plain TCP tested. Plain-TCP survival is a weak proxy — QUIC carries process-local crypto/stream state. **Highest remaining risk.**
2. **Linux `forkpty` parity** — Windows-only run.
3. **100× restart + resize-under-load** — single restart only.
4. **Idempotent delivery** across the broker↔brain boundary (codex #14).

## Verdict

ADR-0004's broker/brain split is **not falsified** — the hardest OS path (ConPTY) passes, and the DSR stall that would have bitten the real terminal layer is now caught and documented. The split survives Stage A. Promote the four open gaps into the M3 daemon plan rather than re-opening the decision.


Adding new hazards to KNOWN-HAZARDS + registering REQ-HAZARD entries in traceable-reqs.toml. Reading both formats.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && echo "=== toml: a REQ-HAZARD sample ===" && grep -n -A6 "REQ-HAZARD" traceable-reqs.toml | head -40 && echo "=== toml tail ===" && tail -20 traceable-reqs.toml

Inspect traceable-reqs.toml hazard format


=== toml: a REQ-HAZARD sample ===
311:id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
312-title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
313-required_stages = []
314-[[requirements]]
315:id = "REQ-HAZARD-INFO-JSON-TORN-READ"
316-title = "State-file reads tolerate concurrent writes (1.2)"
317-required_stages = []
318-[[requirements]]
319:id = "REQ-HAZARD-STALE-INDEX-LOCK"
320-title = "Sweep stale lockfiles on daemon boot (1.3)"
321-required_stages = []
322-[[requirements]]
323:id = "REQ-HAZARD-DEFERRED-DRAIN"
324-title = "Deferred spool rows excluded from the event-stream drain (1.4)"
325-required_stages = []
326-[[requirements]]
327:id = "REQ-HAZARD-WORKER-PATH"
328-title = "Single source of truth for Worker/Psyche perch location (1.5)"
329-required_stages = []
330-[[requirements]]
331:id = "REQ-HAZARD-PARENT-PID-PREFER"
332-title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
333-required_stages = []
334-[[requirements]]
335:id = "REQ-HAZARD-STDIN-SESSION-ID"
336-title = "Stdin session_id precedence over env (2.2)"
337-required_stages = []
338-[[requirements]]
339:id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
340-title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
341-required_stages = []
342-[[requirements]]
343:id = "REQ-HAZARD-GEN-START-NOW"
344-title = "gen_start = now() on cold-start and handoff (2.4)"
345-required_stages = []
346-[[requirements]]
347:id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
348-title = "Ephemeral perch cleanup on every ring exit path (3.1)"
349-required_stages = []
350-[[requirements]]
=== toml tail ===
[[requirements]]
id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
title = "Single path/registry source of truth; no layout ambiguity (6.1)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-SOFT-CLEANUP"
title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
title = "No hard-delete of a parent hosting non-empty children (6.3)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
title = "Drop files are daemon-owned single-writer (6.4)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []


Edit

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = []


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


380	title = "Windows PID-recycling false positives guarded (5.1)"
381	required_stages = []
382	[[requirements]]
383	id = "REQ-HAZARD-EBUSY-RENAME"
384	title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
385	required_stages = []
386	[[requirements]]
387	id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
388	title = "Every harness/git subprocess has a timeout (5.3)"
389	required_stages = []
390	[[requirements]]
391	id = "REQ-HAZARD-UNC-PATH-STRIP"
392	title = "Strip Windows UNC prefix on serialized paths (5.4)"
393	required_stages = []
394	[[requirements]]
395	id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
396	title = "Single path/registry source of truth; no layout ambiguity (6.1)"
397	required_stages = []
398	[[requirements]]
399	id = "REQ-HAZARD-SOFT-CLEANUP"
400	title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
401	required_stages = []
402	[[requirements]]
403	id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
404	title = "No hard-delete of a parent hosting non-empty children (6.3)"
405	required_stages = []
406	[[requirements]]
407	id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
408	title = "Drop files are daemon-owned single-writer (6.4)"
409	required_stages = []
410	[[requirements]]
411	id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
412	title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
413	required_stages = []
414

title = "Windows PID-recycling false positives guarded (5.1)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-EBUSY-RENAME"
title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
title = "Every harness/git subprocess has a timeout (5.3)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-UNC-PATH-STRIP"
title = "Strip Windows UNC prefix on serialized paths (5.4)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
title = "Single path/registry source of truth; no layout ambiguity (6.1)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-SOFT-CLEANUP"
title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
title = "No hard-delete of a parent hosting non-empty children (6.3)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
title = "Drop files are daemon-owned single-writer (6.4)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []


id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = []


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)

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = []
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = []

+# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---

+title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"

+title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"

+title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"

+title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"


Now add the three new hazard entries to KNOWN-HAZARDS.md (§5 platform + new §7 boundary/security).


194	| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
195	| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
196	| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
197	| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
198	| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
199	| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
200	| 4.2 | Parser panic-free + tolerant | spt-proto |
201	| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
202	| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
203	| 5.3 | Timeout every harness subprocess | manifest invocations |
204	| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
205	| 6.1 | Single path/registry source of truth | storage layout |
206	| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
207	| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |

| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |
| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |


Grep

^## |^### 5\.|^## 6|Conformance checklist


9:## 1. Race conditions & ordering
43:## 2. Identity & session-binding
71:## 3. Lifecycle
93:## 4. Wire / transport
126:## 5. Platform-specific
128:### 5.1 Windows PID recycling false positives
134:### 5.2 Windows EBUSY on atomic rename
140:### 5.3 Git/subprocess timeout stamping
146:### 5.4 Windows UNC prefix in serialized paths
154:## 6. Documented regressions (non-obvious invariants)
188:## Conformance checklist (condensed)


146	### 5.4 Windows UNC prefix in serialized paths
147	- **Failure:** canonicalized `\\?\C:\...` serializes to `//?/C:/...` and fails `read_to_string`.
148	- **Invariant:** strip the `\\?\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.
149	- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
150	- **Sister cite:** `src/common/owlery.rs:377-384`.
151
152	---
153
154	## 6. Documented regressions (non-obvious invariants)
155
156	### 6.1 No flat/nested perch siblings; resolver-routed paths
157	- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.

### 5.4 Windows UNC prefix in serialized paths
- **Failure:** canonicalized `\\?\C:\...` serializes to `//?/C:/...` and fails `read_to_string`.
- **Invariant:** strip the `\\?\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.
- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

---

## 6. Documented regressions (non-obvious invariants)

### 6.1 No flat/nested perch siblings; resolver-routed paths
- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.


Edit

- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

---

## 6. Documented regressions (non-obvious invariants)

- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.
- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.
- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).
- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.

---

## 6. Documented regressions (non-obvious invariants)


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

- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

---

## 6. Documented regressions (non-obvious invariants)

- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.
- **Sister cite:** `src/common/owlery.rs:377-384`.

### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.
- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.
- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).
- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.

---

## 6. Documented regressions (non-obvious invariants)

+### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`

+- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.

+- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.

+- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).

+- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.


Adding §7 (the two missing boundary/security hazards codex flagged) before the conformance checklist, plus checklist rows.


188	- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.
189	- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity, and the freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the distributed generalization of this guard. Highest-value carryover for the sync design.
190	- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
191
192	---
193
194	## Conformance checklist (condensed)
195
196	| # | Invariant | spt-core surface |
197	|---|---|---|
198	| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
199	| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
200	| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
201	| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
202	| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
203	| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
204	| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
205	| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
206	| 4.2 | Parser panic-free + tolerant | spt-proto |
207	| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
208	| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
209	| 5.3 | Timeout every harness subprocess | manifest invocations |

- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.
- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity, and the freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the distributed generalization of this guard. Highest-value carryover for the sync design.
- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

## Conformance checklist (condensed)

| # | Invariant | spt-core surface |
|---|---|---|
| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |


- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

## Conformance checklist (condensed)

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).
- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.
- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.
- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).

### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.
- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
- **Source:** codex Stage A #14.

---

## Conformance checklist (condensed)


- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

## Conformance checklist (condensed)

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).
- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.
- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.
- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).

### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.
- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.
- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.
- **Source:** codex Stage A #14.

---

## Conformance checklist (condensed)

+## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)

+These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.

+### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`

+- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).

+- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.

+- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.

+- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).

+### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`

+- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.

+- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.

+- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.


222	| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
223	| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
224	| 4.2 | Parser panic-free + tolerant | spt-proto |
225	| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
226	| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
227	| 5.3 | Timeout every harness subprocess | manifest invocations |
228	| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
229	| 6.1 | Single path/registry source of truth | storage layout |
230	| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
231	| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
232

| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |
| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |


| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |

| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |


| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |

| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |

| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |

| 5.3 | Timeout every harness subprocess | manifest invocations |

+| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |

+| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |

+| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |


ADR-0004 + spike + hazards captured. Now brief Stage-A amendment notes on ADR-0002/0003/0005 (their findings are M3/M4-milestone, but record so nothing's lost). Reading tails.


Bash

cd "C:/Users/decid/Documents/projects/spt-core/docs/adr" && for f in 0002-*.md 0003-*.md 0005-*.md; do echo "=== $f (last 4 lines) ==="; tail -4 "$f"; done

Show tails of ADR 0002,0003,0005


=== 0002-bake-networking-into-core-collapse-spt-node.md (last 4 lines) ===
- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)
=== 0003-multi-instance-node-anchored-identity-model.md (last 4 lines) ===
- Bare-ID addressing needs a deterministic resolution policy (local → most-recently-active → explicit `id@node` override).
- The networking decision (ADR-0002) is now load-bearing for a flagship user feature, not just messaging — remote drive depends on it working well.
- Cross-node Psyche sync over P2P replaces the sister project's GitHub-repo sync, removing the `gh`/account/setup dependency.
- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.
=== 0005-totp-seeded-spake2-node-pairing.md (last 4 lines) ===
- 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.


ADR-0005's last bullet ("revocation is a trust-store delete") is exactly what codex FATAL #10 refutes. Appending Stage-A amendment blocks to all three.


Edit

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

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

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).


1	# TOTP-seeded SPAKE2 node pairing

# TOTP-seeded SPAKE2 node pairing


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

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

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).


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)

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

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).

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

+## Stage A red-team amendments (2026-05-31)

+Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

+- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.

+- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.

+- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).


1	# Multi-instance, node-anchored identity model


- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.

- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) flagged three SERIOUS issues; resolve before M4:

- **#7 — wall-clock "newest" loses concurrent context writes.** Two active instances editing the same live topic is a semantic *conflict*, not a stale-write race; wall-clock newest silently drops a valid update, and LLM-authored live/project tiering is too soft to be the conflict boundary. **Required:** monotonic per-node epochs / vector clocks (or CRDT-ish merge / explicit conflict review) for the cross-node Psyche merge — the distributed generalization of KNOWN-HAZARDS 6.5. *Decision pending: which merge model.*
- **#8 — `local → most-recently-active → id@node` is ambiguous under partition/clock-skew.** Can route messages or remote-control to the wrong instance around active/dormant transitions. **Required:** registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.
- **#9 — dormant-warm default has real resource/security cost.** Warm sessions retain credentials, file locks, model/billing footprint, PTYs, watchers on *every* node. **Required:** measure N dormant sessions on real adapters; define an adapter-specific warm/cold policy + resource budget before locking warm as the default (R-INST-3).


The file C:\Users\decid\Documents\projects\spt-core\docs\adr\0003-multi-instance-node-anchored-identity-model.md has been updated successfully. (file state is current in your context — no need to Read it back)

- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) flagged three SERIOUS issues; resolve before M4:

- **#7 — wall-clock "newest" loses concurrent context writes.** Two active instances editing the same live topic is a semantic *conflict*, not a stale-write race; wall-clock newest silently drops a valid update, and LLM-authored live/project tiering is too soft to be the conflict boundary. **Required:** monotonic per-node epochs / vector clocks (or CRDT-ish merge / explicit conflict review) for the cross-node Psyche merge — the distributed generalization of KNOWN-HAZARDS 6.5. *Decision pending: which merge model.*
- **#8 — `local → most-recently-active → id@node` is ambiguous under partition/clock-skew.** Can route messages or remote-control to the wrong instance around active/dormant transitions. **Required:** registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.
- **#9 — dormant-warm default has real resource/security cost.** Warm sessions retain credentials, file locks, model/billing footprint, PTYs, watchers on *every* node. **Required:** measure N dormant sessions on real adapters; define an adapter-specific warm/cold policy + resource budget before locking warm as the default (R-INST-3).

# Multi-instance, node-anchored identity model

## Status

accepted (2026-05-29) · amended (2026-05-30) — see Amendment below; the original framing over-weighted remote-drive as the *definition* of multi-instance.

## Amendment (2026-05-30): native-instances-with-synced-mind is primary; remote-drive is a separate mode

The original decision below framed "operate a LiveAgent on any node from any other machine" primarily as **remote PTY drive**. That is backwards-weighted. Corrected model:

- **Primary — instance:** the *same* endpoint (same harness+adapter) running **natively** on a machine (local files, local compute), with the **Psyche mind synced** across instances. Files/session-history are per-node and cannot teleport; the mind follows. This is the core meaning of "same agent on multiple machines."
- **Instance dormancy:** typically one instance is actively-driven; others are **dormant** (live, state-preserved, re-activatable in place). Switching machines makes the previously-driven instance dormant — no explicit stop. Registry status is active / dormant / offline.
- **Remote-control (separate, Shell-like):** attaching a control/view surface to an instance *running on another node* (compute+files stay remote; you are a viewport — the byte-stream terminal attach). A driven surface, user→agent direction; distinct from the instance concept.
- **Handoff (remote-control → local):** `git pull` brings files; the fresh-with-preload resume seam preloads the other instance's synced context; the remote instance goes dormant. No teardown.
- **Two-tier context sync:** **live context** (per-agent) syncs to all instances; **project context** (per-agent-per-project) syncs only to same-project instances. Resolves divergent simultaneous work without losing "mind follows everywhere." (Depends on refining the Psyche authoring directives so each tier holds the right material.)

The decision below stands except where it implies remote-drive is the primary realization of multi-instance — read "drivable from anywhere" as "runnable natively on any of its nodes with synced mind, **plus** an optional remote-control surface mode."

## Context

The sister project models an agent as essentially one perch on one machine, and explicitly deferred "agent name resolution" across a subnet. spt-core commits to two user-facing requirements that break that model: (a) the user can interact with any endpoint from any machine, and (b) a LiveAgent can be operated on any node it has ever been instantiated on, from any other machine. The Instances concept (each endpoint classifies as an instance; the same endpoint ID can have instances across multiple machines with equal functionality) requires deciding what an "instance" actually is before any of it can be built.

## Decision

Split agent identity into two layers: a subnet-wide **endpoint ID** (the logical identity, `ling`) and an **instance** (a materialization of that ID on a specific node, `ling@desktop`).

**Instances are node-anchored, not portable.** An instance's substance — project working directory, harness session history, Psyche context, spool — lives on its node's filesystem and does not teleport. Instances of one ID are distinct *seats* (possibly on different projects), sharing only identity. "Equal functionality across all machines" therefore means *any instance is drivable with the full command surface from any node*, NOT that instances share state.

Rejected alternative — **portable/synced instances** (one logical conversation+state replicated across nodes): a LiveAgent's whole point is operating inside a project on a machine; the project filesystem can't be teleported, and full state replication across nodes is a distributed-consistency problem with no payoff for the actual use case (drive my desktop agent from my laptop).

Remote drive is realized by daemon-to-daemon PTY proxying over the network session-surface abstraction — enabled by the prior decision (ADR-0002) to collapse PTY-host and network-host into one `spt-daemon`.

**v1 scope (V1-mid):** ship the data-model split, the subnet registry (the now-mandatory name-resolution layer), the resolution policy, remote-drive of already-running instances, and cross-node Psyche sync transport. Defer (seam-compatible) instantiate-anywhere + its consent gate, remote command execution + its security gate, and the cross-instance context-freshness auto-feed rule.

## Consequences

- The subnet registry becomes a mandatory, first-class subsystem (eventually-consistent `endpoint_id → [instances]`), not a deferred nicety.
- Bare-ID addressing needs a deterministic resolution policy (local → most-recently-active → explicit `id@node` override).
- The networking decision (ADR-0002) is now load-bearing for a flagship user feature, not just messaging — remote drive depends on it working well.
- Cross-node Psyche sync over P2P replaces the sister project's GitHub-repo sync, removing the `gh`/account/setup dependency.
- Remote command execution and remote instantiation are the highest-risk capabilities in the system; both are gated behind a consent/security model whose design is deferred but whose existence is assumed by the architecture.

+Codex (`docs/reviews/STAGE-A-codex-redteam.md`) flagged three SERIOUS issues; resolve before M4:

+- **#7 — wall-clock "newest" loses concurrent context writes.** Two active instances editing the same live topic is a semantic *conflict*, not a stale-write race; wall-clock newest silently drops a valid update, and LLM-authored live/project tiering is too soft to be the conflict boundary. **Required:** monotonic per-node epochs / vector clocks (or CRDT-ish merge / explicit conflict review) for the cross-node Psyche merge — the distributed generalization of KNOWN-HAZARDS 6.5. *Decision pending: which merge model.*

+- **#8 — `local → most-recently-active → id@node` is ambiguous under partition/clock-skew.** Can route messages or remote-control to the wrong instance around active/dormant transitions. **Required:** registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.

+- **#9 — dormant-warm default has real resource/security cost.** Warm sessions retain credentials, file locks, model/billing footprint, PTYs, watchers on *every* node. **Required:** measure N dormant sessions on real adapters; define an adapter-specific warm/cold policy + resource budget before locking warm as the default (R-INST-3).


1	# Bake networking into core; collapse spt-node into the per-machine daemon


- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)

- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) sharpened the collapse's cost:

- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4.
- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the "stable broker," enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — "stable" now means *update cadence*, not *narrowness*.


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

- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) sharpened the collapse's cost:

- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4.
- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the "stable broker," enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — "stable" now means *update cadence*, not *narrowness*.

# Bake networking into core; collapse spt-node into the per-machine daemon

## Status

accepted (2026-05-28) — reverses the sister project's separate-daemon decision (claude_skill_owl CONTEXT.md §Networking) for the spt-core context. · amended (2026-05-31): GitHub re-admitted as an opt-in context-sync hub (see Amendment below).

## Amendment (2026-05-31): GitHub re-admitted as an opt-in context-sync hub

This ADR retired GitHub as the *required/default* cross-node sync. It is now re-admitted as an **opt-in alternative transport for the context repo only** (`tracked/`), enabled by `spt context-github-setup`:

- **Default stays P2P** + no-account — the core promise is unchanged.
- **Hub mode (opt-in):** every node pull→merge→pushes against a private GitHub remote (always-online sync hub + live GUI view), authed by a **shared deploy key** distributed as subnet secret material (only the setup node needs a GitHub account). **Prefer-GitHub, P2P backstop.**
- **Scope:** hub mode carries the **context repo only**. Messaging, registry, remote-drive, pairing, and presence always ride P2P/Iroh; gh-on never removes the P2P requirement. So the original decision (networking baked-in, first-class, P2P) stands in full — the hub is a context-sync convenience layered on top, not a return to GitHub-as-infrastructure.

## Context

The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.

spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — "just works."**

## Decision

Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).

"Just works WAN, reachable even when no session is running" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.

Rejected alternatives:
- **Seams-only in core, transport in spt-node (A):** inconsistent with first-class/zero-config networking; preserves a separate-deliverable boundary whose original justification is gone.
- **Two daemons — term-daemon + net-daemon (B2):** doubles the always-on process count and the crash/restart/binary-handoff surface for no benefit once both are in core.
- **LAN-in-core, WAN-in-node (C):** puts the repo boundary *inside* the transport layer, splitting shared discovery/identity/framing code across two repos.

**v1 relay stance:** default to n0.computer's free public relays for zero-config onboarding; ship a self-hostable-relay config knob and plain-language disclosure as the principled escape hatch. The dependency is operational, metadata-only (traffic is end-to-end encrypted), and escapable.

## Consequences

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport is a **first-class v1 capability** of spt-core (baked into the core crates, default-on in the reference binary) — *not* a post-v1 milestone and not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.
- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)

+Codex (`docs/reviews/STAGE-A-codex-redteam.md`) sharpened the collapse's cost:

+- **SERIOUS #6 — one collapsed daemon = one failure domain.** A networking bug can take out terminal hosting; a PTY-broker bug can take out WAN reachability. Accepted (the collapse is locked), but v1 owes a **fault-injection acceptance matrix**: crash/hang Iroh, mDNS, registry sync, PTY broker, and manifest invocation independently; measure blast radius + recovery. Track at M4.

+- **Interaction with ADR-0004 (FATAL #4):** preserving live WAN streams across a brain restart forces Iroh endpoint / QUIC stream state into the "stable broker," enlarging the kernel ADR-0002 wanted minimal. Resolved in ADR-0004's ownership table — "stable" now means *update cadence*, not *narrowness*.


All findings folded into ADRs + hazards. Validate with `traceable-reqs check` (new reqs are inactive → should pass).


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && (traceable-reqs check 2>&1 || echo "EXIT=$?")

Run traceable-reqs check


manifest_error: scan root 'src' is unreadable or does not exist
EXIT=2


CLI installed but errors: scan root `src` missing (no code yet). Check the toml config — likely was the case at seed too. Inspect scan-root config.


cd "C:/Users/decid/Documents/projects/spt-core" && grep -n -i "root\|scan\|src\|docs\|path" traceable-reqs.toml | head -30

Find scan root config in toml


4:# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
12:[scan]
13:roots = ["src", "tests", "docs"]
116:title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
264:title = "Two install paths; signed one-line script; OS-service registration"
285:id = "REQ-DOCS-1"
286:title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
289:id = "REQ-DOCS-2"
293:id = "REQ-DOCS-3"
297:id = "REQ-DOCS-4"
301:id = "REQ-DOCS-5"
306:# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
327:id = "REQ-HAZARD-WORKER-PATH"
348:title = "Ephemeral perch cleanup on every ring exit path (3.1)"
391:id = "REQ-HAZARD-UNC-PATH-STRIP"
392:title = "Strip Windows UNC prefix on serialized paths (5.4)"
395:id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
396:title = "Single path/registry source of truth; no layout ambiguity (6.1)"


Scan roots = `["src","tests","docs"]`; `src`/`tests` don't exist pre-code, so the gate hard-errors (would've at seed too — likely never run green yet). Minimal fix: placeholder dirs so roots are readable. All reqs inactive → should then pass.


cd "C:/Users/decid/Documents/projects/spt-core" && mkdir -p src tests && (test -f src/.gitkeep || echo "# placeholder until M0 creates the cargo workspace" > src/.gitkeep) && (test -f tests/.gitkeep || echo "# placeholder until M0" > tests/.gitkeep) && echo "--- re-run check ---" && (traceable-reqs check 2>&1 || echo "EXIT=$?")

Create placeholder roots and re-run check


--- re-run check ---
95 requirements: 95 complete, 0 incomplete, 2 findings
  [OK] REQ-API-1  required: []  stages: -doc -impl -unit -int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: []  stages: -doc -impl -unit -int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: []  stages: -doc -impl -unit -int
       commune/signoff are file-drops, not commands
  [OK] REQ-ARCH-1  required: []  stages: -doc -impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: []  stages: -doc -impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: []  stages: -doc -impl -unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: []  stages: -doc -impl -unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-DAEMON-1  required: []  stages: -doc -impl -unit -int
       One per-machine spt-daemon owning all per-machine state
  [OK] REQ-DAEMON-2  required: []  stages: -doc -impl -unit -int
       Broker/brain split for seamless self-update
  [OK] REQ-DAEMON-3  required: []  stages: -doc -impl -unit -int
       Any api invocation auto-starts the daemon if absent
  [OK] REQ-DAEMON-4  required: []  stages: -doc -impl -unit -int
       Honor every KNOWN-HAZARDS invariant
  [OK] REQ-DOCS-1  required: []  stages: -doc -impl -unit -int
       Dual-audience docs (human + AI dev-agent), markdown once / two depths
  [OK] REQ-DOCS-2  required: []  stages: -doc -impl -unit -int
       Sub-10-minute runnable killer quickstart per audience
  [OK] REQ-DOCS-3  required: []  stages: -doc -impl -unit -int
       Diátaxis structure; one canonical way to do X
  [OK] REQ-DOCS-4  required: []  stages: -doc -impl -unit -int
       Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)
  [OK] REQ-DOCS-5  required: []  stages: -doc -impl -unit -int
       Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked
  [OK] REQ-EP-1  required: []  stages: -doc -impl -unit -int
       Day-one endpoint types; open type system
  [OK] REQ-EP-2  required: []  stages: -doc -impl -unit -int
       Agent endpoints vs Shells distinction in the type model
  [OK] REQ-EP-3  required: []  stages: -doc -impl -unit -int
       Messaging payloads carry typed operation commands + file blobs
  [OK] REQ-EP-4  required: []  stages: -doc -impl -unit -int
       PresenceChannel broker endpoint (seam day-one)
  [OK] REQ-FRONT-1  required: []  stages: -doc -impl -unit -int
       Day-one launcher/manager frontend (list/launch/attach/init)
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: []  stages: -doc -impl -unit -int
       No hard-delete of a parent hosting non-empty children (6.3)
  [OK] REQ-HAZARD-CONPTY-DSR  required: []  stages: -doc -impl -unit -int
       ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
  [OK] REQ-HAZARD-DEFERRED-DRAIN  required: []  stages: -doc -impl -unit -int
       Deferred spool rows excluded from the event-stream drain (1.4)
  [OK] REQ-HAZARD-DEFERRED-SURVIVE-DRAIN  required: []  stages: -doc -impl -unit -int
       Deferred rows survive poll drain (4.4)
  [OK] REQ-HAZARD-DIRECT-WRITE-PRECEDENCE  required: []  stages: -doc -impl -unit -int
       Direct-write precedence marker (with node id) guards stale overwrite (6.5)
  [OK] REQ-HAZARD-DROP-FILE-SINGLE-WRITER  required: []  stages: -doc -impl -unit -int
       Drop files are daemon-owned single-writer (6.4)
  [OK] REQ-HAZARD-EBUSY-RENAME  required: []  stages: -doc -impl -unit -int
       tmp-write + atomic-rename + retry on Windows EBUSY (5.2)
  [OK] REQ-HAZARD-ECHO-BEFORE-SIGNOFF  required: []  stages: -doc -impl -unit -int
       Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: []  stages: -doc -impl +unit -int
       Envelope decode order, ampersand decoded last (4.1)
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: []  stages: -doc -impl -unit -int
       Two-slice envelope parser is panic-free and tolerant (4.2)
  [OK] REQ-HAZARD-EPHEMERAL-CLEANUP  required: []  stages: -doc -impl -unit -int
       Ephemeral perch cleanup on every ring exit path (3.1)
  [OK] REQ-HAZARD-GEN-START-NOW  required: []  stages: -doc -impl -unit -int
       gen_start = now() on cold-start and handoff (2.4)
  [OK] REQ-HAZARD-GRACE-BEFORE-SIGNOFF  required: []  stages: -doc -impl -unit -int
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
  [OK] REQ-HAZARD-HANDOFF-ARGV-COMPAT  required: []  stages: -doc -impl -unit -int
       Broker/brain IPC + handoff argv version-tolerant (2.3)
  [OK] REQ-HAZARD-INBOX-NO-DOUBLE  required: []  stages: -doc -impl -unit -int
       No double-delivery via legacy inbox (4.5)
  [OK] REQ-HAZARD-INFO-JSON-TORN-READ  required: []  stages: -doc -impl -unit -int
       State-file reads tolerate concurrent writes (1.2)
  [OK] REQ-HAZARD-LOCAL-API-AUTH  required: []  stages: -doc -impl -unit -int
       Every local `api` mutation authenticated to an endpoint/session (codex #13)
  [OK] REQ-HAZARD-PARENT-PID-PREFER  required: []  stages: -doc -impl -unit -int
       Prefer stable parent PID / broker handle over ephemeral PID (2.1)
  [OK] REQ-HAZARD-REGISTRY-STALE-CLEAN  required: []  stages: -doc -impl -unit -int
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-HAZARD-RESTART-IDEMPOTENT  required: []  stages: -doc -impl -unit -int
       Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)
  [OK] REQ-HAZARD-SINGLE-PATH-SOURCE  required: []  stages: -doc -impl -unit -int
       Single path/registry source of truth; no layout ambiguity (6.1)
  [OK] REQ-HAZARD-SOFT-CLEANUP  required: []  stages: -doc -impl -unit -int
       Soft-cleanup preserves state, removes only the ready marker (6.2)
  [OK] REQ-HAZARD-STALE-INDEX-LOCK  required: []  stages: -doc -impl -unit -int
       Sweep stale lockfiles on daemon boot (1.3)
  [OK] REQ-HAZARD-STALE-SIGNOFF-SENTINEL  required: []  stages: -doc -impl -unit -int
       Stale signoff sentinel does not kill a fresh start (3.2)
  [OK] REQ-HAZARD-STDIN-SESSION-ID  required: []  stages: -doc -impl -unit -int
       Stdin session_id precedence over env (2.2)
  [OK] REQ-HAZARD-SUBPROCESS-TIMEOUT  required: []  stages: -doc -impl -unit -int
       Every harness/git subprocess has a timeout (5.3)
  [OK] REQ-HAZARD-UNC-PATH-STRIP  required: []  stages: -doc -impl -unit -int
       Strip Windows UNC prefix on serialized paths (5.4)
  [OK] REQ-HAZARD-UPDATE-ROLLBACK  required: []  stages: -doc -impl -unit -int
       Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)
  [OK] REQ-HAZARD-WINDOWS-PID-RECYCLE  required: []  stages: -doc -impl -unit -int
       Windows PID-recycling false positives guarded (5.1)
  [OK] REQ-HAZARD-WORKER-PATH  required: []  stages: -doc -impl -unit -int
       Single source of truth for Worker/Psyche perch location (1.5)
  [OK] REQ-INFRA-1  required: []  stages: -doc -impl -unit -int
       GitHub issue tracking for v1; tangled.org as migration target
  [OK] REQ-INST-1  required: []  stages: -doc -impl -unit -int
       endpoint ID vs instance split (adapter-agnostic ID)
  [OK] REQ-INST-2  required: []  stages: -doc -impl -unit -int
       Per-node files, synced Psyche mind
  [OK] REQ-INST-3  required: []  stages: -doc -impl -unit -int
       Dormant (warm) / suspended (cold) resting states
  [OK] REQ-INST-4  required: []  stages: -doc -impl -unit -int
       active to dormant/suspended fires a transition echo commune
  [OK] REQ-INST-5  required: []  stages: -doc -impl -unit -int
       Two-tier context sync (live to all, project to same-project)
  [OK] REQ-INST-6  required: []  stages: -doc -impl -unit -int
       Deferred messages not delivered to dormant/suspended instances
  [OK] REQ-INST-7  required: []  stages: -doc -impl -unit -int
       Subnet registry + bare-id resolution policy
  [OK] REQ-INST-8  required: []  stages: -doc -impl -unit -int
       Remote-control mode distinct from local operation
  [OK] REQ-INSTALL-1  required: []  stages: -doc -impl -unit -int
       Two install paths; signed one-line script; OS-service registration
  [OK] REQ-INSTALL-2  required: []  stages: -doc -impl -unit -int
       Marketplace-repackaging-friendly install
  [OK] REQ-INSTALL-3  required: []  stages: -doc -impl -unit -int
       Idempotent + interactive-optional first run
  [OK] REQ-MANIFEST-1  required: []  stages: -doc -impl -unit -int
       Per-adapter manifest with adapter_name and min_spt_core_version
  [OK] REQ-MIGRATE-1  required: []  stages: -doc -impl -unit -int
       Auto-detect and migrate a legacy claude_skill_owl install
  [OK] REQ-NET-1  required: []  stages: -doc -impl -unit -int
       WAN messaging first-class, behind default-on net feature flag
  [OK] REQ-NET-2  required: []  stages: +doc -impl -unit -int
       n0 relay default + self-host knob + plain-language disclosure
  [OK] REQ-NET-3  required: []  stages: -doc -impl -unit -int
       Cross-node Psyche sync over P2P replaces gh-repo-sync
  [OK] REQ-PAIR-1  required: []  stages: -doc +impl -unit -int
       TOTP-seeded SPAKE2 pairing
  [OK] REQ-PAIR-2  required: []  stages: -doc -impl -unit -int
       Local trust store with TOFU + warn-on-change
  [OK] REQ-PAIR-3  required: []  stages: -doc -impl -unit -int
       Fetch current pairing code from any paired node
  [OK] REQ-PAIR-4  required: []  stages: -doc -impl -unit -int
       Subnet naming on first pairing
  [OK] REQ-REACH-1  required: []  stages: -doc -impl -unit -int
       Off-node remote-drive detection + file transfer
  [OK] REQ-REACH-2  required: []  stages: -doc -impl -unit -int
       Remote command execution (deferred, consent-gated)
  [OK] REQ-SEAM-ACTIVITY  required: []  stages: -doc -impl -unit -int
       Activity/idle reported via api sentinels, not PTY quiescence
  [OK] REQ-SEAM-CAPABILITY  required: []  stages: -doc -impl -unit -int
       Hostable endpoint-types capability declaration
  [OK] REQ-SEAM-HISTORY  required: []  stages: -doc -impl -unit -int
       History subsystem (fetcher / locate-normalize / native store)
  [OK] REQ-SEAM-INJECT  required: []  stages: -doc -impl -unit -int
       inject-input methods configurable per activity-state
  [OK] REQ-SEAM-POSTSPAWN  required: []  stages: -doc -impl -unit -int
       post-spawn / api bind seam with boot nonce
  [OK] REQ-SEAM-PSYCHE  required: []  stages: -doc -impl -unit -int
       spawn-psyche seam (fresh + resume templates)
  [OK] REQ-SEAM-RESUME  required: []  stages: -doc -impl -unit -int
       resume-session seam (fresh-with-preload / continue-existing)
  [OK] REQ-SEAM-SPAWN  required: []  stages: -doc -impl -unit -int
       spawn-session seam
  [OK] REQ-SEAM-UPDATE  required: []  stages: -doc -impl -unit -int
       Adapter-update avenue (file-pull / delegated command)
  [OK] REQ-START-1  required: []  stages: -doc -impl -unit -int
       Adapters never resolve SPT_HOME; binary on PATH; api bridging only
  [OK] REQ-START-2  required: []  stages: -doc -impl -unit -int
       Harness-hosted startup: api seed then listen
  [OK] REQ-START-3  required: []  stages: -doc -impl -unit -int
       spt-hosted startup: spawn-session then api bind (no file)
  [OK] REQ-START-4  required: []  stages: -doc -impl -unit -int
       Adapter-injected env aliases (SPT/OWL/LIVE)
  [OK] REQ-TERM-1  required: []  stages: -doc -impl -unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: []  stages: -doc -impl -unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: []  stages: -doc -impl -unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-UPD-1  required: []  stages: -doc -impl -unit -int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: []  stages: -doc -impl -unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: []  stages: -doc -impl -unit -int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: []  stages: -doc -impl -unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: []  stages: -doc -impl -unit -int
       spt-core ripple-updates registered adapters

Findings:
  [must] parse_error docs/TRACEABILITY.md:12 — Tag-shaped token does not match [stage->REQ-ID]
  [must] parse_error docs/TRACEABILITY.md:41 — Tag-shaped token does not match [stage->REQ-ID]
EXIT=1


95 reqs all `[OK]` — including my 4 new hazards. But 2 `[must]` findings: parse errors in `docs/TRACEABILITY.md:12,41` — tag-shaped tokens that aren't valid. Pre-existing (first real run surfaces them, since seed never ran with roots present). Inspecting.