Root cause found — clean and high-confidence:

## The harness dies because the daemon never breaks away from the terminal's Job Object

- **ConPTY isolation is already correct** — portable-pty creates the pseudoconsole *inside the daemon*, the harness child is bound to it with no inherited handles and no shared console. So `CTRL_CLOSE` from your tab can't reach it. Not a console bug.
- **The real mechanism:** Windows Terminal / VS Code put the shell they launch — *and every descendant* — into a **Job Object with `KILL_ON_JOB_CLOSE`**. Close the tab → the OS kills everything in that job. A child escapes only with **`CREATE_BREAKAWAY_FROM_JOB`** — and a grep of the whole tree shows **it's used nowhere**.
- Daemon spawn flags (`daemon.rs:707`, `deelevate.rs:519`) use `DETACHED_PROCESS`/`CREATE_NEW_CONSOLE` — which drop the *console* but **not** job membership. So the daemon, its broker, the ConPTY host, and the harness are all in your terminal's job → tab close reaps them.
- Why the daemon *looked* like it survived: it was a **prior** daemon instance living in a benign/no job; the freshly broker-spawned harness was bound to the launching tab's job lifetime. Same root defect either way: **missing job breakaway**.

This is Windows-specific. On Unix the daemon's session detachment already covers it (terminal close → SIGHUP to the terminal's group, not the daemon's session) — to be verified, but not the observed defect.

## Fix design (two layers)
1. **Primary — job breakaway:** add `CREATE_BREAKAWAY_FROM_JOB` to both daemon spawn paths, **plus** pin each broker-spawned harness into a **daemon-owned Job Object** (mirroring Breap) as backstop — so even if a terminal sets `SILENT_BREAKAWAY_OK=false`, the harness's lifetime binds to the daemon, never the viewer. `pty.rs` needs **no** change (isolation already right).
2. **Secondary — robustness:** the loopback-backpressure / 2-worker-exhaustion fix, so a *legitimately* dead child still can't wedge the broker.

## CONTEXT.md invariant (for your sign-off)
> **A view is not the endpoint.** An spt-hosted endpoint runs in a daemon-owned PTY, decoupled from whatever terminal launched it. Closing the tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under the daemon and stays re-attachable via `spt rc <id>`. *Implementation:* the daemon and its broker-spawned harness subtree break away from the launching terminal's process grouping — on Windows `CREATE_BREAKAWAY_FROM_JOB` + a daemon-owned Job Object for hosted harnesses; on Unix the daemon's own session detachment covers it.

## Reopen plan
1. Author that invariant into `CONTEXT.md`; mint **two `REQ-HAZARD-*`** registry-first: `VIEWER-CLOSE-DETACH` (primary) + `ATTACH-WEDGE` (robustness, folds the `status=online` check).
2. Build the **dummy-harness fixture** → reproduce both (tab-close kill + broker wedge) deterministically.
3. Fix both layers → gate with the real E2E: daemon under a parent `KILL_ON_JOB_CLOSE` job → run endpoint → close the job → assert harness **alive** + `spt rc` re-attaches + a new endpoint launches + `daemon stop` bounded.
4. (Separate follow-ups, not lifecycle: the two picker bugs + `--local` removal.)

todlando builds, I design + gate — real harness, no mocks.

**GO on this design + the invariant wording?** Once you confirm, I'll write CONTEXT.md, mint the REQs, and dispatch. (Your wedged machine is still up — clear it now, or keep it for todlando's repro?)


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 invariant should avoid unnecessary negatives: "A view is independent from the endpoint." otherwise sounds good. --- proceed with the plan, have todlando execute. see it through to a v0.12.1 release. can clear the wedged machine.


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


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…


On it. Order: set up the v0.12.1 branch + design artifacts, dispatch todlando (while owl works), then clear your machine last (so I keep coordination until the handoff). Tagline fixed to "A view is independent from the endpoint."

First, branch + conventions:


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== git state ==="; git branch --show-current; git status --short | head
echo "=== jit plan convention? ==="; ls docs/ 2>/dev/null | head -30; find . -iname '*jit*' -not -path './target*' 2>/dev/null | head; ls docs/jit 2>/dev/null
echo "=== create v0.12.1-lifecycle off origin/main ==="; git fetch origin main 2>&1 | tail -1; git checkout -b v0.12.1-lifecycle origin/main 2>&1 | tail -3; git log --oneline -1

Check git state + JIT convention, create v0.12.1 branch off main


=== git state ===
v0.12.0-lifecycle
=== jit plan convention? ===
BROKER-BRAIN-SPLIT-RESTORATION.md
CONTEXT-MEMORY.md
DEBUG-CONVERGE-PLAN.md
DEBUG-ROLLOUT.md
DEFERRED.md
DOCS-STRATEGY.md
DORMANCY-BUDGET.md
FAULT-MATRIX.md
FLAKE-LEDGER.md
KNOWN-HAZARDS.md
MANIFEST.md
RELEASE-RUNBOOK.md
STORAGE.md
TRACEABILITY.md
TWO-HOST-RUNBOOK.md
adr
reviews
spikes
./M9-WAVE3-JIT.md
=== create v0.12.1-lifecycle off origin/main ===
 * branch            main       -> FETCH_HEAD
branch 'v0.12.1-lifecycle' set up to track 'origin/main'.
Switched to a new branch 'v0.12.1-lifecycle'
5f9ea23 docs(changelog): correct v0.12.0 notes — scope resume-picker to ready agents + add un-host Psyche reap


Branch `v0.12.1-lifecycle` created off main. JIT plans live at root (`*-JIT.md`). Writing the anchor plan:


Write

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


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

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


JIT plan anchored. Now the CONTEXT.md invariant — finding the right anchor (near the bringup section):


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== candidate anchors in CONTEXT.md ==="; grep -n "spt endpoint run\|spt-hosted bringup\|daemon-host\|re-attach\|reattach\|attach pump\|rc pump\|detach" CONTEXT.md | head -20

Find CONTEXT.md anchor for the view-independence invariant


=== candidate anchors in CONTEXT.md ===
34:- **daemon brain** ("userspace") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.
94:Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.
138:- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-hosted `[session.psyche_init]` runner, where the brain's live-host reconcile resolves the Psyche program against the matching record's `source_dir` — and the `--manifest` file's parent dir when an explicit `--manifest` overrides on the api seam.
161:**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own running id**. It is **node-local** — purge reaches only *this* node's records; a remote endpoint's records are unreachable and its subnet-registry rows decay on their own via the epoch-lease eviction. It removes: the perch directory **tree** recursively (incl every nested `{id}-psyche` / `{id}-w*` / shell child — info.json, ready marker, the `sessions.log` ledger, spool, the idle/echo-gate sentinels, the auth token); the registry address; the **context store** (`a-<id>` branch + worktree and the `<id>/` rows in every `p-<project>` branch — the same path `endpoint fork --delete-source` uses); and the node-local trust rows keyed on the id (access + visibility). Implementation-wise it is `fork --delete-source` generalized (recursive perch-remove + unregister + context `remove_endpoint`) plus the trust-record cleanup, sharing `endpoint rename`'s record-set enumeration and offline-only gate.
343:**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)
345:**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
347:The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
359:status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
363:through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
368:**`spt endpoint run` is the spt-hosted bringup for BOTH endpoint types** (v0.12.0):
414:A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*
501:**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.
535:- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
682:**subnet attachment (attached / detached)**:
683:Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon config, renamed from the once-planned `--auto`; an unsaved flip deliberately does not survive a daemon restart). A *degraded* daemon that cannot serve at all (net-less broker — endpoint bind failed) is rendered as **no connection**, never conflated with deliberate detachment. Corollary, ratified 2026-06-06: **membership implies reachability** — the membership-creating verbs (`subnet create`, `subnet join`) ensure the daemon is running, because a sole seed-holder with no responder is a contradiction of the subnet's purpose.


Best home is the daemon-persistence definition (line 414). Reading it precisely to insert the invariant cleanly:


412
413	**term-daemon** (process supervisor):
414	A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*
415
416	**session surface** (library abstraction):
417	The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.
418
419	**input injection**:

**term-daemon** (process supervisor):
A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

**session surface** (library abstraction):
The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.

**input injection**:


note that you are running on legacy spt, so there's no issue modifying spt-core on this machine.


Edit

The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

**A view is independent from the endpoint** (invariant):
<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->
An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the
tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under
the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never
the session's lifeline. *Implementation:* the daemon **and** its broker-spawned harness subtree must break away from the
launching terminal's process grouping — on Windows the daemon spawns with `CREATE_BREAKAWAY_FROM_JOB` and each hosted
harness is pinned to a **daemon-owned Job Object** (so a terminal's `KILL_ON_JOB_CLOSE` job can never reap it, even where
`SILENT_BREAKAWAY_OK` is refused); on Unix the daemon's own session detachment (new session, no controlling terminal)
already keeps the SIGHUP of a closing terminal off the daemon's children. (The ConPTY/pseudoconsole isolation is correct
on its own — the lifetime binding that leaks is the Job Object, not the console.)


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

The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

**A view is independent from the endpoint** (invariant):
<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->
An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the
tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under
the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never
the session's lifeline. *Implementation:* the daemon **and** its broker-spawned harness subtree must break away from the
launching terminal's process grouping — on Windows the daemon spawns with `CREATE_BREAKAWAY_FROM_JOB` and each hosted
harness is pinned to a **daemon-owned Job Object** (so a terminal's `KILL_ON_JOB_CLOSE` job can never reap it, even where
`SILENT_BREAKAWAY_OK` is refused); on Unix the daemon's own session detachment (new session, no controlling terminal)
already keeps the SIGHUP of a closing terminal off the daemon's children. (The ConPTY/pseudoconsole isolation is correct
on its own — the lifetime binding that leaks is the Job Object, not the console.)

A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

+**A view is independent from the endpoint** (invariant):

+An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the

+tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under

+the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never

+the session's lifeline. *Implementation:* the daemon **and** its broker-spawned harness subtree must break away from the

+launching terminal's process grouping — on Windows the daemon spawns with `CREATE_BREAKAWAY_FROM_JOB` and each hosted

+harness is pinned to a **daemon-owned Job Object** (so a terminal's `KILL_ON_JOB_CLOSE` job can never reap it, even where

+`SILENT_BREAKAWAY_OK` is refused); on Unix the daemon's own session detachment (new session, no controlling terminal)

+already keeps the SIGHUP of a closing terminal off the daemon's children. (The ConPTY/pseudoconsole isolation is correct

+on its own — the lifetime binding that leaks is the Job Object, not the console.)

**session surface** (library abstraction):

The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.


Prompt: note that you are running on legacy spt, so there's no issue modifying spt-core on this machine.


Good to know — I'm on **legacy spt**, so the spt-core daemon (76560) + wall-a/b/c are the device-under-test, separate from my legacy-owl coordination. That de-risks clearing the machine (I won't lose owl reachability) and means we can validate v0.12.1 against your real claude-spt setup here, not just a scratch dir. I'll still scope the clear to the spt-core path/pid (legacy may share the `spt` name).

Now minting the REQs registry-first. Reading the toml tail to match format:


605	id = "REQ-HAZARD-UNHOST-PSYCHE-REAP"
606	title = "On un-host, the detached `{id}-psyche` HARNESS PROCESS is reaped — not just its in-brain pulse-driver thread. Today stop_host (livehost.rs:203) trips the HostedLife stop flag + JOINS the driver thread, but the Psyche is a detached harness process (spawn_psyche → ManifestRuntime detached spawn, runtime.rs:341-356; its pid is untracked in HostedLife though stamped on the `{id}-psyche` perch, where residency-confirm already reads it). So endpoint-stop / mid-life agent-death / a B2/B5 offline-then-unhost leaves the psyche process ORPHANED, alive until the next daemon-stop (where Breap's job/group reaps the whole brain subtree). The Psyche STAYS a harness process by design (CONTEXT.md 97/203/251 — headless harness session, its own perch) — the fix does NOT move it in-brain; it SCOPED-kills the `{id}-psyche` pid on un-host (never machine-wide — shared box). Track the pid in HostedLife at host_one (cleanest) or read the `{id}-psyche` perch pid at stop_host. Composes with H3 (endpoint stop → offline → reconcile un-host → reap) and B2/B5 (the offline arms that trigger un-host). (v0.12.0)"
607	required_stages = ["impl", "unit", "int"]  # activated v0.12.0 wave 5 (operator-greenlit fold-in): impl = spawn_session_owned/spawn_psyche_owned RETAIN the Child up to HostedLife.psyche_child; stop_host reaps BY HANDLE (child.kill()+wait(), recycle-proof — NOT a bare pid that could hit a recycled sibling); unit = stop_host_reaps_the_detached_psyche_process (real stand-in terminated); int = endpoint-stop of a live spt-hosted agent leaves no {id}-psyche process
608	[[requirements]]
609	id = "REQ-ENDPOINT-PURGE"
610	title = "`spt endpoint purge <id>` fully removes an endpoint AND every record keyed on it — the formal teardown devs/CI need for clean test setup/reset. NOT consent-gated (a local dev/test op — no peer consent). OFFLINE-ONLY: refuses while the endpoint is online / daemon-hosted (deleting records out from under a live host risks the daemon re-creating or re-hosting mid-purge); `--force` STOPS it first (endpoint stop → wait for the daemon reconcile to un-host + reap the Psyche) THEN purges. Confirms interactively unless `--yes` (the CI path). Refuses purging the CALLER's OWN running id. All LOCAL — purge reaches only THIS node's records; a remote endpoint's records can't be touched, and its subnet-registry rows decay via the epoch-lease eviction (REQ-HAZARD-REGISTRY-DECAY). Removes: (1) the perch dir TREE recursively — owlery/<id>/ incl every nested {id}-psyche / {id}-w* / shells child (info.json, ready marker, sessions.log ledger, spool.db, inbox, .idle/.more-done sentinels, auth token); (2) the registry address (registry::unregister_address); (3) the context store — ContextStore::remove_endpoint(id): the a-<id> branch+worktree + the <id>/ rows from every p-<project> branch (the same fn `fork --delete-source` already uses); (4) node-local trust rows keyed on the id — access.json + visibility.json. Reuse-heavy: it is `fork --delete-source` generalized (recursive perch remove + unregister + remove_endpoint) + the trust-record cleanup; `endpoint rename` already enumerates the same record set + uses the same offline-only gate. (v0.12.0)"
611	required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `spt endpoint purge`); built v0.12.0 wave 5. impl = cmd_endpoint_purge (offline-gate + --force stop-then-wait-offline; recursive remove_dir_all of the perch tree incl nested; unregister_address; ContextStore::remove_endpoint; access.json + visibility.json row removal) + EndpointCmd::Purge{id, yes, force}; unit = offline-gate refuses an online ep without --force + self-purge guard; int = create endpoint w/ perch + a-<id> context branch → purge → assert EVERY record gone (perch tree, registry row, a-<id> + p-* rows, access/visibility)
612	[[requirements]]
613	id = "REQ-READY-AGENT-RESUME"
614	title = "An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint run --resume <session>` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0)"
615	required_stages = ["doc", "impl", "unit", "int"]  # operator add v0.12.0; built. FIX(2)=SUBSUMED (verified-at-code, ZERO code): cmd_endpoint_run is type-agnostic, the no-psyche discriminator is livehost reconcile's start-side STATE gate (info.state != live_agent → skip a ready_agent perch) ahead of the psyche_init gate. doc = CONTEXT.md `spt endpoint run` is the bringup for BOTH types. impl = ReadyAgent::start_homed ledgers a Boot row (sessions::append, mirror establish_perch:250). unit = start_ledgers_a_boot_session_row (ready bind writes one Boot row, survives soft_cleanup/offline). int = ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers the carried session + reconcile_once hosts NO psyche even with a live-capable psyche_init adapter resolved — state gate)
616	[[requirements]]
617	id = "REQ-PICKER-ADAPTER-DESCRIPTION"
618	title = "The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0)"
619	required_stages = []  # DEFERRED fast-follow (operator 2026-06-18) — NOT built in v0.12.0; registry-first placeholder
620	[[requirements]]
621	id = "REQ-HAZARD-VIEWER-ISOLATION"
622	title = "A slow / dead / hostile VIEWER must NEVER stall the controller, the PTY child, or the session drain thread. The broker drain fans output to the controller on the authoritative blocking bounded path (advances delivered_through) but to each viewer via a bounded per-viewer channel with a dedicated writer thread; the drain `try_send`s under the log lock and a viewer whose bounded queue OVERFLOWS (can't keep up) is EVICTED (queue dropped, writer thread ends, removed from the viewers map) — the drain thread NEVER touches a viewer socket, so no viewer write can backpressure or block it. A soft viewer cap bounds the thread count. Viewer eviction never perturbs the controller stream, the delivered_through cursor, or the child."
623	required_stages = ["unit", "int"]  # activated M12-W2.5 (unit: try_send-overflow eviction decision on the fan-out; int: a wedged viewer is evicted and the controller stream + child run on unaffected)
624
625	# ───────────────────────────── Install / migration / infra ──────────────────
626	[[requirements]]
627	id = "REQ-INSTALL-1"
628	title = "Two install paths; signed one-line script; OS-service registration"
629	required_stages = ["doc", "impl", "int"]  # activated M6-D2: installer/install.sh + install.ps1 — the standalone one-line script half (latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → absolute-path print); the harness-bootstrapped half (path a) calls into the same script by design (CONTEXT §Installation). int = oneliner_e2e.rs staged-release rung on both runners. OS-service registration EXCLUDED → docs/DEFERRED.md (daemon auto-start covers dev-stage); "signed" = sha256 at first fetch, full ed25519 is `spt update`'s job (grill decision 3)

id = "REQ-HAZARD-UNHOST-PSYCHE-REAP"
title = "On un-host, the detached `{id}-psyche` HARNESS PROCESS is reaped — not just its in-brain pulse-driver thread. Today stop_host (livehost.rs:203) trips the HostedLife stop flag + JOINS the driver thread, but the Psyche is a detached harness process (spawn_psyche → ManifestRuntime detached spawn, runtime.rs:341-356; its pid is untracked in HostedLife though stamped on the `{id}-psyche` perch, where residency-confirm already reads it). So endpoint-stop / mid-life agent-death / a B2/B5 offline-then-unhost leaves the psyche process ORPHANED, alive until the next daemon-stop (where Breap's job/group reaps the whole brain subtree). The Psyche STAYS a harness process by design (CONTEXT.md 97/203/251 — headless harness session, its own perch) — the fix does NOT move it in-brain; it SCOPED-kills the `{id}-psyche` pid on un-host (never machine-wide — shared box). Track the pid in HostedLife at host_one (cleanest) or read the `{id}-psyche` perch pid at stop_host. Composes with H3 (endpoint stop → offline → reconcile un-host → reap) and B2/B5 (the offline arms that trigger un-host). (v0.12.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.12.0 wave 5 (operator-greenlit fold-in): impl = spawn_session_owned/spawn_psyche_owned RETAIN the Child up to HostedLife.psyche_child; stop_host reaps BY HANDLE (child.kill()+wait(), recycle-proof — NOT a bare pid that could hit a recycled sibling); unit = stop_host_reaps_the_detached_psyche_process (real stand-in terminated); int = endpoint-stop of a live spt-hosted agent leaves no {id}-psyche process
[[requirements]]
id = "REQ-ENDPOINT-PURGE"
title = "`spt endpoint purge <id>` fully removes an endpoint AND every record keyed on it — the formal teardown devs/CI need for clean test setup/reset. NOT consent-gated (a local dev/test op — no peer consent). OFFLINE-ONLY: refuses while the endpoint is online / daemon-hosted (deleting records out from under a live host risks the daemon re-creating or re-hosting mid-purge); `--force` STOPS it first (endpoint stop → wait for the daemon reconcile to un-host + reap the Psyche) THEN purges. Confirms interactively unless `--yes` (the CI path). Refuses purging the CALLER's OWN running id. All LOCAL — purge reaches only THIS node's records; a remote endpoint's records can't be touched, and its subnet-registry rows decay via the epoch-lease eviction (REQ-HAZARD-REGISTRY-DECAY). Removes: (1) the perch dir TREE recursively — owlery/<id>/ incl every nested {id}-psyche / {id}-w* / shells child (info.json, ready marker, sessions.log ledger, spool.db, inbox, .idle/.more-done sentinels, auth token); (2) the registry address (registry::unregister_address); (3) the context store — ContextStore::remove_endpoint(id): the a-<id> branch+worktree + the <id>/ rows from every p-<project> branch (the same fn `fork --delete-source` already uses); (4) node-local trust rows keyed on the id — access.json + visibility.json. Reuse-heavy: it is `fork --delete-source` generalized (recursive perch remove + unregister + remove_endpoint) + the trust-record cleanup; `endpoint rename` already enumerates the same record set + uses the same offline-only gate. (v0.12.0)"
required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `spt endpoint purge`); built v0.12.0 wave 5. impl = cmd_endpoint_purge (offline-gate + --force stop-then-wait-offline; recursive remove_dir_all of the perch tree incl nested; unregister_address; ContextStore::remove_endpoint; access.json + visibility.json row removal) + EndpointCmd::Purge{id, yes, force}; unit = offline-gate refuses an online ep without --force + self-purge guard; int = create endpoint w/ perch + a-<id> context branch → purge → assert EVERY record gone (perch tree, registry row, a-<id> + p-* rows, access/visibility)
[[requirements]]
id = "REQ-READY-AGENT-RESUME"
title = "An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint run --resume <session>` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0)"
required_stages = ["doc", "impl", "unit", "int"]  # operator add v0.12.0; built. FIX(2)=SUBSUMED (verified-at-code, ZERO code): cmd_endpoint_run is type-agnostic, the no-psyche discriminator is livehost reconcile's start-side STATE gate (info.state != live_agent → skip a ready_agent perch) ahead of the psyche_init gate. doc = CONTEXT.md `spt endpoint run` is the bringup for BOTH types. impl = ReadyAgent::start_homed ledgers a Boot row (sessions::append, mirror establish_perch:250). unit = start_ledgers_a_boot_session_row (ready bind writes one Boot row, survives soft_cleanup/offline). int = ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers the carried session + reconcile_once hosts NO psyche even with a live-capable psyche_init adapter resolved — state gate)
[[requirements]]
id = "REQ-PICKER-ADAPTER-DESCRIPTION"
title = "The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0)"
required_stages = []  # DEFERRED fast-follow (operator 2026-06-18) — NOT built in v0.12.0; registry-first placeholder
[[requirements]]
id = "REQ-HAZARD-VIEWER-ISOLATION"
title = "A slow / dead / hostile VIEWER must NEVER stall the controller, the PTY child, or the session drain thread. The broker drain fans output to the controller on the authoritative blocking bounded path (advances delivered_through) but to each viewer via a bounded per-viewer channel with a dedicated writer thread; the drain `try_send`s under the log lock and a viewer whose bounded queue OVERFLOWS (can't keep up) is EVICTED (queue dropped, writer thread ends, removed from the viewers map) — the drain thread NEVER touches a viewer socket, so no viewer write can backpressure or block it. A soft viewer cap bounds the thread count. Viewer eviction never perturbs the controller stream, the delivered_through cursor, or the child."
required_stages = ["unit", "int"]  # activated M12-W2.5 (unit: try_send-overflow eviction decision on the fan-out; int: a wedged viewer is evicted and the controller stream + child run on unaffected)

# ───────────────────────────── Install / migration / infra ──────────────────
[[requirements]]
id = "REQ-INSTALL-1"
title = "Two install paths; signed one-line script; OS-service registration"
required_stages = ["doc", "impl", "int"]  # activated M6-D2: installer/install.sh + install.ps1 — the standalone one-line script half (latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → absolute-path print); the harness-bootstrapped half (path a) calls into the same script by design (CONTEXT §Installation). int = oneliner_e2e.rs staged-release rung on both runners. OS-service registration EXCLUDED → docs/DEFERRED.md (daemon auto-start covers dev-stage); "signed" = sha256 at first fetch, full ed25519 is `spt update`'s job (grill decision 3)


Got the format. Appending the five new REQs at the file end (registry-first). Checking the tail first:


Bash

cd /c/Users/decid/Documents/projects/spt-core; echo "=== last 6 lines + line count ==="; tail -6 traceable-reqs.toml; echo "TOTAL_LINES=$(wc -l < traceable-reqs.toml)"

Read toml tail to append cleanly


=== last 6 lines + line count ===
title = "Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill)."
required_stages = ["doc", "impl", "unit", "int"]  # M11-W1 closed: doc = CONTEXT §per-capability approval gates; impl = linkhost::act_gate_decide / drive_shell gate + cli::escalate_act; unit = per_capability_act_gate_blocks_until_class_scoped_grant + run_action_refuses_gated_op_without_spooling; int = shell_actgate_e2e (class-scoped grant flips the command through the real binary)
[[requirements]]
id = "REQ-SHELL-5"
title = "Shell ownership is owner-type-agnostic: any non-Shell endpoint type may own/spawn/drive/command/link a shell (Gateway the named first) — control-exclusivity keys on the owner endpoint_id, NEVER on the owner's endpoint type. No ownership path (mint, launch, owner-from-link, cmd, drive, tunnel, sleep/wake, owner-shutdown) inspects the owner's type (CONTEXT:264, ratified 2026-06-11 Gateway grill)."
required_stages = ["doc", "impl", "unit", "int"]  # M11-W4 capstone (proof-not-change — the T0 audit found no type-gate on any ownership path; W4 proves it end-to-end): doc = CONTEXT §Shell model "Owner-linked, exclusive" owner-type-agnostic note; impl = shellhost::find_shell_by_token (the "owner from the link" resolution returns an opaque owner-id with NO type inspection — the load-bearing type-agnostic path); unit = linkhost::gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type (resolve_link_target/run_action resolve + drive a gateway-typed owner identically; a same-type different-id owner is NoShell — exclusivity keys on the id, not the type); int = gateway_owner_shell_e2e (a state="gateway" owner succeeds on EVERY path same-node — spawn/bind/cmd/drive/tunnel/act-gate/relink — and a same-type different-id gateway-B is refused on cmd/drive/tunnel)
TOTAL_LINES=1135


Appending the five REQs (registry-first; VIEWER-CLOSE-DETACH gets `["doc"]` since CONTEXT.md is tagged now, rest stay `[]` until todlando activates per wave):


Edit

required_stages = ["doc", "impl", "unit", "int"]  # M11-W4 capstone (proof-not-change — the T0 audit found no type-gate on any ownership path; W4 proves it end-to-end): doc = CONTEXT §Shell model "Owner-linked, exclusive" owner-type-agnostic note; impl = shellhost::find_shell_by_token (the "owner from the link" resolution returns an opaque owner-id with NO type inspection — the load-bearing type-agnostic path); unit = linkhost::gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type (resolve_link_target/run_action resolve + drive a gateway-typed owner identically; a same-type different-id owner is NoShell — exclusivity keys on the id, not the type); int = gateway_owner_shell_e2e (a state="gateway" owner succeeds on EVERY path same-node — spawn/bind/cmd/drive/tunnel/act-gate/relink — and a same-type different-id gateway-B is refused on cmd/drive/tunnel)

required_stages = ["doc", "impl", "unit", "int"]  # M11-W4 capstone (proof-not-change — the T0 audit found no type-gate on any ownership path; W4 proves it end-to-end): doc = CONTEXT §Shell model "Owner-linked, exclusive" owner-type-agnostic note; impl = shellhost::find_shell_by_token (the "owner from the link" resolution returns an opaque owner-id with NO type inspection — the load-bearing type-agnostic path); unit = linkhost::gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type (resolve_link_target/run_action resolve + drive a gateway-typed owner identically; a same-type different-id owner is NoShell — exclusivity keys on the id, not the type); int = gateway_owner_shell_e2e (a state="gateway" owner succeeds on EVERY path same-node — spawn/bind/cmd/drive/tunnel/act-gate/relink — and a same-type different-id gateway-B is refused on cmd/drive/tunnel)

# ───────────────────────────── v0.12.1 lifecycle reopen (real-harness gated) ──────────────────
[[requirements]]
id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
title = "A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. (v0.12.1)"
required_stages = ["doc"]  # doc by doyle (CONTEXT.md "A view is independent from the endpoint" invariant). todlando activates [doc,impl,unit,int] when building: impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + a daemon-owned Job Object holding each broker-spawned harness; unit = the daemon spawn flags include breakaway + a harness is assigned to the daemon job; int = REAL detached daemon spawned UNDER a parent-held KILL_ON_JOB_CLOSE job → endpoint run a dummy harness → close the parent job → assert the harness pid stays ALIVE + `spt rc <id>` re-attaches + a brand-new endpoint launches (the dummy-harness fixture, NOT a mock)
[[requirements]]
id = "REQ-HAZARD-ATTACH-WEDGE"
title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock (this is data-plane backpressure + worker-pool exhaustion). FIX: loopback sends fail-fast — a full-buffer / BrokenPipe loopback write is an ordinary per-stream error that ENDS serve_attach so one dead stream can never hold a runtime worker (defense-in-depth: raise worker count). Folds the status=online sub-check: a dead spt-hosted endpoint must be marked OFFLINE within one reconcile tick on abrupt child death (confirm the broker session reaps so B2 sees it absent). (v0.12.1)"
required_stages = []  # registry-first; todlando activates [impl,unit,int] when building. int = dummy-harness fixture on a REAL detached daemon: kill the child abruptly + drop the operator pump without a clean detach → assert a NEW endpoint is still served, brain.sessions() returns promptly, the dead endpoint is offlined within a tick, and `daemon stop` completes bounded
[[requirements]]
id = "REQ-PICKER-HISTORY-FRESH"
title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate. Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-PICKER-ONLINE-ACTION"
title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
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)"
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


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 = ["doc", "impl", "unit", "int"]  # M11-W4 capstone (proof-not-change — the T0 audit found no type-gate on any ownership path; W4 proves it end-to-end): doc = CONTEXT §Shell model "Owner-linked, exclusive" owner-type-agnostic note; impl = shellhost::find_shell_by_token (the "owner from the link" resolution returns an opaque owner-id with NO type inspection — the load-bearing type-agnostic path); unit = linkhost::gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type (resolve_link_target/run_action resolve + drive a gateway-typed owner identically; a same-type different-id owner is NoShell — exclusivity keys on the id, not the type); int = gateway_owner_shell_e2e (a state="gateway" owner succeeds on EVERY path same-node — spawn/bind/cmd/drive/tunnel/act-gate/relink — and a same-type different-id gateway-B is refused on cmd/drive/tunnel)

# ───────────────────────────── v0.12.1 lifecycle reopen (real-harness gated) ──────────────────
[[requirements]]
id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
title = "A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. (v0.12.1)"
required_stages = ["doc"]  # doc by doyle (CONTEXT.md "A view is independent from the endpoint" invariant). todlando activates [doc,impl,unit,int] when building: impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + a daemon-owned Job Object holding each broker-spawned harness; unit = the daemon spawn flags include breakaway + a harness is assigned to the daemon job; int = REAL detached daemon spawned UNDER a parent-held KILL_ON_JOB_CLOSE job → endpoint run a dummy harness → close the parent job → assert the harness pid stays ALIVE + `spt rc <id>` re-attaches + a brand-new endpoint launches (the dummy-harness fixture, NOT a mock)
[[requirements]]
id = "REQ-HAZARD-ATTACH-WEDGE"
title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock (this is data-plane backpressure + worker-pool exhaustion). FIX: loopback sends fail-fast — a full-buffer / BrokenPipe loopback write is an ordinary per-stream error that ENDS serve_attach so one dead stream can never hold a runtime worker (defense-in-depth: raise worker count). Folds the status=online sub-check: a dead spt-hosted endpoint must be marked OFFLINE within one reconcile tick on abrupt child death (confirm the broker session reaps so B2 sees it absent). (v0.12.1)"
required_stages = []  # registry-first; todlando activates [impl,unit,int] when building. int = dummy-harness fixture on a REAL detached daemon: kill the child abruptly + drop the operator pump without a clean detach → assert a NEW endpoint is still served, brain.sessions() returns promptly, the dead endpoint is offlined within a tick, and `daemon stop` completes bounded
[[requirements]]
id = "REQ-PICKER-HISTORY-FRESH"
title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate. Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-PICKER-ONLINE-ACTION"
title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
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)"
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

title = "Shell ownership is owner-type-agnostic: any non-Shell endpoint type may own/spawn/drive/command/link a shell (Gateway the named first) — control-exclusivity keys on the owner endpoint_id, NEVER on the owner's endpoint type. No ownership path (mint, launch, owner-from-link, cmd, drive, tunnel, sleep/wake, owner-shutdown) inspects the owner's type (CONTEXT:264, ratified 2026-06-11 Gateway grill)."

+# ───────────────────────────── v0.12.1 lifecycle reopen (real-harness gated) ──────────────────

+title = "A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. (v0.12.1)"

+required_stages = ["doc"]  # doc by doyle (CONTEXT.md "A view is independent from the endpoint" invariant). todlando activates [doc,impl,unit,int] when building: impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + a daemon-owned Job Object holding each broker-spawned harness; unit = the daemon spawn flags include breakaway + a harness is assigned to the daemon job; int = REAL detached daemon spawned UNDER a parent-held KILL_ON_JOB_CLOSE job → endpoint run a dummy harness → close the parent job → assert the harness pid stays ALIVE + `spt rc <id>` re-attaches + a brand-new endpoint launches (the dummy-harness fixture, NOT a mock)

+title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock (this is data-plane backpressure + worker-pool exhaustion). FIX: loopback sends fail-fast — a full-buffer / BrokenPipe loopback write is an ordinary per-stream error that ENDS serve_attach so one dead stream can never hold a runtime worker (defense-in-depth: raise worker count). Folds the status=online sub-check: a dead spt-hosted endpoint must be marked OFFLINE within one reconcile tick on abrupt child death (confirm the broker session reaps so B2 sees it absent). (v0.12.1)"

+required_stages = []  # registry-first; todlando activates [impl,unit,int] when building. int = dummy-harness fixture on a REAL detached daemon: kill the child abruptly + drop the operator pump without a clean detach → assert a NEW endpoint is still served, brain.sessions() returns promptly, the dead endpoint is offlined within a tick, and `daemon stop` completes bounded

+title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"

+required_stages = []  # registry-first; investigate-then-activate. Likely [impl,unit] once root is pinned

+title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"

+required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned

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

+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


REQs minted. Verifying traceable is green (doc tag satisfies VIEWER-CLOSE-DETACH's `["doc"]`, others `[]`):


Bash

cd /c/Users/decid/Documents/projects/spt-core; traceable-reqs check 2>&1 | tail -8; echo "EXIT=$?"

Run traceable check after minting REQs + CONTEXT.md doc tag


  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
EXIT=0


traceable EXIT=0. Committing registry-first (JIT plan + invariant + 5 REQs) + pushing:


cd /c/Users/decid/Documents/projects/spt-core
git add V0.12.1-LIFECYCLE-JIT.md CONTEXT.md traceable-reqs.toml
git commit -F - <<'EOF'
v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)

v0.12.0 shipped 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-proc reconcile_once. v0.12.1 reopens with real-harness gating.

- JIT plan (V0.12.1-LIFECYCLE-JIT.md): waves = dummy-harness fixture → lifecycle
  (VIEWER-CLOSE-DETACH primary + ATTACH-WEDGE robustness + status/daemon-stop) → picker
  (history-fresh + online-action) → endpoint-list merge-local (--local removal).
- CONTEXT.md: "A view is independent from the endpoint" invariant — spt-hosted endpoints
  run in a daemon-owned PTY decoupled from the launching terminal; closing the tab detaches
  only the rc pump; the endpoint stays running + re-attachable. [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH]
- traceable-reqs.toml: mint REQ-HAZARD-VIEWER-CLOSE-DETACH (doc activated), REQ-HAZARD-ATTACH-WEDGE,
  REQ-PICKER-HISTORY-FRESH, REQ-PICKER-ONLINE-ACTION, REQ-ENDPOINT-LIST-MERGE-LOCAL (registry-first,
  required_stages=[] until todlando activates per wave).

Co-authored by: doyle
EOF
echo "=== push ==="; git push -u origin v0.12.1-lifecycle 2>&1 | tail -4

Commit registry-first mint + JIT + invariant, push branch


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 a4a76c6] v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)
 3 files changed, 121 insertions(+)
 create mode 100644 V0.12.1-LIFECYCLE-JIT.md
=== push ===
remote:
branch 'v0.12.1-lifecycle' set up to track 'origin/v0.12.1-lifecycle'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      v0.12.1-lifecycle -> v0.12.1-lifecycle


Committed + pushed (`a4a76c6`). Dispatching todlando — pointing at the JIT plan as source of truth to avoid EVENT truncation:


$OWL send todlando <<'EOF'
NEW SPRINT — v0.12.1 lifecycle reopen. You execute, I gate per-wave, deployah releases.

WHY: v0.12.0 shipped but real-harness testing (operator, claude-spt, Windows) showed the lifecycle fixes DON'T work in the real daemon+broker+PTY path. We gated on mocks + in-proc reconcile_once — green tests, broken reality. This sprint is real-harness-gated.

START HERE: branch v0.12.1-lifecycle (pushed, off main@5f9ea23). Read V0.12.1-LIFECYCLE-JIT.md on that branch — it is the authoritative plan (every item, root cause, fix, and gate is spelled out). REQs already minted registry-first in traceable-reqs.toml; activate each REQ's stages when you start its wave + tag evidence in the same commit.

WAVE ORDER:
1. Wave 1 FIRST — the dummy-harness fixture (operator's idea): a kind=harness adapter whose command is a trivial program that binds its perch on startup, prints stdout on an interval, stays alive until killed. Drives the REAL `spt endpoint run`->launch_harness_brokered->broker PTY->rc attach path. This UNBLOCKS every lifecycle gate. Build it before the fixes.
2. Wave 2 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY: CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths daemon.rs:707 + deelevate.rs:519, AND pin each broker-spawned harness into a daemon-owned Job Object backstop; pty.rs UNCHANGED, ConPTY isolation is already correct) + REQ-HAZARD-ATTACH-WEDGE (loopback sends fail-fast on a dead/undrained subscriber so one dead stream can't hold a net-runtime worker) + the status=online-offline-within-a-tick check + daemon-stop-ends-everything.
3. Wave 3 — picker P1/P2: INVESTIGATE the root FIRST (report to me before fixing — these may be real bugs or symptoms of the broker wedge / stale state).
4. Wave 4 — REQ-ENDPOINT-LIST-MERGE-LOCAL (drop --local, bare list always merges local perches, fix whoami alias; xtask gen in DEFAULT target).

BINDING GATING DISCIPLINE: every lifecycle gate runs against the REAL dummy-harness fixture + a REAL detached daemon — NOT a mock, NOT in-proc reconcile_once with a live-draining peer. That gap is exactly why v0.12.0 escaped. I re-run each gate independently.

NOTE (operator-confirmed): we are running on LEGACY spt on this machine, so spt-core can be freely modified + its daemon restarted here without touching live-agent coordination — real-harness validation can use the operator's actual claude-spt setup, not just a scratch dir.

Ping me wave-by-wave SEAM-GREEN. Start with Wave 1 + the Wave 2 keystone. Full per-item detail is in V0.12.1-LIFECYCLE-JIT.md.
EOF

Dispatch todlando for v0.12.1 sprint


➜ SENT:todlando


todlando dispatched. Now clearing your machine — scoped to the spt-core test instance (safe: I'm on legacy spt). First checking which endpoint the orphan psyche belongs to, then purging the dead endpoints via the control plane:


PowerShell

$cim = Get-CimInstance Win32_Process -Filter "ProcessId=130584"; "psyche 130584 cmdline: " + $cim.CommandLine; "=== other live spt-core procs (confirm before any clear) ==="; Get-CimInstance Win32_Process -Filter "Name='spt.exe' OR Name LIKE '%psyche%'" | Where-Object { $_.ExecutablePath -like '*spt-core*' } | ForEach-Object { "{0,-8} {1}" -f $_.ProcessId, $_.CommandLine }

Identify orphan psyche owner + list spt-core processes before clearing


<owl_messages>
<owl_message from="todlando" priority="high">ACK v0.12.1 sprint. Understood: v0.12.0 escaped because we gated on mocks + in-proc reconcile_once, not the real daemon+broker+PTY path. New discipline = every lifecycle gate vs REAL dummy-harness fixture + REAL detached daemon (legacy spt here = freely restartable). Checking out v0.12.1-lifecycle, reading V0.12.1-LIFECYCLE-JIT.md + minted REQs now. Starting Wave 1 (dummy-harness fixture) then Wave 2 keystone (VIEWER-CLOSE-DETACH breakaway+Job backstop). Ping you wave-by-wave SEAM-GREEN.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


psyche 130584 cmdline: "C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-psyche.exe" --id wall-a-psyche --session-id 9caa9805-f841-4a5c-9789-da602ec9f3ae --prompt "PSYCHE REVIVAL
time: epoch-ms:1781823469632
incoming event: (none)
"
=== other live spt-core procs (confirm before any clear) ===
76560    C:\Users\decid\AppData\Local\spt-core\bin\spt.exe daemon run
78204    "C:\Users\decid\AppData\Local\spt-core\bin\spt.exe" daemon brain --generation 0 --start-reason cold
130584   "C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-psyche.exe" --id wall-a-psyche --session-id 9caa9805-f841-4a5c-9789-da602ec9f3ae --prompt "PSYCHE REVIVAL
time: epoch-ms:1781823469632
incoming event: (none)
"
85632    "spt" ready wall-a-psyche --once


todlando ACK'd — on Wave 1 (dummy-harness fixture) → Wave 2 keystone, will ping wave-by-wave. Self-driving, no reply needed.

The clear is fully safe to proceed: every alive spt-core process is the dead test instance — daemon (76560), brain (78204), **orphan `wall-a-psyche` (130584)** + its `spt ready wall-a-psyche --once` (85632). No separate live agent. (Note: that orphaned wall-a psyche surviving its dead endpoint is REQ-HAZARD-UNHOST-PSYCHE-REAP failing in the real harness too — more evidence for the reopen.)

Purging the dead endpoints via the control plane (bounded, in case the wedge bites):


Bash

cd /c/Users/decid/AppData/Local/spt-core/bin
for id in wall-a wall-b wall-c; do echo "=== purge $id ==="; timeout 20 ./spt.exe endpoint purge "$id" --force --yes 2>&1 | tail -5 || echo "PURGE_TIMEOUT_OR_FAIL:$id"; done
echo "=== roster after purge ==="; timeout 15 ./spt.exe endpoint list --local 2>&1 | head

Purge dead wall-a/b/c endpoints (bounded), check roster


=== purge wall-a ===
--force: stopping 'wall-a' before purge ...
STOPPED:wall-a
ERROR: failed to remove the perch tree for 'wall-a': The process cannot access the file because it is being used by another process. (os error 32)
=== purge wall-b ===
--force: stopping 'wall-b' before purge ...
STOPPED:wall-b
PURGED:wall-b
=== purge wall-c ===
--force: stopping 'wall-c' before purge ...
STOPPED:wall-c
PURGED:wall-c
=== roster after purge ===
(no perches)
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


wall-b/c **PURGED**. wall-a stopped but its perch tree couldn't be removed (os error 32) — its orphan psyche (130584) + `spt ready` (85632) are still alive holding files (the unhost-reap bug again). Control-plane `STOP` worked cleanly on all three, confirming the wedge is data-plane only. Killing the wall-a orphans, then re-purging:


PowerShell

# Scope-kill ONLY the confirmed spt-core wall-a orphans by pid (verify path/cmdline first)
foreach ($p in 130584, 85632) { try { $c = Get-CimInstance Win32_Process -Filter "ProcessId=$p"; if ($c -and ($c.CommandLine -like '*wall-a-psyche*')) { Stop-Process -Id $p -Force -ErrorAction Stop; "killed $p ($($c.Name))" } else { "SKIP $p — not a wall-a orphan (cmdline: $($c.CommandLine))" } } catch { "pid $p already gone" } }

Scope-kill wall-a orphan psyche + ready (verified by cmdline)


killed 130584 (claude-spt-psyche.exe)
SKIP 85632 — not a wall-a orphan (cmdline: )


130584 killed; 85632 already exiting (empty cmdline). Re-purging wall-a + checking remaining spt-core processes:


Bash

cd /c/Users/decid/AppData/Local/spt-core/bin
echo "=== re-purge wall-a ==="; timeout 20 ./spt.exe endpoint purge wall-a --force --yes 2>&1 | tail -4 || echo "still failing"
echo "=== remaining spt-core processes ==="; powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='spt.exe' OR Name LIKE '%psyche%'\" | Where-Object { \$_.ExecutablePath -like '*spt-core*' } | ForEach-Object { '{0,-8} {1}' -f \$_.ProcessId, \$_.CommandLine }"
echo "=== owlery contents ==="; ls /c/Users/decid/AppData/Local/spt-core/owlery 2>/dev/null

Re-purge wall-a, list remaining spt-core procs + owlery


=== re-purge wall-a ===
--force: stopping 'wall-a' before purge ...
STOPPED:wall-a (no ready marker; address unregistered)
WARN: 'wall-a' did not fully quiesce within the stop window — purging anyway
PURGED:wall-a
=== remaining spt-core processes ===
76560    C:\Users\decid\AppData\Local\spt-core\bin\spt.exe daemon run
78204    "C:\Users\decid\AppData\Local\spt-core\bin\spt.exe" daemon brain --generation 0 --start-reason cold
=== owlery contents ===
doyle
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


wall-a purged; owlery now holds only the (offline, no-process) `doyle` perch record — left intact (yours). The only remaining spt-core processes are the wedged daemon (76560) + brain (78204). Killing them for a clean reset (safe — I'm on legacy spt; the daemon's data-plane is wedged anyway so graceful stop would likely hang):


PowerShell

# Scope-kill the wedged spt-core daemon + brain by pid (verified spt-core path). Daemon first (stops supervisor), then brain.
foreach ($p in 76560, 78204) { try { $c = Get-CimInstance Win32_Process -Filter "ProcessId=$p"; if ($c -and $c.ExecutablePath -like '*spt-core*') { Stop-Process -Id $p -Force -ErrorAction Stop; "killed $p ($($c.CommandLine))" } else { "SKIP $p (path: $($c.ExecutablePath))" } } catch { "pid $p already gone" } }
Start-Sleep -Milliseconds 600
"=== any spt-core processes left? ==="; $left = Get-CimInstance Win32_Process -Filter "Name='spt.exe' OR Name LIKE '%psyche%'" | Where-Object { $_.ExecutablePath -like '*spt-core*' }; if ($left) { $left | ForEach-Object { '{0,-8} {1}' -f $_.ProcessId, $_.CommandLine } } else { "CLEAN — no spt-core daemon/brain/psyche processes remain" }

Scope-kill wedged spt-core daemon + brain, verify clean


killed 76560 (C:\Users\decid\AppData\Local\spt-core\bin\spt.exe daemon run)
SKIP 78204 (path: )
=== any spt-core processes left? ===
194292   C:\Users\decid\AppData\Local\spt-core\bin\spt.exe daemon run
144668   "C:\Users\decid\AppData\Local\spt-core\bin\spt.exe" daemon brain --generation 0 --start-reason cold


Killing the wedged daemon triggered an **auto-respawn** — a fresh daemon (194292) + brain (144668) came up. That's actually the desired end state *if* it's clean (the wedge lived in the old process; a fresh daemon has no stuck workers + no phantom endpoints). Verifying the new daemon is clean + what respawned it:


Bash

cd /c/Users/decid/AppData/Local/spt-core/bin
echo "=== new daemon: roster (should be clean — no wall-*) ==="; timeout 15 ./spt.exe endpoint list --local 2>&1 | head
echo "=== owlery ==="; ls /c/Users/decid/AppData/Local/spt-core/owlery 2>/dev/null
echo "=== what spawned the new daemon (parent of 194292)? ==="; powershell -NoProfile -Command "\$d = Get-CimInstance Win32_Process -Filter 'ProcessId=194292'; \$par = Get-CimInstance Win32_Process -Filter \"ProcessId=\$(\$d.ParentProcessId)\"; 'daemon 194292 parent=' + \$d.ParentProcessId + ' (' + \$par.Name + ' : ' + \$par.CommandLine + ')'"

Verify fresh daemon clean + find respawner


=== new daemon: roster (should be clean — no wall-*) ===
(no perches)
=== owlery ===
doyle
=== what spawned the new daemon (parent of 194292)? ===
daemon 194292 parent=68664 ( : )
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Machine **cleared**:
- wall-a/b/c **purged**; orphan `wall-a-psyche` + its `spt ready` killed.
- Wedged daemon/brain killed → a **fresh clean daemon** auto-started (roster empty, no phantoms). The respawn was benign: my `endpoint list`/`purge` CLI calls hit `ensure_running`, which spawned a fresh daemon when it found none (parent was the now-exited CLI — not a mystery watchdog). Net result is exactly what we want: a clean, non-wedged daemon.
- The offline `doyle` perch record is left intact (yours).

That clear also produced a **concrete real-harness repro** worth banking — sending it to todlando: