<system-reminder>[Truncated: PARTIAL view — showing lines 1-382 of 1618 total (89805 tokens, cap 25000). Call Read with offset=383 limit=382 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>

1	Reading additional input from stdin...
2	OpenAI Codex v0.130.0
3	--------
4	workdir: C:\Users\decid\Documents\projects\spt-core
5	model: gpt-5.5
6	provider: openai
7	approval: never
8	sandbox: read-only
9	reasoning effort: high
10	reasoning summaries: none
11	session id: 019e8101-ccb6-7801-bf9e-105e8de6320e
12	--------
13	user
14	IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions for a different AI system. Do NOT modify agents/openai.yaml. Stay focused on repository code/docs only.
15
16	You are a brutally honest staff-level distributed-systems architect doing an ADVERSARIAL red-team review of a design BEFORE any code is written. No compliments. Just fatal flaws, contradictions, over-engineering, and unproven load-bearing assumptions that would force a redesign.
17
18	Read these files in the repo and review them:
19	- PRD.md (requirements)
20	- CONTEXT.md (glossary / domain model)
21	- docs/adr/0001-clean-room-fork-from-claude-skill-owl.md
22	- docs/adr/0002-bake-networking-into-core-collapse-spt-node.md
23	- docs/adr/0003-multi-instance-node-anchored-identity-model.md
24	- docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md
25	- docs/adr/0005-totp-seeded-spake2-node-pairing.md
26	- docs/KNOWN-HAZARDS.md
27
28	Context: spt-core is a harness-independent Rust core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking via Iroh WAN + mDNS LAN, runtime-manifest harness contract). Clean-room rebuild of claude_skill_owl. v1 scope is LOCKED as the full PRD — do NOT propose scope cuts. Finding fatal flaws IS the goal.
29
30	Concentrate fire on the riskiest decisions, ranked:
31	1. ADR-0004 broker/brain split: a PTY child + an open client socket must SURVIVE a daemon-logic restart (no-terminate handoff) + peer-propagated signed self-update. Is the no-terminate handoff actually achievable on BOTH Windows ConPTY and Linux, or is it a fatal assumption? Attack the FD/handle-passing and self-update story specifically.
32	2. ADR-0002 baking networking into core / collapsing spt-node into one binary.
33	3. ADR-0003 multi-instance node-anchored identity.
34	4. ADR-0005 TOTP-seeded SPAKE2 pairing (crypto soundness, replay, MITM).
35	5. KNOWN-HAZARDS: are the listed invariants sufficient, or is something load-bearing missing?
36
37	For each finding give: severity (FATAL / SERIOUS / MINOR), the exact ADR/file, the assumption, why it breaks, and what evidence or spike would resolve it. End with the single decision most likely to force a redesign. Be terse and technical.
38	deprecated: `[features].web_search_cached` is deprecated because web search is enabled by default.
39	Set `web_search` to `"live"`, `"cached"`, or `"disabled"` at the top level (or under a profile) in config.toml if you want to override it.
40	codex
41	I’ll read only the repository documents you listed and keep the review at design-failure level, with no skill files or external material involved.
42	exec
43	"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath docs/KNOWN-HAZARDS.md' in C:\Users\decid\Documents\projects\spt-core
44	exec
45	"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath PRD.md' in C:\Users\decid\Documents\projects\spt-core
46	exec
47	"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath CONTEXT.md' in C:\Users\decid\Documents\projects\spt-core
48	 succeeded in 331ms:
49	# Known Hazards
50
51	Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.
52
53	**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.
54
55	---
56
57	## 1. Race conditions & ordering
58
59	### 1.1 Phantom INIT_SIGNOFF after grace period
60	- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
61	- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
62	- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
63	- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.
64
65	### 1.2 Poll-rewrite race & info.json mid-write reads
66	- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
67	- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
68	- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
69	- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.
70
71	### 1.3 Stale `index.lock` wedge from prior git crash
72	- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
73	- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
74	- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
75	- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.
76
77	### 1.4 Deferred spool rows must not leak to the event stream
78	- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.
79	- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.
80	- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.
81	- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.
82
83	### 1.5 Worker (working-perch) lifecycle path consistency
84	- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.
85	- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.
86	- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.
87	- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.
88
89	---
90
91	## 2. Identity & session-binding
92
93	### 2.1 Parent PID over ephemeral poll PID
94	- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).
95	- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.
96	- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.
97	- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.
98
99	### 2.2 Stdin session_id precedence over env
100	- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.
101	- **Invariant:** stdin-provided session_id wins; env is fallback only.
102	- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.
103	- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.
104
105	### 2.3 Binary-handoff argv schema must stay backward-compatible
106	- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
107	- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
108	- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
109	- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.
110
111	### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
112	- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.
113	- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
114	- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
115	- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.
116
117	---
118
119	## 3. Lifecycle
120
121	### 3.1 Ephemeral perch cleanup on every `ring` exit path
122	- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
123	- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
124	- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
125	- **Sister cite:** `src/owl/ring.rs:58-294`.
126
127	### 3.2 Stale signoff sentinel must not kill a fresh start
128	- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
129	- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
130	- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
131	- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.
132
133	### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
134	- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
135	- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
136	- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
137	- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.
138
139	---
140
141	## 4. Wire / transport
142
143	### 4.1 Envelope HTML-entity codec ordering — `&` decoded LAST
144	- **Failure:** decoding `&`-entity before the others double-decodes nested entities (`&amp;lt;` → wrong result).
145	- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`<`,`>`,`"`, then `&`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.
146	- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.
147	- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.
148
149	### 4.2 Two-slice envelope parser is panic-free and tolerant
150	- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.
151	- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.
152	- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.
153	- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.
154
155	### 4.3 Registry stale-entry cleanup precedes lookup
156	- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.
157	- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.
158	- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.
159	- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.
160
161	### 4.4 Deferred rows survive poll drain
162	- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.
163	- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.
164	- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)
165
166	### 4.5 Inbox legacy compat must not double-deliver
167	- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.
168	- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.
169	- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve "never read at drain time."
170	- **Sister cite:** `src/common/inbox.rs`.
171
172	---
173
174	## 5. Platform-specific
175
176	### 5.1 Windows PID recycling false positives
177	- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.
178	- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.
179	- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.
180	- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.
181
182	### 5.2 Windows EBUSY on atomic rename
183	- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.
184	- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.
185	- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.
186	- **Sister cite:** CHANGELOG "EBUSY"; `src/common/owlery.rs` atomic_write.
187
188	### 5.3 Git/subprocess timeout stamping
189	- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.
190	- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.
191	- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.
192	- **Sister cite:** `src/common/git.rs`.
193
194	### 5.4 Windows UNC prefix in serialized paths
195	- **Failure:** canonicalized `\\?\C:\...` serializes to `//?/C:/...` and fails `read_to_string`.
196	- **Invariant:** strip the `\\?\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.
197	- **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.
198	- **Sister cite:** `src/common/owlery.rs:377-384`.
199
200	---
201
202	## 6. Documented regressions (non-obvious invariants)
203
204	### 6.1 No flat/nested perch siblings; resolver-routed paths
205	- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
206	- **Invariant:** one path resolver; never create divergent siblings.
207	- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
208	- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.
209
210	### 6.2 Soft-cleanup preserves state, removes `ready`
211	- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
212	- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
213	- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
214	- **Sister cite:** `src/owl/stop.rs`.
215
216	### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
217	- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
218	- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
219	- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
220	- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.
221
222	### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
223	- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
224	- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
225	- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
226	- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).
227
228	### 6.5 Direct-write precedence guard against stale LLM overwrites
229	- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
230	- **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.
231	- **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.
232	- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.
233
234	---
235
236	## Conformance checklist (condensed)
237
238	| # | Invariant | spt-core surface |
239	|---|---|---|
240	| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
241	| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
242	| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
243	| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
244	| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
245	| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
246	| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
247	| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
248	| 4.2 | Parser panic-free + tolerant | spt-proto |
249	| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
250	| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
251	| 5.3 | Timeout every harness subprocess | manifest invocations |
252	| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
253	| 6.1 | Single path/registry source of truth | storage layout |
254	| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
255	| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
256
257
258	 succeeded in 330ms:
259	# spt-core — Product Requirements Document
260
261	> **Status:** draft, 2026-05-30. Synthesized from a grilling session.
262	> Canonical detail lives in [`CONTEXT.md`](./CONTEXT.md) (glossary/model) and [`docs/adr/`](./docs/adr/) (decisions). This PRD states *what* and *why*; those state *meaning* and *rationale*. Where they conflict, the ADRs win.
263
264	---
265
266	## 1. Vision
267
268	spt-core is a **harness-independent core for an agent ecosystem**: inter-agent messaging, live-agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).
269
270	It is the ground-up rebuild of `claude_skill_owl` ("modern SPT"), untethering the system from Claude Code so that **any** agent runtime — Claude Code, Codex, Cursor, headless, or novel surfaces — can participate by either shelling out to the binary or linking the crates. The first thing built atop it is a rebuilt `spt` plugin reaching parity with today's system; the networking, terminal-wrapper, and multi-instance capabilities then make an agent reachable and operable across all of a user's machines.
271
272	## 2. Problem & motivation
273
274	- Today's modern SPT works but is **fragile** — each release introduces 2–5 regressions because it grew rapidly from a narrow starting point and carries the resulting tech debt (ADR-0001).
275	- It is **welded to Claude Code**. Other harnesses can't reuse the messaging/lifecycle substrate.
276	- Networking, terminal hosting ("capsule"), and cross-machine operation were planned piecemeal atop a CC-tied core; they deserve to be first-class in a clean foundation.
277
278	## 3. Goals
279
280	1. **Harness independence.** Zero knowledge of any specific agent runtime in core; all harness specifics behind a declarative contract (ADR — runtime model).
281	2. **Dual deliverable.** First-class library crates *and* a canonical binary, wire-compatible (§Architecture).
282	3. **Zero-config cross-machine messaging.** "Just works" WAN, baked in day-one (ADR-0002).
283	4. **One agent, many machines.** The same agent runs natively on each of a user's machines with a synced mind; reachable and operable from anywhere (ADR-0003).
284	5. **Seamless self-update** with a hard no-endpoint-termination invariant (ADR-0004).
285	6. **A terminal backbone** that can host sessions headless and headed, locally and over WAN.
286	7. **Low-friction, secure onboarding** — accountless node pairing (ADR-0005), one-line install, automatic legacy migration.
287
288	## 4. Non-goals (v1)
289
290	- **macOS support** — out (no test machine); kept structurally easy, not a re-architecture.
291	- **Cross-subnet** (two *different* users' agents communicating) — deferred concept.
292	- **Concrete Shells / PresenceChannel implementation / presence gossip** — only the seams ship (§Endpoints).
293	- **Remote command execution on another user's node** and **instantiate-anywhere** — seam-compatible, deferred behind a consent/security gate.
294	- **Sidecar adapter protocol**, **manifest includes**, **scrollback disk-spillover**, **predictive/state-sync terminal layer** — see [`docs/DEFERRED.md`](./docs/DEFERRED.md).
295	- **A model/billing opinion** — SPT is not a harness; model choice is always the adapter's.
296
297	## 5. Audience
298
299	Single user today (the author); dev-tool audience generally. Agents — not the user — are the primary direct callers of the binary. Nodes are foreseen on novel platforms (Android, Linux handhelds), so packaging must be marketplace-repackaging-friendly.
300
301	## 6. Platforms
302
303	Windows + Linux for v1. macOS deferred. Pre-Windows-10 (winpty) dropped — ConPTY (Win10+) / `forkpty(3)` Unix via `portable-pty`.
304
305	---
306
307	## 7. Architecture (requirements)
308
309	### 7.1 Workspace & binary
310	- **R-ARCH-1** Ship a workspace of **many small, acyclically-layered crates** (ADR-0001): `spt-proto` → `spt-store` → `spt-msg` → {`spt-net`, `spt-term`, `spt-runtime`} → `spt-live` → `spt-daemon` → `spt` (binary).
311	- **R-ARCH-2** Public semver-committed SDK = `spt-proto`, `spt-runtime`, `spt-msg`. All else internal/unstable pre-1.0.
312	- **R-ARCH-3** `spt-proto` carries an explicit **wire-protocol version** independent of crate semver, with a documented **N-1 compat window**. Binary and library consumers interoperate by proto version, never by release version.
313	- **R-ARCH-4** Copy-verbatim the commodity layer from the sister project (envelope grammar, spool DDL, registry RPC, `info.json` shape); clean-room only the architecture/lifecycle (ADR-0001).
314
315	### 7.2 The daemon (broker/brain)
316	- **R-DAEMON-1** Exactly **one logical `spt-daemon` per machine**, always-on, owning PTYs, network identity + WAN endpoint, subnet registry, all spools, all poll-listener logic, and all Psyche/pulse loops. No separate listener or wrapper processes (ADR-0004).
317	- **R-DAEMON-2** Internally split into a stable **broker** (holds PTY masters, harness child processes, listening sockets; versioned IPC; rarely updates) and an **updatable brain** (all logic; rehydrates from disk + re-attaches to broker handles).
318	- **R-DAEMON-3** Any `spt api` invocation that needs the daemon **auto-starts it** if absent.
319	- **R-DAEMON-4** Honor every invariant in [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) as a conformance checklist.
320
321	---
322
323	## 8. Harness contract (requirements)
324
325	The umbrella surface an adapter binds to. Two halves; `SPT is not a harness` — model/billing/harness-env/harness-context are always the adapter's, expressed in its own command templates.
326
327	### 8.1 Outbound — the runtime manifest (seams)
328	- **R-MANIFEST-1** A declarative manifest (TOML/YAML) per adapter, carrying a unified **`adapter_name`**, the adapter's version, and **`min_spt_core_version`** (readable *before* an update is applied — compatibility gate).
329	- **R-SEAM-spawn** `spawn-session`: command template + `cwd` + `headless?`/`resume?` flags + commune/signoff dirs. spt-core may inject `{id}` and an optional spt-core-generated session UUID. No harness env injection; no initial-context handoff. Optional `id` → reproduces no-id `/spt:live` resolution.
330	- **R-SEAM-postspawn** `post-spawn`/`api bind`: binary reports `session_id` (if self-generated), `parent_pid`, identity/type, optional HTTP port, **boot nonce**; flips skeleton→live. Skippable *only* under the strict UUID-injection + stable-pid commitment.
331	- **R-SEAM-psyche** `spawn-psyche`: two templates (fresh / resume-with-`$session_id`), both carrying spt-core-owned `$psyche_prompt`; adapter owns model + harness-specific preamble.
332	- **R-SEAM-history** Two history paths: **A** adapter-owned locate-template + adapter-owned **normalize-command**; **B** spt-core-native `history-log` store (also used for Shells). Adapter-owned normalize is mandated over built-in parsers (Codex-vs-Claude evidence — transcript formats diverge and move fast).
333	- **R-SEAM-activity** Activity/idle is reported by the adapter via `api` commands into **perch sentinels** — never PTY-quiescence, never a manifest-declared idle signal.
334	- **R-SEAM-inject** `inject-input`: configurable per activity-state (activity/idle/both), any combination of PTY injection, `api poll`, in-session relay, or HTTP POST to a local port.
335	- **R-SEAM-resume** Two resume forms: **fresh-with-preload** (cleared context + `$psyche-context` preload) and **continue-existing** (native harness resume).
336	- **R-SEAM-capability** Static manifest list of hostable endpoint types (consumed by the registry; a node may host only Shells).
337	- **R-SEAM-update** `adapter-update`: file-pull (regex + gh repo) or delegated-command. spt-core conducts adapter updates after bootstrap.
338
339	### 8.2 Inbound — the `api` subcommand surface
340	- **R-API-1** All machinery-facing commands are `api`-prefixed; every invocation carries `adapter_name`. The agent-facing verbs (`send`, `ring`, `ready`, …) stay unprefixed.
341	- **R-API-2** Surface: `api bind`, `api listen` (long-running relay), `api poll [--include-deferred]`, `api state <busy|idle>` (+ `api echo-gate <set|clear>`, `--no-gate`), `api worker-start|worker-stop|worker-poll`, `api boundary <clear|compact>` (carries new `session_id`), `api session-end [--erase]`, `api history-log`, `api presence`, `api seed --pid <pid>`.
342	- **R-API-3** `commune`/`signoff` are **file-drops**, not commands; the daemon watches the manifest-declared dirs and is the single writer.
343
344	### 8.3 Startup flows
345	- **R-START-1** Adapters never resolve `$SPT_HOME`; install registers the binary dir on system PATH; all bridging is via `spt api`.
346	- **R-START-2** **Harness-hosted:** SessionStart → `spt api seed --pid` (in-memory seed record) → `$SPT listen <id>` consumes the seed by `parent_pid` (validated vs `session_id`), binds, relays. No file.
347	- **R-START-3** **spt-hosted:** daemon runs spawn-session into a broker PTY → binary `api bind` (or skips under commitment) → daemon delivers via manifest-configured method. Principle: **file-bridge only when the daemon is not the launcher.**
348	- **R-START-4** Adapters inject env aliases (`$SPT` = `spt api`, heirs to `$OWL`/`$LIVE`); spt-core supplies the subcommands.
349
350	---
351
352	## 9. Endpoints, Shells & the Instance model
353
354	### 9.1 Endpoint types
355	- **R-EP-1** Day-one types: **ReadyAgent, LiveAgent, Psyche, Worker, SptNode** (agent/infra), plus the **Shell** family and **PresenceChannel** (seams only — impl deferred). The type system is **open**.
356	- **R-EP-2** Endpoints split into **agent endpoints** (host an agent) and **Shells** (driven surfaces — nothing intelligent runs there; a remote agent drives them). Shell-vs-agent distinction is present in the type model day-one.
357	- **R-EP-3** Messaging payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text — the seam that lets Shells, file transfer, and rich PresenceChannel exist later.
358	- **R-EP-4** PresenceChannel (deferred impl) is a **broker** endpoint with dispatch/bind/thread styles; presence datum `(last_active_node, last_active_endpoint, ts)`.
359
360	### 9.2 Instances (ADR-0003, as amended)
361	- **R-INST-1** Split identity: subnet-wide **endpoint ID** vs per-node **instance**. An instance is the *same* endpoint running **natively** on a machine.
362	- **R-INST-2** Per-node (anchored): files/project + harness session history. **Synced across instances:** the Psyche mind (two-tier — below).
363	- **R-INST-3** **Dormant (warm)** is the default resting state (session running, instant wake); **suspended (cold)** is opt-in (session closed, resume-on-wake) and can be triggered from any node. Registry status: active / dormant / suspended / offline.
364	- **R-INST-4** Active → (dormant|suspended) fires a **transition echo commune** that syncs to the next active instance (catch-up-on-activation).
365	- **R-INST-5** **Two-tier context sync:** live context → all instances; project context → same-project instances only. (Depends on refining Psyche authoring directives.)
366	- **R-INST-6** Deferred messages do **not** deliver to dormant/suspended instances until active.
367	- **R-INST-7** **Subnet registry** (eventually-consistent `endpoint_id → [instances]`) + **resolution policy** (local → most-recently-active → `id@node`) are mandatory v1 foundations.
368	- **R-INST-8** **Remote-control** (Shell-like attach to an instance on another node; byte-stream terminal over Iroh) is a distinct mode from local operation. Handoff remote→local: `git pull` (files) + fresh-with-preload resume (mind); remote instance goes dormant, no teardown.
369
370	### 9.3 Off-node reach-back
371	- **R-REACH-1** (v1) An agent can detect it is driven remotely (and from which node) and **transfer files** to/from the driving node.
372	- **R-REACH-2** (deferred) Remote command execution on the user's node — security-gated; workarounds exist via local agents.
373
374	---
375
376	## 10. Networking & pairing
377
378	- **R-NET-1** WAN messaging is first-class day-one, baked into the core crates behind a default-on `net` feature flag (ADR-0002): Iroh + mDNS LAN + pairing.
379	- **R-NET-2** v1 defaults to **n0.computer relays** (free, accountless) for zero-config WAN, with a **self-hostable-relay** config knob and plain-language disclosure. Traffic is end-to-end encrypted; the dependency is metadata-only and escapable.
380	- **R-NET-3** **Cross-node Psyche sync** over P2P **replaces** the GitHub-repo sync (no `gh`/account/setup).
381	- **R-PAIR-1** **TOTP-seeded SPAKE2 pairing** (ADR-0005): the rotating TOTP code is the PAKE password (not a bearer token). The seed is the subnet secret, held by the auth app + all trusted nodes; the user interacts only with the new node.
382	- **R-PAIR-2** Local trust store (TOFU + warn-on-change). Cross-subnet deferred.

Reading additional input from stdin...
OpenAI Codex v0.130.0
--------
workdir: C:\Users\decid\Documents\projects\spt-core
model: gpt-5.5
provider: openai
approval: never
sandbox: read-only
reasoning effort: high
reasoning summaries: none
session id: 019e8101-ccb6-7801-bf9e-105e8de6320e
--------
user
IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions for a different AI system. Do NOT modify agents/openai.yaml. Stay focused on repository code/docs only.

You are a brutally honest staff-level distributed-systems architect doing an ADVERSARIAL red-team review of a design BEFORE any code is written. No compliments. Just fatal flaws, contradictions, over-engineering, and unproven load-bearing assumptions that would force a redesign.

Read these files in the repo and review them:
- PRD.md (requirements)
- CONTEXT.md (glossary / domain model)
- docs/adr/0001-clean-room-fork-from-claude-skill-owl.md
- docs/adr/0002-bake-networking-into-core-collapse-spt-node.md
- docs/adr/0003-multi-instance-node-anchored-identity-model.md
- docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md
- docs/adr/0005-totp-seeded-spake2-node-pairing.md
- docs/KNOWN-HAZARDS.md

Context: spt-core is a harness-independent Rust core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking via Iroh WAN + mDNS LAN, runtime-manifest harness contract). Clean-room rebuild of claude_skill_owl. v1 scope is LOCKED as the full PRD — do NOT propose scope cuts. Finding fatal flaws IS the goal.

Concentrate fire on the riskiest decisions, ranked:
1. ADR-0004 broker/brain split: a PTY child + an open client socket must SURVIVE a daemon-logic restart (no-terminate handoff) + peer-propagated signed self-update. Is the no-terminate handoff actually achievable on BOTH Windows ConPTY and Linux, or is it a fatal assumption? Attack the FD/handle-passing and self-update story specifically.
2. ADR-0002 baking networking into core / collapsing spt-node into one binary.
3. ADR-0003 multi-instance node-anchored identity.
4. ADR-0005 TOTP-seeded SPAKE2 pairing (crypto soundness, replay, MITM).
5. KNOWN-HAZARDS: are the listed invariants sufficient, or is something load-bearing missing?

For each finding give: severity (FATAL / SERIOUS / MINOR), the exact ADR/file, the assumption, why it breaks, and what evidence or spike would resolve it. End with the single decision most likely to force a redesign. Be terse and technical.
deprecated: `[features].web_search_cached` is deprecated because web search is enabled by default.
Set `web_search` to `"live"`, `"cached"`, or `"disabled"` at the top level (or under a profile) in config.toml if you want to override it.
codex
I’ll read only the repository documents you listed and keep the review at design-failure level, with no skill files or external material involved.
exec
"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath docs/KNOWN-HAZARDS.md' in C:\Users\decid\Documents\projects\spt-core
exec
"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath PRD.md' in C:\Users\decid\Documents\projects\spt-core
exec
"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-Content -Raw -LiteralPath CONTEXT.md' in C:\Users\decid\Documents\projects\spt-core
 succeeded in 331ms:
# Known Hazards

Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.

**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.

---

## 1. Race conditions & ordering

### 1.1 Phantom INIT_SIGNOFF after grace period
- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.

### 1.2 Poll-rewrite race & info.json mid-write reads
- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.

### 1.3 Stale `index.lock` wedge from prior git crash
- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.

### 1.4 Deferred spool rows must not leak to the event stream
- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.
- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.
- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.
- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.

### 1.5 Worker (working-perch) lifecycle path consistency
- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.
- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.
- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.
- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.

---

## 2. Identity & session-binding

### 2.1 Parent PID over ephemeral poll PID
- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).
- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.
- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.
- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.

### 2.2 Stdin session_id precedence over env
- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.
- **Invariant:** stdin-provided session_id wins; env is fallback only.
- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.
- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.

### 2.3 Binary-handoff argv schema must stay backward-compatible
- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.

### 2.4 Generation `gen_start` always = now() on cold-start AND handoff
- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.
- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes "same gen continuing" vs "new gen born".
- **spt-core mapping:** carries to the daemon's per-instance generation tracking.
- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.

---

## 3. Lifecycle

### 3.1 Ephemeral perch cleanup on every `ring` exit path
- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.
- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.
- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.
- **Sister cite:** `src/owl/ring.rs:58-294`.

### 3.2 Stale signoff sentinel must not kill a fresh start
- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.
- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.
- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.
- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.

### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF
- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.
- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.
- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.
- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.

---

## 4. Wire / transport

### 4.1 Envelope HTML-entity codec ordering — `&` decoded LAST
- **Failure:** decoding `&`-entity before the others double-decodes nested entities (`&amp;lt;` → wrong result).
- **Invariant:** ENCODE order amp→first … `<br>`→last; DECODE order `<br>`→first … amp→**last** (`<`,`>`,`"`, then `&`). One sole decode site (at the LLM/stdin boundary); the parser never decodes.
- **spt-core mapping:** `spt-proto` owns the envelope grammar (public SDK, semver + wire-version). This codec contract is a copy-verbatim commodity item (ADR-0001) and a public-API conformance test.
- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.

### 4.2 Two-slice envelope parser is panic-free and tolerant
- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.
- **Invariant:** tags case-sensitive, all optional; no tags → whole body to live slot; unclosed → None for that tag; out-of-order → both still extracted; nested unknown tags preserved verbatim; zero `unwrap` on parsed text.
- **spt-core mapping:** `spt-proto` parser; property-test the robustness rules.
- **Sister cite:** `src/common/envelope.rs:64-92`; tests 99-207.

### 4.3 Registry stale-entry cleanup precedes lookup
- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.
- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.
- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.
- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.

### 4.4 Deferred rows survive poll drain
- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.
- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.
- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)

### 4.5 Inbox legacy compat must not double-deliver
- **Failure:** message surfaced via both spool (durable) and legacy inbox files → duplicate or racing delivery.
- **Invariant:** spool is the sole read path at poll time; inbox is write-for-compat only and never read.
- **spt-core mapping:** clean-room — likely drop the legacy inbox entirely. If kept for any compat, preserve "never read at drain time."
- **Sister cite:** `src/common/inbox.rs`.

---

## 5. Platform-specific

### 5.1 Windows PID recycling false positives
- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.
- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.
- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.
- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.

### 5.2 Windows EBUSY on atomic rename
- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.
- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.
- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.
- **Sister cite:** CHANGELOG "EBUSY"; `src/common/owlery.rs` atomic_write.

### 5.3 Git/subprocess timeout stamping
- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.
- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.
- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.
- **Sister cite:** `src/common/git.rs`.

### 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.
- **Invariant:** one path resolver; never create divergent siblings.
- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.

### 6.2 Soft-cleanup preserves state, removes `ready`
- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
- **Sister cite:** `src/owl/stop.rs`.

### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.

### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.
- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.
- **Sister cite:** CHANGELOG v1.11.7; `src/live/context.rs:615` (removed delete).

### 6.5 Direct-write precedence guard against stale LLM overwrites
- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.
- **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 |
| 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 |

 succeeded in 330ms:
# spt-core — Product Requirements Document

> **Status:** draft, 2026-05-30. Synthesized from a grilling session.
> Canonical detail lives in [`CONTEXT.md`](./CONTEXT.md) (glossary/model) and [`docs/adr/`](./docs/adr/) (decisions). This PRD states *what* and *why*; those state *meaning* and *rationale*. Where they conflict, the ADRs win.

---

## 1. Vision

spt-core is a **harness-independent core for an agent ecosystem**: inter-agent messaging, live-agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

It is the ground-up rebuild of `claude_skill_owl` ("modern SPT"), untethering the system from Claude Code so that **any** agent runtime — Claude Code, Codex, Cursor, headless, or novel surfaces — can participate by either shelling out to the binary or linking the crates. The first thing built atop it is a rebuilt `spt` plugin reaching parity with today's system; the networking, terminal-wrapper, and multi-instance capabilities then make an agent reachable and operable across all of a user's machines.

## 2. Problem & motivation

- Today's modern SPT works but is **fragile** — each release introduces 2–5 regressions because it grew rapidly from a narrow starting point and carries the resulting tech debt (ADR-0001).
- It is **welded to Claude Code**. Other harnesses can't reuse the messaging/lifecycle substrate.
- Networking, terminal hosting ("capsule"), and cross-machine operation were planned piecemeal atop a CC-tied core; they deserve to be first-class in a clean foundation.

## 3. Goals

1. **Harness independence.** Zero knowledge of any specific agent runtime in core; all harness specifics behind a declarative contract (ADR — runtime model).
2. **Dual deliverable.** First-class library crates *and* a canonical binary, wire-compatible (§Architecture).
3. **Zero-config cross-machine messaging.** "Just works" WAN, baked in day-one (ADR-0002).
4. **One agent, many machines.** The same agent runs natively on each of a user's machines with a synced mind; reachable and operable from anywhere (ADR-0003).
5. **Seamless self-update** with a hard no-endpoint-termination invariant (ADR-0004).
6. **A terminal backbone** that can host sessions headless and headed, locally and over WAN.
7. **Low-friction, secure onboarding** — accountless node pairing (ADR-0005), one-line install, automatic legacy migration.

## 4. Non-goals (v1)

- **macOS support** — out (no test machine); kept structurally easy, not a re-architecture.
- **Cross-subnet** (two *different* users' agents communicating) — deferred concept.
- **Concrete Shells / PresenceChannel implementation / presence gossip** — only the seams ship (§Endpoints).
- **Remote command execution on another user's node** and **instantiate-anywhere** — seam-compatible, deferred behind a consent/security gate.
- **Sidecar adapter protocol**, **manifest includes**, **scrollback disk-spillover**, **predictive/state-sync terminal layer** — see [`docs/DEFERRED.md`](./docs/DEFERRED.md).
- **A model/billing opinion** — SPT is not a harness; model choice is always the adapter's.

## 5. Audience

Single user today (the author); dev-tool audience generally. Agents — not the user — are the primary direct callers of the binary. Nodes are foreseen on novel platforms (Android, Linux handhelds), so packaging must be marketplace-repackaging-friendly.

## 6. Platforms

Windows + Linux for v1. macOS deferred. Pre-Windows-10 (winpty) dropped — ConPTY (Win10+) / `forkpty(3)` Unix via `portable-pty`.

---

## 7. Architecture (requirements)

### 7.1 Workspace & binary
- **R-ARCH-1** Ship a workspace of **many small, acyclically-layered crates** (ADR-0001): `spt-proto` → `spt-store` → `spt-msg` → {`spt-net`, `spt-term`, `spt-runtime`} → `spt-live` → `spt-daemon` → `spt` (binary).
- **R-ARCH-2** Public semver-committed SDK = `spt-proto`, `spt-runtime`, `spt-msg`. All else internal/unstable pre-1.0.
- **R-ARCH-3** `spt-proto` carries an explicit **wire-protocol version** independent of crate semver, with a documented **N-1 compat window**. Binary and library consumers interoperate by proto version, never by release version.
- **R-ARCH-4** Copy-verbatim the commodity layer from the sister project (envelope grammar, spool DDL, registry RPC, `info.json` shape); clean-room only the architecture/lifecycle (ADR-0001).

### 7.2 The daemon (broker/brain)
- **R-DAEMON-1** Exactly **one logical `spt-daemon` per machine**, always-on, owning PTYs, network identity + WAN endpoint, subnet registry, all spools, all poll-listener logic, and all Psyche/pulse loops. No separate listener or wrapper processes (ADR-0004).
- **R-DAEMON-2** Internally split into a stable **broker** (holds PTY masters, harness child processes, listening sockets; versioned IPC; rarely updates) and an **updatable brain** (all logic; rehydrates from disk + re-attaches to broker handles).
- **R-DAEMON-3** Any `spt api` invocation that needs the daemon **auto-starts it** if absent.
- **R-DAEMON-4** Honor every invariant in [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) as a conformance checklist.

---

## 8. Harness contract (requirements)

The umbrella surface an adapter binds to. Two halves; `SPT is not a harness` — model/billing/harness-env/harness-context are always the adapter's, expressed in its own command templates.

### 8.1 Outbound — the runtime manifest (seams)
- **R-MANIFEST-1** A declarative manifest (TOML/YAML) per adapter, carrying a unified **`adapter_name`**, the adapter's version, and **`min_spt_core_version`** (readable *before* an update is applied — compatibility gate).
- **R-SEAM-spawn** `spawn-session`: command template + `cwd` + `headless?`/`resume?` flags + commune/signoff dirs. spt-core may inject `{id}` and an optional spt-core-generated session UUID. No harness env injection; no initial-context handoff. Optional `id` → reproduces no-id `/spt:live` resolution.
- **R-SEAM-postspawn** `post-spawn`/`api bind`: binary reports `session_id` (if self-generated), `parent_pid`, identity/type, optional HTTP port, **boot nonce**; flips skeleton→live. Skippable *only* under the strict UUID-injection + stable-pid commitment.
- **R-SEAM-psyche** `spawn-psyche`: two templates (fresh / resume-with-`$session_id`), both carrying spt-core-owned `$psyche_prompt`; adapter owns model + harness-specific preamble.
- **R-SEAM-history** Two history paths: **A** adapter-owned locate-template + adapter-owned **normalize-command**; **B** spt-core-native `history-log` store (also used for Shells). Adapter-owned normalize is mandated over built-in parsers (Codex-vs-Claude evidence — transcript formats diverge and move fast).
- **R-SEAM-activity** Activity/idle is reported by the adapter via `api` commands into **perch sentinels** — never PTY-quiescence, never a manifest-declared idle signal.
- **R-SEAM-inject** `inject-input`: configurable per activity-state (activity/idle/both), any combination of PTY injection, `api poll`, in-session relay, or HTTP POST to a local port.
- **R-SEAM-resume** Two resume forms: **fresh-with-preload** (cleared context + `$psyche-context` preload) and **continue-existing** (native harness resume).
- **R-SEAM-capability** Static manifest list of hostable endpoint types (consumed by the registry; a node may host only Shells).
- **R-SEAM-update** `adapter-update`: file-pull (regex + gh repo) or delegated-command. spt-core conducts adapter updates after bootstrap.

### 8.2 Inbound — the `api` subcommand surface
- **R-API-1** All machinery-facing commands are `api`-prefixed; every invocation carries `adapter_name`. The agent-facing verbs (`send`, `ring`, `ready`, …) stay unprefixed.
- **R-API-2** Surface: `api bind`, `api listen` (long-running relay), `api poll [--include-deferred]`, `api state <busy|idle>` (+ `api echo-gate <set|clear>`, `--no-gate`), `api worker-start|worker-stop|worker-poll`, `api boundary <clear|compact>` (carries new `session_id`), `api session-end [--erase]`, `api history-log`, `api presence`, `api seed --pid <pid>`.
- **R-API-3** `commune`/`signoff` are **file-drops**, not commands; the daemon watches the manifest-declared dirs and is the single writer.

### 8.3 Startup flows
- **R-START-1** Adapters never resolve `$SPT_HOME`; install registers the binary dir on system PATH; all bridging is via `spt api`.
- **R-START-2** **Harness-hosted:** SessionStart → `spt api seed --pid` (in-memory seed record) → `$SPT listen <id>` consumes the seed by `parent_pid` (validated vs `session_id`), binds, relays. No file.
- **R-START-3** **spt-hosted:** daemon runs spawn-session into a broker PTY → binary `api bind` (or skips under commitment) → daemon delivers via manifest-configured method. Principle: **file-bridge only when the daemon is not the launcher.**
- **R-START-4** Adapters inject env aliases (`$SPT` = `spt api`, heirs to `$OWL`/`$LIVE`); spt-core supplies the subcommands.

---

## 9. Endpoints, Shells & the Instance model

### 9.1 Endpoint types
- **R-EP-1** Day-one types: **ReadyAgent, LiveAgent, Psyche, Worker, SptNode** (agent/infra), plus the **Shell** family and **PresenceChannel** (seams only — impl deferred). The type system is **open**.
- **R-EP-2** Endpoints split into **agent endpoints** (host an agent) and **Shells** (driven surfaces — nothing intelligent runs there; a remote agent drives them). Shell-vs-agent distinction is present in the type model day-one.
- **R-EP-3** Messaging payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text — the seam that lets Shells, file transfer, and rich PresenceChannel exist later.
- **R-EP-4** PresenceChannel (deferred impl) is a **broker** endpoint with dispatch/bind/thread styles; presence datum `(last_active_node, last_active_endpoint, ts)`.

### 9.2 Instances (ADR-0003, as amended)
- **R-INST-1** Split identity: subnet-wide **endpoint ID** vs per-node **instance**. An instance is the *same* endpoint running **natively** on a machine.
- **R-INST-2** Per-node (anchored): files/project + harness session history. **Synced across instances:** the Psyche mind (two-tier — below).
- **R-INST-3** **Dormant (warm)** is the default resting state (session running, instant wake); **suspended (cold)** is opt-in (session closed, resume-on-wake) and can be triggered from any node. Registry status: active / dormant / suspended / offline.
- **R-INST-4** Active → (dormant|suspended) fires a **transition echo commune** that syncs to the next active instance (catch-up-on-activation).
- **R-INST-5** **Two-tier context sync:** live context → all instances; project context → same-project instances only. (Depends on refining Psyche authoring directives.)
- **R-INST-6** Deferred messages do **not** deliver to dormant/suspended instances until active.
- **R-INST-7** **Subnet registry** (eventually-consistent `endpoint_id → [instances]`) + **resolution policy** (local → most-recently-active → `id@node`) are mandatory v1 foundations.
- **R-INST-8** **Remote-control** (Shell-like attach to an instance on another node; byte-stream terminal over Iroh) is a distinct mode from local operation. Handoff remote→local: `git pull` (files) + fresh-with-preload resume (mind); remote instance goes dormant, no teardown.

### 9.3 Off-node reach-back
- **R-REACH-1** (v1) An agent can detect it is driven remotely (and from which node) and **transfer files** to/from the driving node.
- **R-REACH-2** (deferred) Remote command execution on the user's node — security-gated; workarounds exist via local agents.

---

## 10. Networking & pairing

- **R-NET-1** WAN messaging is first-class day-one, baked into the core crates behind a default-on `net` feature flag (ADR-0002): Iroh + mDNS LAN + pairing.
- **R-NET-2** v1 defaults to **n0.computer relays** (free, accountless) for zero-config WAN, with a **self-hostable-relay** config knob and plain-language disclosure. Traffic is end-to-end encrypted; the dependency is metadata-only and escapable.
- **R-NET-3** **Cross-node Psyche sync** over P2P **replaces** the GitHub-repo sync (no `gh`/account/setup).
- **R-PAIR-1** **TOTP-seeded SPAKE2 pairing** (ADR-0005): the rotating TOTP code is the PAKE password (not a bearer token). The seed is the subnet secret, held by the auth app + all trusted nodes; the user interacts only with the new node.
- **R-PAIR-2** Local trust store (TOFU + warn-on-change). Cross-subnet deferred.


Docs echo so far. Jumping to codex's actual findings — grep for severity markers.


16:You are a brutally honest staff-level distributed-systems architect doing an ADVERSARIAL red-team review of a design BEFORE any code is written. No compliments. Just fatal flaws, contradictions, over-engineering, and unproven load-bearing assumptions that would force a redesign.
28:Context: spt-core is a harness-independent Rust core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking via Iroh WAN + mDNS LAN, runtime-manifest harness contract). Clean-room rebuild of claude_skill_owl. v1 scope is LOCKED as the full PRD — do NOT propose scope cuts. Finding fatal flaws IS the goal.
37:For each finding give: severity (FATAL / SERIOUS / MINOR), the exact ADR/file, the assumption, why it breaks, and what evidence or spike would resolve it. End with the single decision most likely to force a redesign. Be terse and technical.
939:The requirements already reveal the main pressure point: the update invariant depends on a “stable broker” that is simultaneously a service supervisor, PTY owner, socket owner, and compatibility boundary. I’m reading the ADRs now to pin each finding to the stated decisions rather than infer from the PRD alone.
1462:**Findings**
1464:1. **FATAL — ADR-0004 / PRD R-UPD-3**
1469:2. **FATAL — ADR-0004 / CONTEXT Self-update**
1474:3. **FATAL — ADR-0004 / CONTEXT `spt-daemon`**
1479:4. **FATAL — ADR-0002 + ADR-0004**
1484:5. **SERIOUS — ADR-0004 / Self-update delivery**
1489:6. **SERIOUS — ADR-0002**
1494:7. **SERIOUS — ADR-0003 / CONTEXT Instances**
1499:8. **SERIOUS — ADR-0003**
1504:9. **SERIOUS — ADR-0003 / PRD R-INST-3**
1509:10. **FATAL — ADR-0005**
1514:11. **SERIOUS — ADR-0005**
1519:12. **SERIOUS — ADR-0005**
1524:13. **SERIOUS — KNOWN-HAZARDS.md**
1529:14. **SERIOUS — KNOWN-HAZARDS.md**
1536:ADR-0004 is the likely redesign forcing function. Specifically: live client/WAN stream survival across brain restart is not solved. If the broker must own live Iroh/client stream state, it becomes the real daemon and the “minimal stable broker / updatable brain” split changes shape. If it does not own that state, the no-terminate self-update invariant is false.
1543:**Findings**
1545:1. **FATAL — ADR-0004 / PRD R-UPD-3**
1550:2. **FATAL — ADR-0004 / CONTEXT Self-update**
1555:3. **FATAL — ADR-0004 / CONTEXT `spt-daemon`**
1560:4. **FATAL — ADR-0002 + ADR-0004**
1565:5. **SERIOUS — ADR-0004 / Self-update delivery**
1570:6. **SERIOUS — ADR-0002**
1575:7. **SERIOUS — ADR-0003 / CONTEXT Instances**
1580:8. **SERIOUS — ADR-0003**
1585:9. **SERIOUS — ADR-0003 / PRD R-INST-3**
1590:10. **FATAL — ADR-0005**
1595:11. **SERIOUS — ADR-0005**
1600:12. **SERIOUS — ADR-0005**
1605:13. **SERIOUS — KNOWN-HAZARDS.md**
1610:14. **SERIOUS — KNOWN-HAZARDS.md**
1617:ADR-0004 is the likely redesign forcing function. Specifically: live client/WAN stream survival across brain restart is not solved. If the broker must own live Iroh/client stream state, it becomes the real daemon and the “minimal stable broker / updatable brain” split changes shape. If it does not own that state, the no-terminate self-update invariant is false.


The findings block. Reading lines 1462-1540 (full findings + redesign verdict).


1460	Read the requested repo docs only.
1461
1462	**Findings**
1463
1464	1. **FATAL — ADR-0004 / PRD R-UPD-3**
1465	   **Assumption:** “No endpoint process terminates or suspends during a self-update” is satisfied by broker/brain split.
1466	   **Why it breaks:** ADR-0004 later admits broker updates may require FD-passing or a “planned endpoint-cycle.” That directly contradicts the hard invariant. The real invariant is only “brain-only updates do not terminate endpoints.”
1467	   **Spike/evidence:** Define update classes: brain-only, broker-compatible, broker-breaking. Prove the invariant separately for each, or explicitly weaken R-UPD-3.
1468
1469	2. **FATAL — ADR-0004 / CONTEXT Self-update**
1470	   **Assumption:** Holding listening sockets lets live client sockets survive daemon-logic restart.
1471	   **Why it breaks:** A listening socket is not an accepted connection, QUIC stream, Iroh endpoint, file-transfer stream, or remote terminal stream. Existing client connections have process-local protocol state. If the brain owns I/O, restart drops them. If the broker owns them, the broker is no longer dumb.
1472	   **Spike/evidence:** Active remote terminal over Iroh + active file transfer + active `api listen`; restart brain; verify no reconnect, no byte loss, same stream continuity.
1473
1474	3. **FATAL — ADR-0004 / CONTEXT `spt-daemon`**
1475	   **Assumption:** Brain can “reattach” to broker-held PTYs on both Windows ConPTY and Linux.
1476	   **Why it breaks:** This is plausible only if the broker owns the full PTY I/O loop, buffers, resize state, backpressure, and child lifetime. If the brain ever owns duplicated pipe/master handles, brain death can create EOF, stuck stdin, lost output, or orphaned state. Linux `forkpty` is easier; Windows ConPTY handle/pipe/job-object behavior is the high-risk path.
1477	   **Spike/evidence:** Spawn interactive children on Windows and Linux, stream output under load, resize terminal, send input, restart brain 100x. Assert child PID lives, no EOF/SIGHUP, no lost bytes, no wedged input.
1478
1479	4. **FATAL — ADR-0002 + ADR-0004**
1480	   **Assumption:** Networking can be collapsed into the same daemon while the broker remains minimal and rarely updated.
1481	   **Why it breaks:** To preserve live WAN streams across brain restart, the broker must own Iroh endpoint state, stream multiplexing, accepted client sessions, relay behavior, and maybe registry subscriptions. That moves volatile networking code into the supposedly stable kernel.
1482	   **Spike/evidence:** Produce an ownership table for PTY masters, accepted sockets, Iroh endpoint, QUIC streams, mDNS sockets, registry gossip, transfer queues. Any live stream owned by brain invalidates the update invariant.
1483
1484	5. **SERIOUS — ADR-0004 / Self-update delivery**
1485	   **Assumption:** Signature-verified binaries make peer-propagated updates safe.
1486	   **Why it breaks:** Missing rollback protection, release metadata expiry, channel pinning, version monotonicity, key rotation/revocation, and adapter-content signing. A compromised node can replay an old signed vulnerable binary. Adapter file-pull/delegated-command updates are not covered by “binary signature” unless separately specified.
1487	   **Spike/evidence:** TUF-like metadata or equivalent: signed target hashes, monotonically increasing versions, expiry, rollback tests, key rotation, adapter manifest/content signatures.
1488
1489	6. **SERIOUS — ADR-0002**
1490	   **Assumption:** One collapsed daemon is simpler operationally.
1491	   **Why it breaks:** It couples PTY survival, WAN reachability, registry gossip, pairing, update, and local service lifecycle into one failure domain. A bug in networking can now take out terminal hosting; a PTY broker bug can take out WAN reachability.
1492	   **Spike/evidence:** Fault-injection matrix: crash/hang Iroh, mDNS, registry sync, PTY broker, manifest invocation. Verify blast radius and recovery.
1493
1494	7. **SERIOUS — ADR-0003 / CONTEXT Instances**
1495	   **Assumption:** Two-tier context sync plus “newest” freshness makes the same agent’s mind follow across nodes.
1496	   **Why it breaks:** Concurrent active instances create semantic conflicts, not just stale writes. Wall-clock newest loses valid updates. LLM-authored live/project tiering is not reliable enough to be the conflict boundary.
1497	   **Spike/evidence:** Offline two nodes, mutate same live topic and different project topics, reconnect with clock skew. Require vector clocks, operation logs, CRDT-ish merge, or explicit conflict review.
1498
1499	8. **SERIOUS — ADR-0003**
1500	   **Assumption:** `local → most-recently-active → id@node` works over an eventually-consistent registry.
1501	   **Why it breaks:** “Most recent” is ambiguous under partitions, clock skew, and concurrent drive. It can route messages or remote-control to the wrong instance, especially around active/dormant transitions.
1502	   **Spike/evidence:** Registry lease/epoch design using monotonic per-node epochs, not wall-clock ordering. Chaos test stale registry entries and ambiguous active instances.
1503
1504	9. **SERIOUS — ADR-0003 / PRD R-INST-3**
1505	   **Assumption:** Dormant warm sessions are a safe default.
1506	   **Why it breaks:** Warm sessions can retain credentials, file locks, model/billing footprint, PTYs, and watchers across every node. The default multiplies resource and security exposure.
1507	   **Spike/evidence:** Measure N dormant sessions across real adapters. Define adapter-specific warm/cold policy and resource budget before locking default warm.
1508
1509	10. **FATAL — ADR-0005**
1510	    **Assumption:** Trust-store delete is revocation.
1511	    **Why it breaks:** The TOTP seed is replicated to every trusted node and fetchable from nodes/agents. If a node or seed is compromised, deleting its pubkey does not revoke its ability to pair again as a new node.
1512	    **Spike/evidence:** Seed epoch + rotation protocol. Demonstrate “remove compromised node” prevents old node/old seed from rejoining.
1513
1514	11. **SERIOUS — ADR-0005**
1515	    **Assumption:** SPAKE2 limits attackers to one online guess.
1516	    **Why it breaks:** Public pre-trust relay plus multiple seed-holder nodes allows distributed guessing unless rate limits are subnet-global. ±1 TOTP window triples accepted passwords.
1517	    **Spike/evidence:** Pairing lockout design: one active pairing ceremony per subnet, shared attempt counters, exponential backoff, relay identity abuse tests.
1518
1519	12. **SERIOUS — ADR-0005**
1520	    **Assumption:** PAKE “binds pubkeys” by construction.
1521	    **Why it breaks:** Only true if the protocol transcript binds roles, node pubkeys, subnet ID/name, time-step, and confirmation MACs. Otherwise unknown-key-share, reflection, or wrong-subnet pairing remains possible.
1522	    **Spike/evidence:** Full pairing transcript spec and negative tests for MITM, replay, reflection, wrong subnet, stale timestep.
1523
1524	13. **SERIOUS — KNOWN-HAZARDS.md**
1525	    **Missing invariant:** Local `api` mutation auth.
1526	    **Why it breaks:** The docs mention per-link auth, but the hazards checklist does not require every `spt api bind/state/session-end/history-log/poll` mutation to be authenticated to an endpoint/session. Local untrusted processes are explicitly in scope via shell/adapters.
1527	    **Spike/evidence:** Per-endpoint link token or OS credential binding. Tests that unrelated local process cannot bind, inject, end, or spoof state.
1528
1529	14. **SERIOUS — KNOWN-HAZARDS.md**
1530	    **Missing invariant:** Exactly-once/idempotent delivery across brain restart.
1531	    **Why it breaks:** Broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker/brain boundary. Restart can duplicate or drop unless every side effect has durable IDs and replay rules.
1532	    **Spike/evidence:** Crash brain at every boundary: before/after spool write, before/after PTY write, mid-transfer, mid-registry update. Assert idempotency.
1533
1534	**Single Redesign Trigger**
1535
1536	ADR-0004 is the likely redesign forcing function. Specifically: live client/WAN stream survival across brain restart is not solved. If the broker must own live Iroh/client stream state, it becomes the real daemon and the “minimal stable broker / updatable brain” split changes shape. If it does not own that state, the no-terminate self-update invariant is false.
1537	SUCCESS: The process with PID 36816 (child process of PID 57388) has been terminated.
1538	SUCCESS: The process with PID 42740 (child process of PID 57388) has been terminated.
1539	tokens used
1540	57,423
1541	Read the requested repo docs only.
1542
1543	**Findings**
1544

Read the requested repo docs only.

**Findings**

1. **FATAL — ADR-0004 / PRD R-UPD-3**
   **Assumption:** “No endpoint process terminates or suspends during a self-update” is satisfied by broker/brain split.
   **Why it breaks:** ADR-0004 later admits broker updates may require FD-passing or a “planned endpoint-cycle.” That directly contradicts the hard invariant. The real invariant is only “brain-only updates do not terminate endpoints.”
   **Spike/evidence:** Define update classes: brain-only, broker-compatible, broker-breaking. Prove the invariant separately for each, or explicitly weaken R-UPD-3.

2. **FATAL — ADR-0004 / CONTEXT Self-update**
   **Assumption:** Holding listening sockets lets live client sockets survive daemon-logic restart.
   **Why it breaks:** A listening socket is not an accepted connection, QUIC stream, Iroh endpoint, file-transfer stream, or remote terminal stream. Existing client connections have process-local protocol state. If the brain owns I/O, restart drops them. If the broker owns them, the broker is no longer dumb.
   **Spike/evidence:** Active remote terminal over Iroh + active file transfer + active `api listen`; restart brain; verify no reconnect, no byte loss, same stream continuity.

3. **FATAL — ADR-0004 / CONTEXT `spt-daemon`**
   **Assumption:** Brain can “reattach” to broker-held PTYs on both Windows ConPTY and Linux.
   **Why it breaks:** This is plausible only if the broker owns the full PTY I/O loop, buffers, resize state, backpressure, and child lifetime. If the brain ever owns duplicated pipe/master handles, brain death can create EOF, stuck stdin, lost output, or orphaned state. Linux `forkpty` is easier; Windows ConPTY handle/pipe/job-object behavior is the high-risk path.
   **Spike/evidence:** Spawn interactive children on Windows and Linux, stream output under load, resize terminal, send input, restart brain 100x. Assert child PID lives, no EOF/SIGHUP, no lost bytes, no wedged input.

4. **FATAL — ADR-0002 + ADR-0004**
   **Assumption:** Networking can be collapsed into the same daemon while the broker remains minimal and rarely updated.
   **Why it breaks:** To preserve live WAN streams across brain restart, the broker must own Iroh endpoint state, stream multiplexing, accepted client sessions, relay behavior, and maybe registry subscriptions. That moves volatile networking code into the supposedly stable kernel.
   **Spike/evidence:** Produce an ownership table for PTY masters, accepted sockets, Iroh endpoint, QUIC streams, mDNS sockets, registry gossip, transfer queues. Any live stream owned by brain invalidates the update invariant.

5. **SERIOUS — ADR-0004 / Self-update delivery**
   **Assumption:** Signature-verified binaries make peer-propagated updates safe.
   **Why it breaks:** Missing rollback protection, release metadata expiry, channel pinning, version monotonicity, key rotation/revocation, and adapter-content signing. A compromised node can replay an old signed vulnerable binary. Adapter file-pull/delegated-command updates are not covered by “binary signature” unless separately specified.
   **Spike/evidence:** TUF-like metadata or equivalent: signed target hashes, monotonically increasing versions, expiry, rollback tests, key rotation, adapter manifest/content signatures.

6. **SERIOUS — ADR-0002**
   **Assumption:** One collapsed daemon is simpler operationally.
   **Why it breaks:** It couples PTY survival, WAN reachability, registry gossip, pairing, update, and local service lifecycle into one failure domain. A bug in networking can now take out terminal hosting; a PTY broker bug can take out WAN reachability.
   **Spike/evidence:** Fault-injection matrix: crash/hang Iroh, mDNS, registry sync, PTY broker, manifest invocation. Verify blast radius and recovery.

7. **SERIOUS — ADR-0003 / CONTEXT Instances**
   **Assumption:** Two-tier context sync plus “newest” freshness makes the same agent’s mind follow across nodes.
   **Why it breaks:** Concurrent active instances create semantic conflicts, not just stale writes. Wall-clock newest loses valid updates. LLM-authored live/project tiering is not reliable enough to be the conflict boundary.
   **Spike/evidence:** Offline two nodes, mutate same live topic and different project topics, reconnect with clock skew. Require vector clocks, operation logs, CRDT-ish merge, or explicit conflict review.

8. **SERIOUS — ADR-0003**
   **Assumption:** `local → most-recently-active → id@node` works over an eventually-consistent registry.
   **Why it breaks:** “Most recent” is ambiguous under partitions, clock skew, and concurrent drive. It can route messages or remote-control to the wrong instance, especially around active/dormant transitions.
   **Spike/evidence:** Registry lease/epoch design using monotonic per-node epochs, not wall-clock ordering. Chaos test stale registry entries and ambiguous active instances.

9. **SERIOUS — ADR-0003 / PRD R-INST-3**
   **Assumption:** Dormant warm sessions are a safe default.
   **Why it breaks:** Warm sessions can retain credentials, file locks, model/billing footprint, PTYs, and watchers across every node. The default multiplies resource and security exposure.
   **Spike/evidence:** Measure N dormant sessions across real adapters. Define adapter-specific warm/cold policy and resource budget before locking default warm.

10. **FATAL — ADR-0005**
    **Assumption:** Trust-store delete is revocation.
    **Why it breaks:** The TOTP seed is replicated to every trusted node and fetchable from nodes/agents. If a node or seed is compromised, deleting its pubkey does not revoke its ability to pair again as a new node.
    **Spike/evidence:** Seed epoch + rotation protocol. Demonstrate “remove compromised node” prevents old node/old seed from rejoining.

11. **SERIOUS — ADR-0005**
    **Assumption:** SPAKE2 limits attackers to one online guess.
    **Why it breaks:** Public pre-trust relay plus multiple seed-holder nodes allows distributed guessing unless rate limits are subnet-global. ±1 TOTP window triples accepted passwords.
    **Spike/evidence:** Pairing lockout design: one active pairing ceremony per subnet, shared attempt counters, exponential backoff, relay identity abuse tests.

12. **SERIOUS — ADR-0005**
    **Assumption:** PAKE “binds pubkeys” by construction.
    **Why it breaks:** Only true if the protocol transcript binds roles, node pubkeys, subnet ID/name, time-step, and confirmation MACs. Otherwise unknown-key-share, reflection, or wrong-subnet pairing remains possible.
    **Spike/evidence:** Full pairing transcript spec and negative tests for MITM, replay, reflection, wrong subnet, stale timestep.

13. **SERIOUS — KNOWN-HAZARDS.md**
    **Missing invariant:** Local `api` mutation auth.
    **Why it breaks:** The docs mention per-link auth, but the hazards checklist does not require every `spt api bind/state/session-end/history-log/poll` mutation to be authenticated to an endpoint/session. Local untrusted processes are explicitly in scope via shell/adapters.
    **Spike/evidence:** Per-endpoint link token or OS credential binding. Tests that unrelated local process cannot bind, inject, end, or spoof state.

14. **SERIOUS — KNOWN-HAZARDS.md**
    **Missing invariant:** Exactly-once/idempotent delivery across brain restart.
    **Why it breaks:** Broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker/brain boundary. Restart can duplicate or drop unless every side effect has durable IDs and replay rules.
    **Spike/evidence:** Crash brain at every boundary: before/after spool write, before/after PTY write, mid-transfer, mid-registry update. Assert idempotency.

**Single Redesign Trigger**

ADR-0004 is the likely redesign forcing function. Specifically: live client/WAN stream survival across brain restart is not solved. If the broker must own live Iroh/client stream state, it becomes the real daemon and the “minimal stable broker / updatable brain” split changes shape. If it does not own that state, the no-terminate self-update invariant is false.
SUCCESS: The process with PID 36816 (child process of PID 57388) has been terminated.
SUCCESS: The process with PID 42740 (child process of PID 57388) has been terminated.
tokens used
57,423
Read the requested repo docs only.

**Findings**


Got all 14 findings. Codex (gpt-5.5, 57k tokens) hit hard on ADR-0004. Writing findings doc, then summary.


Write

# Stage A — Codex adversarial red-team review

> Date: 2026-05-31. Reviewer: OpenAI Codex (gpt-5.5, reasoning=high, read-only sandbox).
> Target: PRD.md, CONTEXT.md, ADR-0001..0005, docs/KNOWN-HAZARDS.md.
> Scope locked = full PRD; goal = surface fatal flaws, not cut scope.
> This is the first half of ROADMAP Stage A (codex pass). `plan-eng-review` is the second half.

## Verdict

**Single most-likely redesign trigger: ADR-0004 live-stream survival across brain restart.**
If the broker must own live Iroh/client/QUIC/PTY stream state to survive a brain restart, the broker *is* the real daemon and the "minimal stable broker / updatable brain" split changes shape. If the broker does *not* own that state, the no-terminate self-update invariant (R-UPD-3) is false. The split's load-bearing boundary is currently **undefined**.

5 FATAL, 9 SERIOUS. FATAL cluster is entirely ADR-0004 (4) + ADR-0005 revocation (1).

## Findings

### FATAL

1. **ADR-0004 / R-UPD-3 — invariant contradicts its own ADR.**
   "No endpoint terminates/suspends during self-update" is asserted absolute, but ADR-0004 itself admits broker updates may need FD-passing or a "planned endpoint-cycle." Real invariant is only *brain-only updates don't terminate endpoints*.
   → Resolve: define update classes (brain-only / broker-compatible / broker-breaking); prove invariant per class, or explicitly weaken R-UPD-3.

2. **ADR-0004 / CONTEXT self-update — listening socket ≠ live connection.**
   Holding a *listening* socket does not preserve accepted connections, QUIC streams, Iroh endpoints, file-transfer streams, or remote-terminal streams — those carry process-local protocol state. Brain owns I/O → restart drops them. Broker owns them → broker isn't dumb.
   → Spike: active remote terminal over Iroh + active file transfer + active `api listen`; restart brain; assert no reconnect, no byte loss, stream continuity.

3. **ADR-0004 / CONTEXT — brain "reattach" to broker PTYs on Windows ConPTY.**
   Plausible only if broker owns the *full* PTY I/O loop, buffers, resize state, backpressure, child lifetime. Any duplicated pipe/master handle in the brain → brain death causes EOF / stuck stdin / lost output / orphan. Linux `forkpty` easier; **Windows ConPTY handle/pipe/job-object is the high-risk path.**
   → Spike #1 (broker/brain handoff): interactive children both OSes, stream under load, resize, input, restart brain 100×; assert child lives, no EOF/SIGHUP, no lost bytes, no wedged input.

4. **ADR-0002 + ADR-0004 — networking collapse vs minimal broker.**
   To preserve live WAN streams across brain restart, broker must own Iroh endpoint, stream mux, accepted sessions, relay behavior, maybe registry subscriptions → moves volatile networking code into the "stable kernel." Contradicts "rarely updates."
   → Resolve: ownership table for PTY masters, accepted sockets, Iroh endpoint, QUIC streams, mDNS sockets, registry gossip, transfer queues. *Any live stream owned by brain invalidates the update invariant.*

10. **ADR-0005 — trust-store delete is not revocation.**
   TOTP seed is replicated to every trusted node and fetchable. Compromised node/seed: deleting its pubkey does not stop it re-pairing as a *new* node.
   → Resolve: seed epoch + rotation protocol; demonstrate "remove compromised node" blocks old node/old seed rejoin.

### SERIOUS

5. **ADR-0004 self-update delivery** — signature alone insufficient. Missing rollback protection, metadata expiry, channel pinning, version monotonicity, key rotation/revocation, adapter-content signing. Compromised node can replay old signed-vulnerable binary. → TUF-like metadata: signed target hashes, monotonic versions, expiry, rollback tests, key rotation, adapter signatures.

6. **ADR-0002** — one collapsed daemon = one failure domain. Networking bug kills terminal hosting; PTY broker bug kills WAN reachability. → Fault-injection matrix: crash/hang Iroh, mDNS, registry, PTY broker, manifest invocation; measure blast radius + recovery.

7. **ADR-0003 / Instances** — two-tier sync + wall-clock "newest" loses valid concurrent updates; LLM-authored live/project tiering not reliable as conflict boundary. → Offline two nodes, mutate same live topic + different project topics, reconnect with clock skew; require vector clocks / op-logs / CRDT-ish merge / explicit conflict review.

8. **ADR-0003** — `local → most-recently-active → id@node` ambiguous under partition / clock skew / concurrent drive; can route to wrong instance around active/dormant transitions. → Registry lease/epoch using monotonic per-node epochs, not wall-clock; chaos-test stale entries + ambiguous active instances.

9. **ADR-0003 / R-INST-3** — dormant-warm default retains creds, file locks, model/billing footprint, PTYs, watchers on *every* node → multiplies resource + security exposure. → Measure N dormant sessions on real adapters; define adapter-specific warm/cold policy + resource budget before locking warm default.

11. **ADR-0005** — SPAKE2 one-guess bound breaks if rate limits aren't *subnet-global*: public pre-trust relay + multiple seed-holders → distributed guessing; ±1 TOTP window triples accepted passwords. → One active pairing ceremony per subnet, shared attempt counters, exponential backoff, relay-abuse tests.

12. **ADR-0005** — PAKE binds pubkeys only if transcript binds roles, node pubkeys, subnet ID, time-step, confirmation MACs. Otherwise unknown-key-share / reflection / wrong-subnet pairing possible. → Full transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale timestep).

13. **KNOWN-HAZARDS — missing: local `api` mutation auth.** Hazards don't require `api bind/state/session-end/history-log/poll` mutations to be authenticated to endpoint/session; local untrusted processes are in scope via shells/adapters. → Per-endpoint link token or OS-credential binding; test unrelated local process cannot bind/inject/end/spoof.

14. **KNOWN-HAZARDS — missing: exactly-once/idempotent delivery across brain restart.** Broker queues, spool rows, PTY injection, remote streams, transfers cross the broker/brain boundary; restart can dup/drop without durable IDs + replay rules. → Crash brain at every boundary (before/after spool write, before/after PTY write, mid-transfer, mid-registry); assert idempotency.

## Disposition (to fold into ADRs/CONTEXT — ROADMAP step 3)

- **ADR-0004:** add update-class taxonomy (#1); add the ownership table (#4, #2) as a normative section; mark Spike #1 to also cover live Iroh/file-transfer stream survival, not just PTY (#2, #3).
- **ADR-0005:** add seed-epoch/rotation for real revocation (#10); subnet-global pairing rate-limit + full transcript binding spec (#11, #12).
- **ADR-0003:** replace wall-clock "newest" with monotonic per-node epochs for both resolution (#8) and context merge (#7); resource/security budget for dormant-warm default (#9).
- **ADR-0002:** document the single-failure-domain tradeoff + fault-injection acceptance (#6).
- **KNOWN-HAZARDS:** add two invariants — local api-mutation auth (#13), idempotent delivery across brain restart (#14). Both become `REQ-HAZARD-*` once their milestone activates.
- **Self-update (R-UPD):** TUF-like metadata (#5).