$OWL send todlando <<'EOF'
Real-harness repro data from clearing the operator's wedged machine (use it for the Wave 2 gates):

1. UNHOST-PSYCHE-REAP fails in the REAL harness too (v0.12.0's reap didn't land): a DEAD endpoint (wall-a, harness process gone) left its `{id}-psyche` HARNESS PROCESS (claude-spt-psyche.exe --id wall-a-psyche) ALIVE + a `spt ready wall-a-psyche --once` alive. The orphan psyche held a file in the perch tree, so `spt endpoint purge wall-a --force` STOPPED fine but FAILED to remove the tree: "os error 32: The process cannot access the file because it is being used by another process." Only after I scope-killed the orphan psyche did purge succeed. So your dummy-harness E2E should assert: when an spt-hosted endpoint's harness DIES (or is stopped), its {id}-psyche process is REAPED (not left holding perch files). Fold into L2/L4.

2. Control plane vs data plane confirmed: `endpoint purge --force` STOP worked instantly on all three (control plane responsive); only attach/output (data plane, the 2-worker loopback) was wedged. Matches the ATTACH-WEDGE root.

3. ensure_running respawns a fresh daemon the moment a CLI command finds none (killing the daemon → next CLI call brings up a new one). Account for that in any daemon-stop E2E: assert stop leaves it down for a bounded window WITHOUT a CLI call racing a respawn.

Not blocking your Wave 1 — just real data to make the gates faithful.
EOF

Send todlando real-harness repro findings from the clear


➜ SENT:todlando


so nothing is forgotten, please write the JIT plan containing each item we intend to address in the imminent sprint. should include the picker and lifecycle bugs i raised. --- the tagline of your inva…


All set up — sprint launched, nothing dropped.

## v0.12.1 sprint — launched

**Committed + pushed** (`a4a76c6`, branch `v0.12.1-lifecycle` off main):
- **`V0.12.1-LIFECYCLE-JIT.md`** — the anchor plan, every item: dummy-harness fixture → lifecycle (VIEWER-CLOSE-DETACH + ATTACH-WEDGE + status/daemon-stop) → picker (history-fresh + online-action) → `--local` removal → release.
- **`CONTEXT.md`** — invariant **"A view is independent from the endpoint"** (your tagline), `[doc->REQ-HAZARD-VIEWER-CLOSE-DETACH]`.
- **5 REQs minted** registry-first (traceable EXIT=0): VIEWER-CLOSE-DETACH (doc active), ATTACH-WEDGE, PICKER-HISTORY-FRESH, PICKER-ONLINE-ACTION, ENDPOINT-LIST-MERGE-LOCAL.

**todlando executing** — Wave 1 (dummy-harness fixture) → Wave 2 keystone (job-breakaway). Binding discipline: every lifecycle gate runs against the **real** dummy-harness + real detached daemon, no mocks. Picker roots get investigated before fixing (reported to me first). I gate per-wave (independent real runs) → deployah → v0.12.1 PATCH.

**Your machine: cleared** — phantom endpoints purged, orphans killed, wedge gone, fresh clean daemon up (empty roster), your `doyle` record intact.

**Bonus from the clear:** confirmed two more real-harness facts and handed them to todlando as gate inputs — (1) UNHOST-PSYCHE-REAP also fails in the real harness (dead wall-a left its psyche alive, holding perch files → `purge` hit os-error-32 until I killed it), so the dummy-harness E2E must assert the psyche is reaped on endpoint death; (2) control plane was responsive while only the data plane wedged (matches ATTACH-WEDGE).

Holding for todlando's wave-by-wave SEAM-GREEN pings. I'll bring each gate result back to you, and surface the picker root-cause findings when they land.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/2" id="02effd8c" type="msg" from="todlando">ACK real-harness repro data — folding into Wave 2: (L2/L4) gate asserts a DIED/stopped spt-hosted harness gets its {id}-psyche REAPED (no orphan holding perch files → no purge os-error-32); (3) daemon-stop gate asserts down-for-bounded-window with NO racing CLI call (ensure_running respawns on any CLI find-none);</EVENT-PART>
<EVENT-PART seq="2/2" id="02effd8c"> (2) control-vs-data plane confirms ATTACH-WEDGE = 2-worker loopback. Wave 1 dummy adapter has no psyche_init (no psyche) so psyche-reap is a Wave 2 concern. Building+running Wave 1 fixture now.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


todlando folded it in correctly — and rightly noted the Wave 1 dummy adapter has no `psyche_init` (no psyche), so psyche-reap is a Wave 2 concern (needs a live-agent dummy variant). Sound. Building Wave 1 now. No reply needed.

Capturing the reopen + the meta-lesson in memory while it's fresh (context is growing):


Write

---
name: v0121-realharness-reopen
description: "v0.12.0 lifecycle fixes shipped but DON'T work in the real claude-spt harness — reopened as v0.12.1; mock-gating escape; root causes found"
metadata:
  type: project
---

**v0.12.0 SHIPPED (counter 25) but its lifecycle fixes DON'T deliver in the REAL harness** (operator dogfood, claude-spt, Windows 2026-06-18). Reopened as **v0.12.1** (branch `v0.12.1-lifecycle` off main@5f9ea23; JIT plan `V0.12.1-LIFECYCLE-JIT.md`; 5 REQs minted @a4a76c6). todlando executes, doyle gates per-wave, deployah → v0.12.1 PATCH.

**META-LESSON (binding):** v0.12.0 was GATE-PASSED on **mock adapters + in-proc `reconcile_once`** + tests that explicitly killed+reaped sessions → green tests, broken reality. The escape class we keep hitting ([[shared-seam-change-run-all-seam-tests]], [[ci-clippy-preflight-workspace]] siblings). **v0.12.1 discipline: every lifecycle gate runs against a REAL dummy-harness fixture (operator's idea: a `kind=harness` adapter whose command is a trivial bind-perch+stdout+stay-alive program, driven through the REAL `spt endpoint run`→broker-PTY→rc path) + a REAL detached daemon — NO mocks, NO in-proc reconcile_once.** I'm on LEGACY spt on HFENDULEAM, so spt-core can be freely built + its daemon restarted here (real-harness validation uses the operator's actual claude-spt setup).

**ROOT CAUSES (two layers, both verified-at-code via investigation agents):**
1. **REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).** Closing the tab/window KILLS the spt-hosted harness (should only detach the view). ROOT: daemon never breaks away from the launching terminal's Windows **Job Object** (`KILL_ON_JOB_CLOSE`); `CREATE_BREAKAWAY_FROM_JOB` used NOWHERE. Both daemon spawn paths (daemon.rs:707, deelevate.rs:519) drop the CONSOLE but not job membership → the daemon's broker-spawned ConPTY harness subtree is reaped on tab-close. ConPTY isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon). FIX: add CREATE_BREAKAWAY_FROM_JOB to both paths + pin each broker-spawned harness into a daemon-owned Job Object backstop. pty.rs unchanged. Unix already OK (daemon session-detach). New CONTEXT.md invariant "**A view is independent from the endpoint**" (operator-mandated, tagline by operator).
2. **REQ-HAZARD-ATTACH-WEDGE (robustness).** A dead PTY child + undrained operator pump WEDGES the broker GLOBALLY (new endpoints stall at `PUMP_IPC_READER: spawned`). ROOT: loopback attach output = blocking `write_all` into a bounded 64KB tokio duplex (nethost.rs:1040,1090); a dead operator stops draining → write_all blocks forever ("loopback never hangs" at nethost.rs:1103 is FALSE) → parks workers in the **2-worker** net runtime (nethost.rs:640) → both saturate → all new attach/endpoint-run stall; daemon stop can't join. DISTINCT from the removed B1 path-(c) mutex deadlock (data-plane backpressure, not lock-ordering). FIX: loopback sends fail-fast on a dead/full subscriber (BrokenPipe → end serve_attach), never hold a worker indefinitely.

**ALSO real-harness-confirmed:** UNHOST-PSYCHE-REAP (v0.12.0) ALSO fails for real — a dead endpoint left its `{id}-psyche` process alive holding perch files → `endpoint purge` hit "os error 32" until scope-killed. Control plane stayed responsive while only the data plane wedged (matches ATTACH-WEDGE). `ensure_running` respawns a fresh daemon on any CLI find-none (account for in daemon-stop gates). Wave 3 picker bugs (no project-history for fresh endpoints; "Start now" shown for online endpoints) are SEPARATE — investigate root before fixing. Wave 4 = `--local` removal (operator decision 2026-06-17, was dropped, now minted REQ-ENDPOINT-LIST-MERGE-LOCAL). Picker-adapter-description stays deferred. Supersedes the "v0.12.0 COMPLETE/shipped" status in [[v012-lifecycle-milestone]].


File created successfully at: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v0121-realharness-reopen.md (file state is current in your context — no need to Read it back)

---
name: v0121-realharness-reopen
description: "v0.12.0 lifecycle fixes shipped but DON'T work in the real claude-spt harness — reopened as v0.12.1; mock-gating escape; root causes found"
metadata:
  node_type: memory
  type: project
  originSessionId: 4a8de2fb-1ebb-4dce-b8fb-c6e4ed060c10
---

**v0.12.0 SHIPPED (counter 25) but its lifecycle fixes DON'T deliver in the REAL harness** (operator dogfood, claude-spt, Windows 2026-06-18). Reopened as **v0.12.1** (branch `v0.12.1-lifecycle` off main@5f9ea23; JIT plan `V0.12.1-LIFECYCLE-JIT.md`; 5 REQs minted @a4a76c6). todlando executes, doyle gates per-wave, deployah → v0.12.1 PATCH.

**META-LESSON (binding):** v0.12.0 was GATE-PASSED on **mock adapters + in-proc `reconcile_once`** + tests that explicitly killed+reaped sessions → green tests, broken reality. The escape class we keep hitting ([[shared-seam-change-run-all-seam-tests]], [[ci-clippy-preflight-workspace]] siblings). **v0.12.1 discipline: every lifecycle gate runs against a REAL dummy-harness fixture (operator's idea: a `kind=harness` adapter whose command is a trivial bind-perch+stdout+stay-alive program, driven through the REAL `spt endpoint run`→broker-PTY→rc path) + a REAL detached daemon — NO mocks, NO in-proc reconcile_once.** I'm on LEGACY spt on HFENDULEAM, so spt-core can be freely built + its daemon restarted here (real-harness validation uses the operator's actual claude-spt setup).

**ROOT CAUSES (two layers, both verified-at-code via investigation agents):**
1. **REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).** Closing the tab/window KILLS the spt-hosted harness (should only detach the view). ROOT: daemon never breaks away from the launching terminal's Windows **Job Object** (`KILL_ON_JOB_CLOSE`); `CREATE_BREAKAWAY_FROM_JOB` used NOWHERE. Both daemon spawn paths (daemon.rs:707, deelevate.rs:519) drop the CONSOLE but not job membership → the daemon's broker-spawned ConPTY harness subtree is reaped on tab-close. ConPTY isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon). FIX: add CREATE_BREAKAWAY_FROM_JOB to both paths + pin each broker-spawned harness into a daemon-owned Job Object backstop. pty.rs unchanged. Unix already OK (daemon session-detach). New CONTEXT.md invariant "**A view is independent from the endpoint**" (operator-mandated, tagline by operator).
2. **REQ-HAZARD-ATTACH-WEDGE (robustness).** A dead PTY child + undrained operator pump WEDGES the broker GLOBALLY (new endpoints stall at `PUMP_IPC_READER: spawned`). ROOT: loopback attach output = blocking `write_all` into a bounded 64KB tokio duplex (nethost.rs:1040,1090); a dead operator stops draining → write_all blocks forever ("loopback never hangs" at nethost.rs:1103 is FALSE) → parks workers in the **2-worker** net runtime (nethost.rs:640) → both saturate → all new attach/endpoint-run stall; daemon stop can't join. DISTINCT from the removed B1 path-(c) mutex deadlock (data-plane backpressure, not lock-ordering). FIX: loopback sends fail-fast on a dead/full subscriber (BrokenPipe → end serve_attach), never hold a worker indefinitely.

**ALSO real-harness-confirmed:** UNHOST-PSYCHE-REAP (v0.12.0) ALSO fails for real — a dead endpoint left its `{id}-psyche` process alive holding perch files → `endpoint purge` hit "os error 32" until scope-killed. Control plane stayed responsive while only the data plane wedged (matches ATTACH-WEDGE). `ensure_running` respawns a fresh daemon on any CLI find-none (account for in daemon-stop gates). Wave 3 picker bugs (no project-history for fresh endpoints; "Start now" shown for online endpoints) are SEPARATE — investigate root before fixing. Wave 4 = `--local` removal (operator decision 2026-06-17, was dropped, now minted REQ-ENDPOINT-LIST-MERGE-LOCAL). Picker-adapter-description stays deferred. Supersedes the "v0.12.0 COMPLETE/shipped" status in [[v012-lifecycle-milestone]].


Edit

- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) —

- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) —


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

- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) —

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave

+- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable

- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) — gating a release CHANGELOG: map EVERY commit in `vPREV..vNEW` to a bullet (catch misses) + check each bullet's SCOPE isn't broader than its commit (catch overstatements — a touched feature may predate the release). v0.12.0 had both errors. Post-publish fix = docs-only (CHANGELOG.md on main + gh release edit notes), no retag

- [pre-flight clippy --workspace](ci-clippy-preflight-workspace.md) — CI denies warnings workspace-wide (`clippy --workspace --all-targets -D warnings`); a `-p <crate>` local check misses sibling-crate lints → CI red round-trip (M11-W1: `-p spt` missed a `useless_vec` in spt-daemon's act-gate test). Pre-flight the exact CI invocation. CI only fires on PR (push branches=main/dev-freeform)

- [shared-seam change → run ALL seam tests](shared-seam-change-run-all-seam-tests.md) — a behavior change to a SHARED runtime seam can break SIBLING tests exercising it; targeted preflight on only the new tests misses them (CI's full suite catches it). v0.8.2 F-010 residency broke daemon_lifecycle_real_brain (NOOP fast-exit psyche) — ran the livehost E2Es but not that spt-daemon sibling. Grep+run ALL seam tests (run_brain/spawn_live_host/psyche_init/daemon run). Test-analog of [[ci-clippy-preflight-workspace]]


Reopen + meta-lesson banked in memory (durable across context).

State: v0.12.1 sprint launched (branch + JIT + 5 REQs + invariant committed `a4a76c6`), your machine cleared, todlando building the Wave 1 dummy-harness fixture → Wave 2 keystone. Real-harness gating mandatory; I gate per-wave.

Holding for todlando's Wave 1 SEAM-GREEN. Will surface each gate result + the picker root-cause findings as they land.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/3" id="ccfbec04" type="msg" from="todlando">WAVE 1 root (fixture WIP): the dummy harness never went ONLINE because cmd_bind (startup.rs:464) marks status=online ONLY for `live_agent &amp;&amp; manifest.psyche_init.is_some()` — the online signal is deliberately gated to match the reconcile host predicate (no online-but-unhostable perch). My first dummy manife</EVENT-PART>
<EVENT-PART seq="2/3" id="ccfbec04">st had no [session.psyche_init]. Fix: the faithful dummy = a live_agent WITH a long-lived psyche_init (mirror brain_restart_psyche_dup's `psychebin ready {id}` pattern) — which also gives Wave 2 the {id}-psyche needed for your repro-#1 psyche-reap gate. Also fixing a test bug (I read the transient api-bind pid for liveness; using endpoint-ru</EVENT-PART>
<EVENT-PART seq="3/3" id="ccfbec04">n's reported broker-spawned harness pid instead). Re-running.</EVENT-PART></event>
</task-notification>


todlando self-correcting well on the Wave 1 fixture:
- Found `status=online` is gated on `live_agent && psyche_init.is_some()` (startup.rs:464) — deliberate (no online-but-unhostable perch). His first dummy had no `psyche_init` → never online.
- Fix: faithful dummy = a `live_agent` **with** a long-lived `psyche_init` (mirrors the dup-reap fixture's `psychebin ready {id}`) — also yields the `{id}-psyche` Wave 2 needs for the psyche-reap gate. One fixture serves both waves.
- Also fixing a test bug (read the transient api-bind pid; should use endpoint-run's broker-spawned harness pid).

Sound reasoning, no reply needed. Holding for Wave 1 SEAM-GREEN.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/5" id="b932cb00" type="msg" from="todlando">WAVE 1 status — bringup PROVEN, one open question (need your call):
GREEN: `endpoint run --start` brings the dummy harness up via the REAL daemon+broker PTY → online=true, harness pid stays ALIVE (heartbeating), psyche hosted. Fixture findings baked in: (a) the dummy MUST declare [session.psyche_init] or cmd_b</EVENT-PART>
<EVENT-PART seq="2/5" id="b932cb00">ind never marks online (startup.rs:464 gates online on live_agent &amp;&amp; psyche_init); (b) the test MUST be net-ENABLED (real node identity) — rc attach rides a broker-minted loopback-over-net conn, a net-less broker refuses it (&quot;net disabled&quot;); (c) rc needs stdin held open or its pump EOF-detaches instantly.
OPEN: with all </EVENT-PART>
<EVENT-PART seq="3/5" id="b932cb00">that, `spt rc &lt;id&gt;` ATTACHES cleanly (PUMP_IPC_READER spawned, no RC_FAIL, stays the full 10s) but receives ZERO of the dummy's DUMMY_HARNESS_TICK stdout. So the broker isn't fanning the harness PTY output to the attach. QUESTION: is that the ATTACH-WEDGE / data-plane defect itself surfacing on the fixture (→ Wave 1 asserts only bringu</EVENT-PART>
<EVENT-PART seq="4/5" id="b932cb00">p + attach-CONNECTS, and output-flow becomes the Wave 2 ATTACH gate), OR do you want Wave 1 to prove output-flow (→ it's a real &quot;broker doesn't drain PTY into OutputLog for endpoint-run sessions&quot; bug to root now)? Leaning: Wave 1 = bringup + attach-connects; output-flow is Wave 2's subject. Context very high — communing + self-cl</EVENT-PART>
<EVENT-PART seq="5/5" id="b932cb00">earing; will resume on your ruling. Wave 1 code is on-branch uncommitted (dummy mode + dummy_harness_e2e.rs).</EVENT-PART></event>
</task-notification>


todlando's "clean attach to a LIVE harness receives ZERO output" is a **red flag, not a defer** — that's very likely the operator's actual "attach shows no output" symptom reproduced on a *live* harness (the operator's bug may not have been only death). But first it must be disambiguated from a fixture artifact. Sending the ruling (todlando is self-clearing — making it crisp + resumable):


Bash

$OWL send todlando <<'EOF'
RULING on the Wave 1 open question.

First: COMMIT the Wave 1 fixture before you self-clear (dummy mode + dummy_harness_e2e.rs) with the three fixture findings in the commit body (psyche_init-gates-online; net-enabled required; rc stdin held). Don't lose proven work to the wipe.

On "clean attach to a LIVE harness gets ZERO output": do NOT paper this over with an attach-CONNECTS-only assertion. Zero output on a fresh attach to a live, heartbeating harness is almost certainly the OPERATOR'S CORE "attach shows no output / dead-or-wedged" symptom — reproduced WITHOUT any death or wedge. That means the operator's complaint may not be (only) death; it may be that endpoint-run harness output never reaches the attach at all. Too central to defer loosely.

But disambiguate fixture-artifact vs product-bug FIRST, in this order:
1. ATTACH TARGET CHECK (most likely artifact): the rc attach shows the [session.self] (the broker-PTY child), NOT the psyche. Make sure your DUMMY_HARNESS_TICK is emitted by the [session.self] command (the broker PTY child = the attach target), with an explicit flush after each line. If you put the ticks in psyche_init, the attach would correctly show nothing (the psyche is daemon-hosted, not the attached PTY). Fix the fixture so SELF ticks+flushes, re-test.
2. KNOWN-GOOD COMPARISON: confirm a `spt rc` to an `api listen` session (or the existing attach.rs E2E harness) DOES deliver output through the same broker — proving the fan/drain works in general.
3. If SELF ticks+flushes AND listen-attach delivers BUT endpoint-run attach still gets zero → it's a REAL endpoint-run-specific bug: the broker doesn't drain the endpoint-run harness PTY into the OutputLog / doesn't fan it to the attach. That is a likely THIRD root cause (alongside VIEWER-CLOSE-DETACH + ATTACH-WEDGE) and the direct cause of the operator's no-output. Root-cause it (reader.rs drain thread + OutputLog fan for SpawnReq sessions; check the Windows reader-park KH 7.6 angle) and PING me — I'll mint a REQ-HAZARD for it.

Scope: Wave 1 = committed fixture + bringup-PROVEN + attach-CONNECTS. The output-delivery question is the IMMEDIATE Wave 2 lead (do it before the breakaway/wedge fixes if it turns out real — it may be THE bug). Commune your full state; resume on this ruling. Ping me which of 1/2/3 it lands on.
EOF

Rule on Wave 1 zero-output question to todlando


1	- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave
2	- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
3	- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) — dummy-harness fixture EXECUTION state (todlando). `mock-session --mode dummy` (binds+heartbeats+stays-alive) + dummy_harness_e2e.rs. Bringup PROVEN green (online+alive+psyche). 3 load-bearing fixture findings: psyche_init required for online (startup.rs:464), test must be NET-ENABLED (rc loopback needs net host), rc stdin held-open. OPEN: rc attaches but gets no PTY output — awaiting doyle ruling (Wave1=connect-only vs root-now). On-branch UNCOMMITTED. Full Wave2 keystone + repro data here too
4	- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) — gating a release CHANGELOG: map EVERY commit in `vPREV..vNEW` to a bullet (catch misses) + check each bullet's SCOPE isn't broader than its commit (catch overstatements — a touched feature may predate the release). v0.12.0 had both errors. Post-publish fix = docs-only (CHANGELOG.md on main + gh release edit notes), no retag
5	- [pre-flight clippy --workspace](ci-clippy-preflight-workspace.md) — CI denies warnings workspace-wide (`clippy --workspace --all-targets -D warnings`); a `-p <crate>` local check misses sibling-crate lints → CI red round-trip (M11-W1: `-p spt` missed a `useless_vec` in spt-daemon's act-gate test). Pre-flight the exact CI invocation. CI only fires on PR (push branches=main/dev-freeform)
6	- [shared-seam change → run ALL seam tests](shared-seam-change-run-all-seam-tests.md) — a behavior change to a SHARED runtime seam can break SIBLING tests exercising it; targeted preflight on only the new tests misses them (CI's full suite catches it). v0.8.2 F-010 residency broke daemon_lifecycle_real_brain (NOOP fast-exit psyche) — ran the livehost E2Es but not that spt-daemon sibling. Grep+run ALL seam tests (run_brain/spawn_live_host/psyche_init/daemon run). Test-analog of [[ci-clippy-preflight-workspace]]
7	- [ccs Windows PTY-spawn fix](ccs-win-pty-program-resolve.md) — bare program (node CLI `ccs`) → portable-pty which picks extensionless shim → CreateProcessW os error 193. Fix: spt_term::winprog PATHEXT resolver (real-exe-first, .cmd/.bat→cmd /c, .ps1→powershell, Unix passthrough) at the single spawn_program_in chokepoint. REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE (doc+impl+unit+int), KNOWN-HAZARDS 5.12. PR #20 off main (v0.8.3), gate-green-local, awaiting operator merge/release (~v0.8.4). doyle solo in worktree. Lesson: NTFS case-insensitive — test exists-helper needed case/sep norm; PIPESTATUS not grep-gated `&&`
8	- [harness adapter-agnostic resolution](harness-adapter-agnostic-resolution.md) — ADR-0021 (grilled+locked 2026-06-16): harness-hosted seed goes adapter-AGNOSTIC (pid+session_id only), --adapter OPTIONAL; listen/poll resolve adapter at bind by pid→exe basename→new [adapter] host_binaries match→active-profiles.toml pointer (`spt adapter use`)→registered_at_ms fallback. Legacy parity ($LIVE start=$SPT listen, no --adapter). CONTEXT §170-186+ADR authored by doyle. todlando builds ~v0.9.0 (REQ-START-5/MANIFEST-8/INSTALL-12/CLI-4), doyle gates, perri adapter fixes follow (NOW: self-contained skill injection + humanized output; PREP: host_binaries + adapter use)
9	- [v0.8.3 broker-QUIC-deadline](v083-broker-quic-deadline.md) — pump-IPC B-half (PATCH): REQ-HAZARD-BROKER-QUIC-DEADLINE. NetHost::bounded_block_on (tokio timeout 10s<30s brain) wraps dial/open_stream/send_stream → dead peer errors ORDINARY (non-TimedOut) INSIDE apply_once (no phantom) → pump redials not wedges. **PR #19 base main = doyle GATE-PASS (full source read, 4 invariants + non-vacuous int verified, CI green both runners). Handed deployah → release counter 19. todlando CLEAR.** deployah CARGO.LOCK+bump at release
10	- [peer-pump stall = deferred B-half](peer-pump-stall-bhalf-confirmed.md) — HFENDULEAM v0.8.2 pump STALLED (~93min frozen heartbeat, brain gen0 never restarted) CONFIRMED = DEFERRED.md:42 broker-side unbounded QUIC op to a DEAD roster peer (ENLYZEAM ~26h stale), A-half bounds only brain reads not broker await; KH 7.6 Win reader-park. NOT new bug. Interim=restart spt-core daemon (safe vs legacy-owl perches); real fix=mint REQ broker-side QUIC bound (broker-update batch)
11	- [v0.8.2 templating+residency](v082-templating-residency.md) — doyle delegation (I own waves+exec, gates→counter 18, NON-blocking perri-dogfood quality). F-009 TEMPLATE-ARGV-FILL: tokenize template FIRST then fill each token (1 value=1 argv elem, no split/injection); preserve errors+{{/}}+install-dir resolve. F-010 LIVEHOST-NONRESIDENT: host_one residency-confirm (child dead within N s → stamp parent psyche_host_error, no phantom online perch). Linked: F-009 split→F-010 early-exit. Branch v0.8.2-templating-residency @3cd5e6f. FLAKE-LEDGER rider folds here
12	- [v0.8.1 livehost boot-race](v081-livehost-bootrace.md) — doyle delegation (I own waves+exec, doyle gates→counter 17): net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). VERIFIED: spawn_live_host @brainproc:230 IS reached net-less (broker-connect is local) → H1 refuted. Explore report's "registry race" theory SHAKY (same-slice inconsistency). True failure runtime-only → W1 = real-detached-daemon E2E (non-negotiable gate) reproduces it. Likely REQ-INSTALL-11 psyche-binary-not-found. Branch v0.8.1-livehost-bootrace @2aab895
13	- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean: live psyche model diverged (M11 daemon-host WINS vs main Feature B in-process install-dir spawn); naive merge silently DROPS Feature B. Reapply recipe (livehost.rs:191 → `with_config_in` w/ registry `source_dir`) + traceable re-tag caveat. BLOCKED: main Feature B on doyle's local unpushed main; wait for push + merge-ordering ping. how-to-live shipped @672b928
14	- [F-007 live-relay acceptance ruling](f007-live-relay-acceptance.md) — live bringup = persistent `api listen <id>` via Monitor (heir to legacy $LIVE start), NOT `--once` (no-Monitor degenerate) nor $OWL listen (ready); psyche-spawn gated on `--manifest` (declares psyche_init, fires regardless of --once) + the v0.7.4 install-dir binary fix (REQ-INSTALL-11) for bare psyche binaries; both F-007 items Bucket 2 docs gap. Lesson: ground in CONTEXT.md before ruling (I erred recommending --once)
15	- [delegate-build preflight hang](delegate-build-preflight-hang.md) — background build agents hang silently on the full spt-daemon/spt integration suite in preflight (no completion notification on hang); instruct targeted units only (`--lib` + named tests); check liveness via transcript mtime, TaskStop the stalled one, take ownership of the (usually good) partial work + verify
16	- [don't wait — proceed autonomously](dont-wait-proceed-autonomously.md) — operator 2026-06-14: don't stop to ask permission between steps; chain next action (ping/commit gated batch/next wave). After doyle W4 gate-PASS, commit myself

... [76 lines truncated] ...


Ruling sent; todlando will commit the fixture then resume on it after its self-clear.

**Notable development for you:** todlando's dummy-harness fixture proved **bringup works** (real daemon+broker PTY → online, harness alive + heartbeating, psyche hosted), but a **clean `spt rc` attach to that LIVE harness receives zero output**. That's likely **your "attach shows no output" symptom reproduced with no death and no wedge** — which means your bug may not be (only) the tab-close kill; there may be a **third root cause: the broker doesn't deliver endpoint-run harness PTY output to the attach at all.**

I ruled todlando to **not** paper over it (don't assert "attach connects" as enough). Disambiguate first:
1. **Fixture artifact check** (likeliest): ensure the ticking output comes from `[session.self]` (the broker-PTY child = the attach target) with an explicit flush — not from the psyche (daemon-hosted, not attached).
2. **Known-good comparison**: confirm `rc` to an `api listen` session *does* deliver output through the same broker.
3. If self ticks+flushes and listen delivers but endpoint-run attach still gets nothing → **real endpoint-run output-delivery bug** (third root cause, and probably the central one). Root-cause it first + I mint a REQ-HAZARD.

So the picture may be three layers, not two: (a) tab-close kills the harness, (b) dead-child wedges the broker, and possibly (c) attach never delivers endpoint-run output even when alive. (c), if real, is the one you'd notice most.

Holding for todlando's post-clear resume + which of 1/2/3 it lands on. I'll bring it straight back.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/3" id="f6b2d884" type="msg" from="todlando">ACK ruling — aborting the self-clear (clear was NOT yet armed). Doing in order: (1) commit Wave 1 fixture green at bringup+attach-CONNECTS with the 3 findings in the body; (2) disambiguate the zero-output: my DUMMY_HARNESS_TICK IS emitted by [session.self] (--mode dummy = the broker PTY child = attach target) with </EVENT-PART>
<EVENT-PART seq="2/3" id="f6b2d884">flush — so it's not the psyche-target artifact; capturing FULL rc stdout to see zero-vs-some; then known-good check (rc→listen/attach.rs delivers); if self-ticks+flush AND listen delivers but endpoint-run attach=zero → REAL endpoint-run drain/fan bug (reader.rs + OutputLog fan for SpawnReq, KH7.6 Win reader-park). Will ping you which of </EVENT-PART>
<EVENT-PART seq="3/3" id="f6b2d884">1/2/3. Not papering over.</EVENT-PART></event>
</task-notification>


todlando aborted the self-clear (wasn't armed yet), executing the ruling in order. Key: it already confirmed the ticks come from `[session.self]` (broker PTY child = attach target) **with flush** — so the **fixture-artifact explanation (#1) is ruled out**. That pushes hard toward a **real endpoint-run output-delivery bug (#3)**, pending the known-good `listen` comparison to confirm.

No reply needed. Holding for todlando's 1/2/3 verdict — increasingly looks like the third (central) root cause, caught precisely because the real dummy-harness fixture exists. I'll bring the verdict straight back.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/5" id="6122d3e4" type="msg" from="todlando">WAVE 1 COMMITTED @33fb39c (dummy mode + dummy_harness_e2e.rs, green at bringup+attach-CONNECTS, clippy --workspace + traceable EXIT=0). 3 findings in the commit body.

DISAMBIGUATION VERDICT = (3) REAL endpoint-run-specific output bug:
- (1) ruled out: DUMMY_HARNESS_TICK is emitted by [session.self] (--mode </EVENT-PART>
<EVENT-PART seq="2/5" id="6122d3e4">dummy = the broker PTY child = attach target), flushed each line. Not the psyche-target artifact.
- (2) known-good CONFIRMED: attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a spawn_session PTY child to a LOOPBACK attach (same transport rc us</EVENT-PART>
<EVENT-PART seq="3/5" id="6122d3e4">es).
- (3) CONFIRMED: my clean rc attach to the live heartbeating endpoint-run harness gets EXACTLY 0 bytes over 10s (no death, no wedge) = the operator's &quot;attach shows no output.&quot;

NARROWED LEAD: both spawn_session AND spawn_session_pid send KIND_SPAWN → the SAME broker dispatch_spawn (broker.rs:706/835) which starts the </EVENT-PART>
<EVENT-PART seq="4/5" id="6122d3e4">per-session drain+OutputLog — so the drain IS wired for endpoint-run too. So the gap is narrower than &quot;no drain&quot;: candidates = (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY, OR (b) the dummy's stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) </EVENT-PART>
<EVENT-PART seq="5/5" id="6122d3e4">→ explains alive-but-0-bytes, OR (c) ConPTY reader-park KH 7.6 on this path, OR (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. Your call on minting the REQ-HAZARD + how deep you want me to root it. Context very high — communing + self-clearing; resume on your REQ dispatch.</EVENT-PART></event>
</task-notification>


**Verdict: confirmed root cause #3 — a real, endpoint-run-specific output bug, and it's the operator's central "no output" symptom** (0 bytes over 10s to a live, heartbeating harness — no death, no wedge). The known-good comparison nails it: attach.rs proves the broker drains+fans a `spawn_session` PTY child to a loopback attach, so the drain is wired — the gap is endpoint-run-specific. This is the keystone: re-attach is meaningless without it.

Minting the REQ + elevating it in the JIT plan. First syncing todlando's Wave 1 commit (@33fb39c):


cd /c/Users/decid/Documents/projects/spt-core
git fetch origin v0.12.1-lifecycle 2>&1 | tail -1
echo "=== local vs origin ==="; git log --oneline -1; echo "origin:"; git log origin/v0.12.1-lifecycle --oneline -1
git status --short | head
echo "=== fast-forward to todlando's 33fb39c ==="; git merge --ff-only origin/v0.12.1-lifecycle 2>&1 | tail -3; git log --oneline -3

Sync local branch to todlando's Wave 1 commit before minting REQ


 * branch            v0.12.1-lifecycle -> FETCH_HEAD
=== local vs origin ===
33fb39c v0.12.1 Wave 1 — dummy-harness real-harness fixture (bringup PROVEN + attach-connects)
origin:
a4a76c6 v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)
?? target-seam/
=== fast-forward to todlando's 33fb39c ===
Already up to date.
33fb39c v0.12.1 Wave 1 — dummy-harness real-harness fixture (bringup PROVEN + attach-connects)
a4a76c6 v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)
5f9ea23 docs(changelog): correct v0.12.0 notes — scope resume-picker to ready agents + add un-host Psyche reap


Local tree is at todlando's Wave 1 (@33fb39c, includes my mint — shared working tree; todlando's commit is local-only, I'll push it with mine). `target-seam/` is todlando's test scratch (untracked, ignore). Adding the new keystone REQ:


Edit

required_stages = []  # registry-first; todlando activates [doc,impl,unit] when building. doc = CONTEXT.md endpoint-list/whoami note; impl = merge-local in cmd_endpoint_list + drop the flag/hint/cmd_list_local; unit = bare list includes a local-unadvertised perch + whoami includes the caller's own just-bound perch

required_stages = []  # registry-first; todlando activates [doc,impl,unit] when building. doc = CONTEXT.md endpoint-list/whoami note; impl = merge-local in cmd_endpoint_list + drop the flag/hint/cmd_list_local; unit = bare list includes a local-unadvertised perch + whoami includes the caller's own just-bound perch
[[requirements]]
id = "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT"
title = "A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)"
required_stages = []  # registry-first KEYSTONE; todlando activates [impl,unit,int] at root+fix. ISOLATE path-vs-program first: run the attach.rs known-DELIVERING child as the endpoint-run [session.self] — if still 0 bytes it's the endpoint-run PATH (root the spawn_session_pid SpawnReq diff + the rc subscribe/resolve_session for endpoint-run sessions); then fix to the exact mechanism. int (dummy-harness fixture): rc attach to a LIVE endpoint-run harness RECEIVES its DUMMY_HARNESS_TICK within a bounded window


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

required_stages = []  # registry-first; todlando activates [doc,impl,unit] when building. doc = CONTEXT.md endpoint-list/whoami note; impl = merge-local in cmd_endpoint_list + drop the flag/hint/cmd_list_local; unit = bare list includes a local-unadvertised perch + whoami includes the caller's own just-bound perch
[[requirements]]
id = "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT"
title = "A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)"
required_stages = []  # registry-first KEYSTONE; todlando activates [impl,unit,int] at root+fix. ISOLATE path-vs-program first: run the attach.rs known-DELIVERING child as the endpoint-run [session.self] — if still 0 bytes it's the endpoint-run PATH (root the spawn_session_pid SpawnReq diff + the rc subscribe/resolve_session for endpoint-run sessions); then fix to the exact mechanism. int (dummy-harness fixture): rc attach to a LIVE endpoint-run harness RECEIVES its DUMMY_HARNESS_TICK within a bounded window

title = "`spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)"

+title = "A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)"

+required_stages = []  # registry-first KEYSTONE; todlando activates [impl,unit,int] at root+fix. ISOLATE path-vs-program first: run the attach.rs known-DELIVERING child as the endpoint-run [session.self] — if still 0 bytes it's the endpoint-run PATH (root the spawn_session_pid SpawnReq diff + the rc subscribe/resolve_session for endpoint-run sessions); then fix to the exact mechanism. int (dummy-harness fixture): rc attach to a LIVE endpoint-run harness RECEIVES its DUMMY_HARNESS_TICK within a bounded window


Elevating it to keystone in the JIT plan (Wave 2, before VIEWER-CLOSE-DETACH):


## Wave 2 — Lifecycle core

- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).**

## Wave 2 — Lifecycle core

- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real
  dummy-harness fixture: a clean `spt rc` attach to a LIVE, heartbeating, psyche-hosted `endpoint run` harness receives
  **0 bytes** over 10s of its flushed `[session.self]` stdout — no death, no wedge. This IS the operator's central
  "attach shows no output," and it blocks the whole "view is independent" goal (re-attach shows nothing). Known-good
  (attach.rs loopback-attach E2Es) proves the broker drains+fans a `spawn_session` PTY child over the same transport →
  the gap is endpoint-run-specific (both paths share `dispatch_spawn`, broker.rs:706/835). ISOLATE path-vs-program first
  (run the attach.rs known-delivering child as the endpoint-run `[session.self]`); then root the mechanism — candidates:
  (a) `spawn_session_pid` SpawnReq stdio/env/cwd diff; (b) harness stdout write-blocks on a full ConPTY buffer (drain not
  reading THIS pty) → alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6); (d) `rc` subscribe/`resolve_session` for an
  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES
  its DUMMY_HARNESS_TICK within a bounded window.

- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).**


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

## Wave 2 — Lifecycle core

- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real
  dummy-harness fixture: a clean `spt rc` attach to a LIVE, heartbeating, psyche-hosted `endpoint run` harness receives
  **0 bytes** over 10s of its flushed `[session.self]` stdout — no death, no wedge. This IS the operator's central
  "attach shows no output," and it blocks the whole "view is independent" goal (re-attach shows nothing). Known-good
  (attach.rs loopback-attach E2Es) proves the broker drains+fans a `spawn_session` PTY child over the same transport →
  the gap is endpoint-run-specific (both paths share `dispatch_spawn`, broker.rs:706/835). ISOLATE path-vs-program first
  (run the attach.rs known-delivering child as the endpoint-run `[session.self]`); then root the mechanism — candidates:
  (a) `spawn_session_pid` SpawnReq stdio/env/cwd diff; (b) harness stdout write-blocks on a full ConPTY buffer (drain not
  reading THIS pty) → alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6); (d) `rc` subscribe/`resolve_session` for an
  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES
  its DUMMY_HARNESS_TICK within a bounded window.

- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).**

# v0.12.1 — Lifecycle + Picker JIT Plan

**Why:** v0.12.0 shipped (counter 25) but real-harness testing (operator, `claude-spt`, Windows) showed the
lifecycle fixes do **not** deliver in the real daemon + broker + PTY path. v0.12.0 was gated on **mock adapters +
in-process `reconcile_once`** — green tests, broken reality. v0.12.1 reopens with **real-harness gating** (no mocks).

**Branch:** `v0.12.1-lifecycle` off `main@5f9ea23`. **Release:** v0.12.1 PATCH (deployah, after doyle gate-pass all waves).
**Roles:** doyle designs/mints/gates · todlando executes · deployah releases.

**Meta-fix (binding for this sprint):** every lifecycle gate runs against the **real dummy-harness fixture + a real
detached daemon** — NOT `reconcile_once` in-proc, NOT a mock with a live-draining peer. doyle independently re-runs each.

---

## Wave 1 — Test infrastructure (unblocks all real-harness gates)

- **T1 — dummy-harness adapter fixture** (operator's idea). A `kind="harness"` adapter whose command is a trivial program
  that (a) binds its perch on startup (the harness contract), (b) prints a stdout line on an interval, (c) stays alive
  until killed. Drives the REAL `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc` attach path.
  Isolates "broker wedged / endpoint died" from "a real harness failed to launch." Becomes the permanent regression
  fixture the v0.12.0 mock tests never had. Lives in the test tree; runs against a scratch target dir (livehost-E2E pattern).

## Wave 2 — Lifecycle core

- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).** Closing the tab/window where `spt endpoint run` was invoked must
  detach only the `spt rc` pump; the daemon-hosted harness keeps running and is re-attachable.
  ROOT: the daemon never breaks away from the launching terminal's Windows Job Object (`KILL_ON_JOB_CLOSE`); no
  `CREATE_BREAKAWAY_FROM_JOB` anywhere → tab close reaps the daemon's freshly-spawned ConPTY harness subtree. ConPTY
  isolation itself is already correct (portable-pty makes the pseudoconsole in the daemon; no console signal/handle leak).
  FIX: add `CREATE_BREAKAWAY_FROM_JOB` to both daemon spawn paths (`daemon.rs:707` `detached_no_inherit`,
  `deelevate.rs:519` elevated) **+** pin each broker-spawned harness into a **daemon-owned Job Object** (mirror Breap,
  `reap.rs`) as backstop (survives even if a terminal sets `SILENT_BREAKAWAY_OK=false`). `pty.rs` unchanged. Unix: verify
  the daemon's session-detach already covers terminal-close (SIGHUP scope) — likely no change, add a guard test.
  GATE: spawn daemon under a parent-held `KILL_ON_JOB_CLOSE` job → `endpoint run` a dummy harness → close the parent job →
  assert the harness pid **stays alive** AND `spt rc <id>` re-attaches AND a brand-new endpoint launches.

- **L2 — REQ-HAZARD-ATTACH-WEDGE (ROBUSTNESS).** Even a *legitimately* dead PTY child (real crash/kill) + an undrained
  operator pump must NOT wedge the broker.
  ROOT: loopback attach output is a blocking `write_all` into a bounded 64 KB tokio duplex (`nethost.rs:1040,1090`); a
  dead operator (closed tab) stops draining → `write_all` blocks forever (the "loopback never hangs" assumption at
  `nethost.rs:1103` is false) → parks workers in the **2-worker** net runtime (`nethost.rs:640`) → both saturate → every
  new attach/`endpoint run` stalls after `PUMP_IPC_READER: spawned` → 30 s `FIRST_EVENT_GRACE` → "dead or wedged";
  `daemon stop` can't join the stuck workers. Distinct from the removed B1 path-(c) mutex deadlock.
  FIX: make loopback sends fail-fast — a full-buffer / `BrokenPipe` loopback write is an ordinary per-stream error that
  ENDS `serve_attach`; one dead stream can never hold a runtime worker. (Defense-in-depth: raise worker count — but the
  real fix is non-blocking-on-dead-peer.)
  GATE (dummy harness): kill the child abruptly + drop the operator pump without a clean detach → assert a **new**
  endpoint is still served, `brain.sessions()` returns promptly, `daemon stop` completes bounded.

- **L3 — status=online persistence (sub-item, folded into L2's gate).** Three dead endpoints stayed `status=online`.
  B2 (`reconcile_hosted_liveness`) should offline a controllable spt-hosted perch whose broker session is gone — confirm
  whether abrupt Windows child death actually reaps the broker session (so B2 sees it absent) and whether the reconcile
  tick fires. Resolve as part of L2; GATE asserts the dead endpoint is marked **offline within one reconcile tick**.

- **L4 — `daemon stop` ends everything (operator item #1, folded).** `daemon stop` did not end all spt processes / take
  everything offline. Largely downstream of L1+L2 (un-wedged broker can stop cleanly; harness in a daemon-owned job is
  reaped by Breap). GATE: after L1+L2, `daemon stop` terminates the daemon + brain + all hosted harness/psyche processes,
  bounded, and the roster goes offline.

## Wave 3 — Picker (operator-raised; ROOTS TBD — investigate before fixing)

- **P1 — REQ-PICKER-HISTORY-FRESH.** `spt endpoint run` picker does **not** show project history for fresh endpoints.
  Investigate the project-history loader (v0.10.0 PICKER-2, `picker/data.rs`) — real bug vs "fresh = no history yet"
  semantics. Then fix.
- **P2 — REQ-PICKER-ONLINE-ACTION.** Picker shows **"Start now"** for endpoints that are already online. Investigate the
  4-state status mapping (v0.10.0 PICKER-1, `picker/model.rs`) — is it reading live/online state correctly, or rendering
  stale/wedged broker state? Then fix (online → "Attach", not "Start now").

## Wave 4 — endpoint list cleanup (operator decision 2026-06-17, dropped — now minted)

- **E1 — REQ-ENDPOINT-LIST-MERGE-LOCAL.** Remove the `--local` flag; the bare `spt endpoint list` ALWAYS merges this
  node's local (unadvertised) perches into the view. Rationale: `spt whoami` is a thin alias — a just-online agent
  running `whoami` must see its own perch. Drop the flag + its `--detail` conflict test + the v0.10.0 hint line +
  `cmd_list_local`; fix the `whoami` alias path. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target).

## Release

- **v0.12.1 PATCH.** deployah drives after doyle gate-pass on all waves. CHANGELOG: cross-check **each bullet's scope vs
  the commit range** (the v0.12.0 lesson — [[changelog-scope-vs-commit-range]]); name the specific delta. Post-publish:
  inform operator + perri (real-harness lifecycle now actually works).

## Open / carried

- Non-blocking test-hardening: drain OUTPUT after `KIND_EXIT` in `broker::spawn_env_reaches_child` (latent flake) — fold
  into Wave 1/2 test work if cheap, else carry.
- Machine cleanup: clear the operator's 3 phantom endpoints (wall-a/b/c) + orphan psyche — scoped, preserve daemon +
  the `doyle` live perch + CI.

+- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real

+  dummy-harness fixture: a clean `spt rc` attach to a LIVE, heartbeating, psyche-hosted `endpoint run` harness receives

+  **0 bytes** over 10s of its flushed `[session.self]` stdout — no death, no wedge. This IS the operator's central

+  "attach shows no output," and it blocks the whole "view is independent" goal (re-attach shows nothing). Known-good

+  (attach.rs loopback-attach E2Es) proves the broker drains+fans a `spawn_session` PTY child over the same transport →

+  the gap is endpoint-run-specific (both paths share `dispatch_spawn`, broker.rs:706/835). ISOLATE path-vs-program first

+  (run the attach.rs known-delivering child as the endpoint-run `[session.self]`); then root the mechanism — candidates:

+  (a) `spawn_session_pid` SpawnReq stdio/env/cwd diff; (b) harness stdout write-blocks on a full ConPTY buffer (drain not

+  reading THIS pty) → alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6); (d) `rc` subscribe/`resolve_session` for an

+  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES

+  its DUMMY_HARNESS_TICK within a bounded window.


Bash

cd /c/Users/decid/Documents/projects/spt-core
traceable-reqs check >/dev/null 2>&1; echo "traceable EXIT=$?"
git add traceable-reqs.toml V0.12.1-LIFECYCLE-JIT.md
git commit -F - <<'EOF'
v0.12.1: mint REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (keystone) — endpoint-run attach delivers ZERO output

Wave 1 dummy-harness fixture confirmed a THIRD root cause, and the operator's central
symptom: a clean `spt rc` attach to a LIVE, heartbeating, psyche-hosted `endpoint run`
harness receives 0 bytes of its flushed [session.self] stdout over 10s — no death, no
wedge. Distinct from VIEWER-CLOSE-DETACH (death) and ATTACH-WEDGE (dead-child backpressure).
Known-good attach.rs loopback E2Es prove the broker drains+fans a spawn_session PTY child
over the same transport, so the gap is endpoint-run-specific (shared dispatch_spawn). Minted
registry-first as the Wave 2 keystone (do first — re-attach is meaningless without it);
JIT plan updated. Root candidates a/b/c/d noted; isolate path-vs-program first.

Co-authored by: doyle
EOF
echo "=== push (incl todlando's Wave 1 33fb39c) ==="; git push origin HEAD:v0.12.1-lifecycle 2>&1 | tail -3; git log --oneline -3

Verify traceable, commit keystone REQ, push (with Wave 1)


traceable EXIT=0
warning: in the working copy of 'V0.12.1-LIFECYCLE-JIT.md', LF will be replaced by CRLF the next time Git touches it
[v0.12.1-lifecycle 0d8ed2e] v0.12.1: mint REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (keystone) — endpoint-run attach delivers ZERO output
 2 files changed, 16 insertions(+)
=== push (incl todlando's Wave 1 33fb39c) ===
To https://github.com/SaberMage/spt-core.git
   a4a76c6..0d8ed2e  HEAD -> v0.12.1-lifecycle
0d8ed2e v0.12.1: mint REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (keystone) — endpoint-run attach delivers ZERO output
33fb39c v0.12.1 Wave 1 — dummy-harness real-harness fixture (bringup PROVEN + attach-connects)
a4a76c6 v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)


Pushed (`0d8ed2e`, includes todlando's Wave 1 + the keystone REQ). Dispatching todlando to root+fix it (resumable after its self-clear):


$OWL send todlando <<'EOF'
KEYSTONE MINTED + DISPATCHED. Pull branch v0.12.1-lifecycle (now @0d8ed2e — your Wave 1 33fb39c + my new REQ). Read V0.12.1-LIFECYCLE-JIT.md Wave 2 "L0".

REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT is the Wave 2 KEYSTONE — do it FIRST, before VIEWER-CLOSE-DETACH/ATTACH-WEDGE. It's the operator's central "attach shows no output," and re-attach (the whole invariant) is meaningless without it.

STEP 1 — ISOLATE path-vs-program (cheap, decisive): make the endpoint-run [session.self] be the EXACT child attach.rs's known-good uses (the one proven to deliver over a loopback attach), run it via `spt endpoint run`, attach.
  - If it STILL gets 0 bytes → the bug is the endpoint-run PATH (not the dummy program). Root there: diff how launch_harness_brokered_in builds the SpawnReq (env/cwd/rows-cols/stdio) for spawn_session_pid vs how attach.rs builds it for spawn_session; AND check `spt rc`'s resolve_session/subscribe for an endpoint-run session — is it subscribing to the right, populated OutputLog, or a wrong/empty one (candidate d)? And the ConPTY-write-block (b) / reader-park (c) angle if the drain isn't reading THIS pty.
  - If it DELIVERS → the dummy program's stdout handling under ConPTY is the fixture issue; fix the fixture, then re-confirm whether the real claude-spt still reproduces (the operator's bug is real regardless).

STEP 2 — root to the exact mechanism (a/b/c/d), fix it. Activate REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT stages + tag evidence in-commit.

GATE (real dummy-harness, no mock): `spt rc <id>` to a LIVE endpoint-run harness RECEIVES its DUMMY_HARNESS_TICK within a bounded window. I re-run it independently.

Ping me SEAM-GREEN (or the path-vs-program result if you want a check-in before the fix). Then L1 (VIEWER-CLOSE-DETACH breakaway) + L2 (ATTACH-WEDGE). Full detail in the REQ title + JIT plan on-branch.
EOF

Dispatch todlando to root+fix the keystone output bug


➜ SENT:todlando


Dispatched. Status for you:

## Three root causes now mapped — and the central one is confirmed

Your dummy-harness idea paid off exactly as intended: the real fixture surfaced a **third root cause that mocks never would have** — and it's the one you'd notice most.

1. **L0 (KEYSTONE) — `endpoint run` attach delivers zero output.** A clean `spt rc` to a *live, heartbeating* harness gets 0 bytes. This **is** your "attach shows no output," with no death and no wedge. Minted `REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT`, pushed (`0d8ed2e`), elevated to do-first — re-attach is meaningless without it.
2. **L1 — tab-close kills the harness** (missing job breakaway).
3. **L2 — dead child wedges the broker** (loopback backpressure).

todlando is rooting L0 now, starting with a decisive **path-vs-program isolation** (run the known-delivering attach.rs child via `endpoint run` — if it still gets 0 bytes, the bug is the endpoint-run path; if it delivers, it's a fixture quirk and we re-check real claude-spt). Then root the exact mechanism + fix, gated against the real fixture (attach to a live endpoint-run harness must receive its ticks).

Wave 1 fixture is committed (`33fb39c`); branch at `0d8ed2e`. todlando pings me SEAM-GREEN on the keystone, then moves to L1/L2. I'll bring the path-vs-program result + each gate straight back to you.

Holding.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/2" id="2c526084" type="msg" from="todlando">ACK keystone dispatch. Branch synced @0d8ed2e (discarded my WT dup of your REQ). Starting Wave 2 L0 NOW.
STEP 1 path-vs-program isolation: swap endpoint-run [session.self] to attach.rs's known-delivering child, run via endpoint run, attach.
Will ping with the isolate result before the fix (your offered check-in</EVENT-PART>
<EVENT-PART seq="2/2" id="2c526084">). Fixture = committed dummy_harness_e2e.rs (flip rc_saw_tick diagnostic-&gt;hard assert once fixed).</EVENT-PART></event>
</task-notification>


todlando synced (@0d8ed2e, discarded its dup), starting L0 with the path-vs-program isolation, will check in with the isolate result before fixing. No reply needed.

Holding for that result — it tells us whether the zero-output is the endpoint-run path itself (likely) or a fixture quirk. I'll relay it the moment it lands.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="06d115c4" type="msg" from="todlando">L0 ROOT-CAUSE NAILED (well past STEP-1 isolate). Not the dummy program, not a drain bug, not worker starvation (tried 2-&gt;8 workers, no change). It is a BROKER REPLAY-vs-FORWARD IPC DEADLOCK in serve_attach, triggered only by a NON-EMPTY OutputLog ring at attach (i.e. an already-producing harness = exactly endpoint</EVENT-PART>
<EVENT-PART seq="2/8" id="06d115c4">-run).

EVIDENCE (instrumented the real dummy fixture, full ordered broker log):
- drain DOES append; ring fills to 9 chunks. serve_attach attaches to the right session; become_controller replays from seq0.
- serve forwards seq0/seq1 via net_stream_send, BUT broker-side send_stream(stream=1) NEVER runs (no SEND_STREAM_ENTER stream=</EVENT-PART>
<EVENT-PART seq="3/8" id="06d115c4">1) -&gt; the operator-side loopback read pump (op_r) never reads -&gt; rc gets 0 bytes.

MECHANISM: serve_attach RECEIVES session Output (broker-&gt;serve) AND SENDS its forwards (serve-&gt;broker) on the SAME broker IPC connection. become_controller does a synchronous MULTI-FRAME ring replay inline inside dispatch_subscribe, holding the</EVENT-PART>
<EVENT-PART seq="4/8" id="06d115c4"> broker's single-threaded per-conn handler in the broker-&gt;serve write loop. While stuck there it cannot read serve's serve-&gt;broker forward frames. Both directions back up -&gt; mutual IPC backpressure deadlock. Operator receives nothing.

WHY UNCAUGHT: known-good local_attach_via_loopback E2E attaches to an IDLE echo child (empty r</EVENT-PART>
<EVENT-PART seq="5/8" id="06d115c4">ing -&gt; 0-frame replay -&gt; no storm) and only forwards ONE chunk; the cwd E2E reads on the spawning conn (no serve-over-IPC). Neither exercises a full-ring replay + sustained forward on one conn.

LIKELY ALSO CROSS-NODE: same serve_attach shape forwards over QUIC on the same conn -&gt; a busy/pre-producing session attached cross-node</EVENT-PART>
<EVENT-PART seq="6/8" id="06d115c4"> should deadlock too (just never tested with a populated ring).

FIX OPTIONS (need your design ruling):
(a) serve_attach forwards on a SEPARATE broker connection from the one it subscribes/receives on -&gt; replay-write and forward-read no longer share the single-threaded conn handler. Most surgical, local to serve_attach + dispatcher</EVENT-PART>
<EVENT-PART seq="7/8" id="06d115c4">. send_stream is keyed by stream_id (conn-agnostic) so this is clean.
(b) make become_controller's controller replay NON-BLOCKING via the dedicated-writer-thread+bounded-channel pattern add_viewer/viewer_writer already use, so dispatch_subscribe returns promptly. More complete (fixes cross-node too) but touches the authoritative cursor-adva</EVENT-PART>
<EVENT-PART seq="8/8" id="06d115c4">ncing controller path.

My lean: (a) for the surgical v0.12.1 fix, with a note to consider (b)/cross-node hardening. Your call on design + whether to fold the cross-node case now. Then I activa
...(truncated)</event>
</task-notification>


**todlando nailed it** — and it's a clean, deep root cause. The L0 zero-output is a **broker replay-vs-forward IPC deadlock in `serve_attach`**: it receives session output *and* sends its forwards on the **same** single-threaded broker conn, and `become_controller`'s **synchronous multi-frame ring replay** (inside `dispatch_subscribe`) holds the conn's write loop so it can't read serve's forwards → mutual backpressure deadlock. Triggers **only with a non-empty OutputLog ring at attach** = an already-producing harness = exactly `endpoint run`. The known-good E2E used an *idle* child (empty ring → 0-frame replay) so it never fired. That's precisely why every prior test passed — and why your dummy-harness (a *producing* harness) caught it.

Design ruling needed. Ruling todlando now (so it proceeds), then surfacing the tradeoff to you:


$OWL send todlando <<'EOF'
RULING: Option (a) for v0.12.1 — serve_attach forwards on a SEPARATE broker conn from the one it subscribes/receives on. It's a structural root fix (decouples the two blocking IPC directions onto independent single-threaded handlers, removing the mutual block), NOT a tape; it does NOT touch the authoritative cursor-advancing controller path (lowest risk for a PATCH); send_stream being stream_id-keyed makes it clean. Excellent root-cause.

CROSS-NODE: (a) covers it BY CONSTRUCTION — cross-node uses the same single-conn multiplex (forward-over-QUIC on the subscribe conn), so the same conn-separation fixes it. Don't block v0.12.1 on a twohost run: gate the LOCAL case now; if the [twohost] rig is cheap add a populated-ring cross-node rider, else document cross-node as covered-by-construction + leave a note for twohost validation.

REQUIREMENTS for the fix:
- Preserve ORDER: replay (historical) then live, in order, to the operator. (a) keeps serve receiving all output in order on conn-1 and forwarding in order on conn-2 — confirm no interleave.
- Preserve controller cursor semantics (delivered_through advance) unchanged — (a) shouldn't touch it; verify.
- Activate REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT [impl,unit,int], tag in-commit.
- GATE (the gap that hid this): int = attach to a LIVE, ALREADY-PRODUCING (NON-EMPTY ring) endpoint-run harness DELIVERS its DUMMY_HARNESS_TICK within a bounded window (flip your rc_saw_tick diagnostic to a hard assert). Add a unit pinning the deadlock root: a non-empty ring at attach does not wedge the forward path. This non-empty-ring-at-attach case is the regression the suite lacked.

RECORD (don't lose the deeper fix): note in the commit + REQ that the controller path does SYNCHRONOUS inline replay while viewers use the async dedicated-writer-thread+bounded-channel pattern — that divergence is the deeper root. Option (b) = unify the controller replay onto that async pattern; flag it as a principled POST-v0.12.1 follow-up (NOT now — it touches the authoritative cursor path).

Ping me SEAM-GREEN on L0, then L1 (VIEWER-CLOSE-DETACH) + L2 (ATTACH-WEDGE). I re-run the gate independently.
EOF

Rule option (a) for the L0 deadlock fix to todlando


Root nailed + ruling made. For you:

## L0 root cause (the central bug)

`serve_attach` receives session output **and** sends its forwards on the **same** single-threaded broker connection. When you attach to an *already-producing* harness, `become_controller` synchronously replays the whole output ring inline — which holds that connection's write loop so it can't read the forwards coming back → **mutual deadlock → you see nothing.** It only triggers with a **non-empty output ring at attach**, which is *every* `endpoint run` harness (they're already printing). Every prior test attached to an *idle* child (empty ring, nothing to replay) → never hit it. Your dummy-harness — a *producing* harness — is what finally exposed it. Likely affects cross-node attach too (same shape, never tested with a populated ring).

## My ruling: option (a)

**Separate the two IPC directions onto different broker connections** (forwards on their own conn, distinct from the subscribe/receive conn). It's a structural fix — decouples the deadlock — is **surgical**, does **not** touch the authoritative resume-cursor path (lowest risk for a patch), and covers cross-node by construction. todlando is building it now, gated against the real fixture (attach to a live, *producing* harness must deliver its output).

**The deeper alternative (b)** — make the controller's replay non-blocking by reusing the async writer-thread pattern viewers *already* use (viewers never deadlock because they're async; the controller diverged by replaying synchronously). More complete, but it touches the authoritative cursor-advance path → higher risk. I deferred it to a **post-v0.12.1 follow-up** to keep the patch safe, and required todlando to record the divergence so it's not lost.

**Your call:** I went surgical-(a)-now + deeper-(b)-later. If you'd rather take the principled (b) unification *now* (accepting more risk in the patch), say so — todlando just started, cheap to pivot. Otherwise it proceeds with (a).

Holding for L0 SEAM-GREEN, then L1/L2.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="baeafda4" type="msg" from="todlando">L0 SEAM-GREEN @cf5eab4 (branch v0.12.1-lifecycle). Option (a) implemented exactly as ruled.

FIX: serve_attach forwards on a SEPARATE broker conn (cold_start) from the one it subscribes/receives on. All 7 wire sends (Output/Size/Exit/Displaced/refuse-finish/eof-finish) -&gt; the wire conn; receives (subscribe/a</EVENT-PART>
<EVENT-PART seq="2/6" id="baeafda4">ttach_as/send_effect/resize/detach_session) stay on brain. Order preserved (one serve thread, seq order in, same order out). delivered_through cursor untouched. broker_name threaded to serve_attach + dispatch + 4 test call sites.

EVIDENCE (instrumented confirmation): pre-fix the broker NEVER ran send_stream(stream=1) for serve's forward</EVENT-PART>
<EVENT-PART seq="3/6" id="baeafda4">s (op_r read pump 0 reads, rc 0 bytes). Post-fix: rc_saw_tick=true, 230 bytes delivered, run 13s-&gt;2.8s.

REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT activated [impl,unit,int], tagged in-commit:
- impl: serve_attach two-conn split.
- unit: attach.rs loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock (non-empty ring at att</EVENT-PART>
<EVENT-PART seq="4/6" id="baeafda4">ach doesn't wedge the forward path — the missing regression).
- int: dummy_harness_e2e rc_saw_tick HARD ASSERT (live already-producing harness delivers tick). Old rc_connected proxy subsumed -&gt; informational (it raced the now-fast teardown).

RECORDED in commit + REQ: controller does SYNC inline replay vs viewers' async writer-th</EVENT-PART>
<EVENT-PART seq="5/6" id="baeafda4">read+channel = the deeper root; Option (b) = unify controller onto async pattern, flagged POST-v0.12.1 (touches cursor path). Cross-node = covered-by-construction (same single-conn multiplex); LOCAL gated now, no twohost run blocked on it.

GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy --workspace -D warnings clean, traceab</EVENT-PART>
<EVENT-PART seq="6/6" id="baeafda4">le EXIT=0.

Proceeding to L1 (VIEWER-CLOSE-DETACH, CREATE_BREAKAWAY_FROM_JOB + daemon-owned Job backstop) then L2 (ATTACH-WEDGE) per the JIT. Will ping SEAM-GREEN per wave for your independent re-run.</EVENT-PART></event>
</task-notification>


L0 SEAM-GREEN @cf5eab4 — concrete evidence (pre-fix 0 bytes → post-fix 230 bytes delivered, attach 13/13, the missing non-empty-ring regression unit added). My gate duty: independently verify, not trust the relay (the v0.12.0 lesson). Code-reading the diff now (safe regardless of todlando's live tree); then deciding on the independent real-harness run:


cd /c/Users/decid/Documents/projects/spt-core
git fetch origin v0.12.1-lifecycle 2>&1 | tail -1
echo "=== cf5eab4 available? + stat ==="; git show cf5eab4 --stat 2>&1 | head -20
echo "=== disk free C: ==="; powershell -NoProfile -Command "$d=Get-PSDrive C; '{0:N1} GB free' -f ($d.Free/1GB)"

Confirm cf5eab4 + stat + check disk for gate-run decision


Error: Exit code 1
 * branch            v0.12.1-lifecycle -> FETCH_HEAD
=== cf5eab4 available? + stat ===
commit cf5eab4b8905a16494d148fd7feed8f581d07eb6
Author: Reavo End <decidel@gmail.com>
Date:   Thu Jun 18 18:04:49 2026 -0700

    v0.12.1 L0 keystone — fix endpoint-run attach "shows no output" (serve_attach two-conn split)

    REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (the operator's central "attach shows
    no output"). Root-caused on the real dummy-harness fixture: NOT a drain/ConPTY
    bug — a REPLAY-vs-FORWARD IPC deadlock in serve_attach.

    serve_attach RECEIVED session output (broker->serve) AND SENT its wire forwards
    (serve->broker) on the SAME broker IPC connection. become_controller's
    synchronous inline multi-frame ring replay holds the broker's single-threaded
    per-conn handler, so it cannot read serve's forward frames -> mutual backpressure
    deadlock -> operator receives 0 bytes. Triggers ONLY with a NON-EMPTY OutputLog
    ring at attach = an already-producing harness = exactly endpoint-run. The
    loopback E2Es only attach to an idle child (empty-ring replay) so never caught
    it; cross-node shares the same single-conn multiplex (covered by construction).

    FIX (Option a, doyle ruling): serve_attach forwards on a SEPARATE broker conn
=== disk free C: ===
=Get-PSDrive : The term '=Get-PSDrive' is not recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ =Get-PSDrive C; '{0:N1} GB free' -f (.Free/1GB)
+ ~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (=Get-PSDrive:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

.Free/1GB : The term '.Free/1GB' is not recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:38
+ =Get-PSDrive C; '{0:N1} GB free' -f (.Free/1GB)
+                                      ~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (.Free/1GB:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException


Diff confirmed present. Let me read the actual serve_attach code change + get disk free cleanly:


cd /c/Users/decid/Documents/projects/spt-core
echo "=== files changed ==="; git show cf5eab4 --stat 2>/dev/null | tail -15
echo "=== serve_attach two-conn split (the impl) ==="; git show cf5eab4 -- crates/spt-daemon/src/attach.rs crates/spt-daemon/src/dispatch.rs 2>/dev/null | grep -E '^\+|^-|^@@|^diff' | grep -vE '^\+\+\+|^---' | head -70

Show serve_attach/dispatch diff for the two-conn split


=== files changed ===
    - int: dummy_harness_e2e rc_saw_tick flipped to a hard assert (LIVE
      already-producing endpoint-run harness delivers DUMMY_HARNESS_TICK; the old
      rc_connected proxy is subsumed and now informational).

    Gate-green local: attach suite 13/13, dummy fixture green, clippy --workspace
    -D warnings clean, traceable-reqs check EXIT=0.

    Co-authored by: todlando

 crates/spt-daemon/src/attach.rs       |  38 +++++++---
 crates/spt-daemon/src/dispatch.rs     |   2 +-
 crates/spt-daemon/tests/attach.rs     | 127 ++++++++++++++++++++++++++++++++--
 crates/spt/tests/dummy_harness_e2e.rs |  40 ++++++-----
 traceable-reqs.toml                   |   2 +-
 5 files changed, 177 insertions(+), 32 deletions(-)
=== serve_attach two-conn split (the impl) ===
diff --git a/crates/spt-daemon/src/attach.rs b/crates/spt-daemon/src/attach.rs
@@ -47,7 +47,7 @@ use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
-use crate::brain::{Brain, BrokerEvent};
+use crate::brain::{now_ms, Brain, BrokerEvent};
@@ -103,14 +103,36 @@ enum ServeRole {
+// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
+    broker_name: &str,
+    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS
+    // (session Output / Size / Exit / Displaced / detach-finish onto the operator
+    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES
+    // session output (broker→serve) — including the controller's synchronous,
+    // inline, multi-frame ring replay (`become_controller`), which holds the
+    // broker's single-threaded per-conn handler for the whole replay. If the
+    // forwards rode `brain` too, that stuck handler could not read them →
+    // serve→broker backs up while broker→serve replay backs up → mutual IPC
+    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an
+    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The
+    // two directions on independent single-threaded handlers cannot block each
+    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from
+    // a second conn is correct. Order is preserved: this one serve thread reads
+    // every Output in seq order on `brain` and forwards it in that same order on
+    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a
+    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-
+    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying
+    // the controller onto that async path would remove the inline-replay block at
+    // the source, but it touches the authoritative cursor-advancing path.)
+    let mut wire = Brain::cold_start(broker_name, now_ms())?;
@@ -150,7 +172,7 @@ pub fn serve_attach(
-                                brain.net_stream_send(stream_id, &[], None, true)?;
+                                wire.net_stream_send(stream_id, &[], None, true)?;
@@ -219,7 +241,7 @@ pub fn serve_attach(
-                        brain.net_stream_send(stream_id, &[], None, true)?;
+                        wire.net_stream_send(stream_id, &[], None, true)?;
@@ -227,12 +249,12 @@ pub fn serve_attach(
-                brain.net_stream_send(stream_id, &line, None, false)?;
+                wire.net_stream_send(stream_id, &line, None, false)?;
-                brain.net_stream_send(stream_id, &line, None, true)?;
+                wire.net_stream_send(stream_id, &line, None, true)?;
@@ -240,7 +262,7 @@ pub fn serve_attach(
-                brain.net_stream_send(stream_id, &[], None, true)?;
+                wire.net_stream_send(stream_id, &[], None, true)?;
@@ -265,11 +287,11 @@ pub fn serve_attach(
-                brain.net_stream_send(stream_id, &line, None, false)?;
+                wire.net_stream_send(stream_id, &line, None, false)?;
-                brain.net_stream_send(stream_id, &line, None, true)?;
+                wire.net_stream_send(stream_id, &line, None, true)?;
diff --git a/crates/spt-daemon/src/dispatch.rs b/crates/spt-daemon/src/dispatch.rs
@@ -347,7 +347,7 @@ fn worker(
-                Some(endpoint) => match serve_attach(&mut brain, stream_id, &endpoint, origin, 0) {
+                Some(endpoint) => match serve_attach(&mut brain, broker_name, stream_id, &endpoint, origin, 0) {


L0 code-read **PASS** — the fix matches my ruling exactly: a separate `wire = Brain::cold_start(broker_name)` conn carries all forwards (Output/Size/Exit/Displaced/finish); `brain` keeps the receives incl the synchronous replay; order preserved (one serve thread, seq order in → same order out on wire); `delivered_through` cursor untouched; `broker_name` threaded through `dispatch.rs`. The deeper-root + (b) follow-up are documented in the code comment. Clean.

Acknowledging todlando + checking disk for the comprehensive gate:


$OWL send todlando <<'EOF'
L0 CODE-READ PASS @cf5eab4 — matches the ruling exactly: separate `wire` conn (cold_start) carries all 7 forwards; `brain` keeps the receives incl the synchronous replay; order preserved (one serve thread, seq in→same order out); delivered_through untouched; broker_name threaded through dispatch worker. Regression unit (non-empty-ring-at-attach delivers) + int hard-assert added. Deeper-root + (b) follow-up documented. Strong root-cause + clean fix.

Proceed to L1 (VIEWER-CLOSE-DETACH) + L2 (ATTACH-WEDGE).

GATING CADENCE: I code-read each wave as it lands (done for L0). For the independent REAL-HARNESS test re-run, I'll do ONE comprehensive pass over L0+L1+L2 at your L2 SEAM-GREEN — in a SEPARATE worktree + separate CARGO_TARGET_DIR so I never collide with your live edits/build in the shared C:/.../spt-core tree (note: we share that working tree — keep committing as you go; I read via git show, and gate-run only in an isolated worktree). That respects the no-mock discipline (your per-wave real run + my comprehensive independent real run before deployah) without 3 redundant 20GB builds.

Keep going — ping me L1 then L2 SEAM-GREEN.
EOF
echo "=== disk free C: ==="; powershell -NoProfile -Command "\$d = Get-PSDrive C; '{0:N1} GB free' -f (\$d.Free/1GB)"

Ack L0 code-read PASS to todlando + check disk


/clear


i'm noticing that `--help` CLI for SPT uses MD formatting syntax, but doesn't actually render MD any differently.Proceed with next steps


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

Note: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions.


[Request interrupted by user for tool use]


No response requested.


<owl-active-perch id="doyle" live="true">
You have an active live agent session running as "doyle".

You are Self. Your Psyche (doyle-psyche) monitors you in the background.

Key commands:
- Send context updates: /spt:commune (canonical — Write tool to .claude/doyle-commune.md per the skill)
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> doyle    (fire-and-forget)
- Wait for reply: $OWL ring <target> doyle   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> doyle
- Schedule reminders: $OWL new-alarm doyle <time> -- <message>
- Stop session: /spt:force-stop doyle

Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
Wait for user input or a background task to complete.

DO NOT run /spt:live or /spt:ready -- your session is already active.
</owl-active-perch>
<psyche-context id="doyle">
<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="v0.12.1-lifecycle" head_sha="cf5eab4b8905a16494d148fd7feed8f581d07eb6" head_subject="v0.12.1 L0 keystone — fix endpoint-run attach "shows no output" (serve_a…" commits_since="0" commits_unpulled="0"/>
**v0.12.1 sprint LIVE**: L0 ROOT CAUSE + FIX SEAM-GREEN @cf5eab4. Root: **broker IPC deadlock** in `serve_attach` — receives session output + sends forwards on same single-threaded broker conn; `become_controller`'s synchronous ring replay holds the write loop, blocking forwards receipt → mutual backpressure. Triggers **only with non-empty OutputLog at attach** (every endpoint-run harness is already producing). Design ruling (option a): **separate two-conn multiplex** — forwards on cold_start wire, receives stay on brain. Surgical, untouches delivered_through cursor, covers cross-node by construction. Post-v0.12.1 follow-up (option b): unify controller onto async writer-thread pattern (deeper fix, higher cursor-path risk). Evidence: pre-fix 0 bytes → post-fix 230 bytes delivered, 13s→2.8s, rc_saw_tick hard assert activated. Code-read PASS (doyle). GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy clean, traceable EXIT=0. **L0 acknowledged + proceeding to L1 (VIEWER-CLOSE-DETACH + daemon-owned Job backstop) then L2 (ATTACH-WEDGE).** Will ping SEAM-GREEN per wave for independent re-run.<project-context-resolved name="spt-core"/>
L0 CLOSED: broker IPC deadlock root cause confirmed (non-empty ring synchronous replay deadlock). Remedy: two-conn split (option a, surgical, low-risk patch path). REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT activated [impl=serve_attach two-conn, unit=loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock, int=dummy_harness_e2e rc_saw_tick hard assert]. Deeper divergence recorded (controller sync replay vs viewers async): option (b) = full unification, flagged post-v0.12.1. Next: L1 tab-close harness kill, L2 loopback backpressure wedge.</psyche-context>


nvm, proceed