docs\MANIFEST.md:73:Per harness event: the `api` command it fires, the stdin fields it maps in, and its **output capability** — `can_inject` (whether this hook can surface context to the agent). When `can_inject = false`, spt-core falls back to a sentinel + relay/poll path instead of expecting injection. This is the load-bearing harness-varying fact (CC's Stop hook can't inject).
docs\MANIFEST.md:231:- **stdout** (binary → spt-core, one per line): `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<payload>"}` · `{"key":"enter"}`, … (extensible vocabulary).
docs\MANIFEST.md:233:- The v0.11.0 raw `payload+\r` inject is the **degenerate** case (a binary that emits `{"text":payload}{"key":"enter"}` with no choreography).
docs\KNOWN-HAZARDS.md:443:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:478:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:480:- **Source:** F-019 (v0.14.2); root confirmed on real claude-spt (perri + doyle). The raw-inject FALLBACK itself (degenerate no-binary floor) is unchanged by F-019 — its removal (message stays SPOOLED + poll-fed, never raw PTY inject) rides a separate ADR-0022 amendment.
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:3:- Status: Accepted (design grilled 2026-06-19; supersedes the v0.11.0 hardcoded direct-PTY-inject). **Amended 2026-06-23 (v0.14.2) — raw-inject removed entirely; see "Amendment" below.**
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:18:2. **It fights the controller.** When an operator is attached via `spt rc`, the raw inject competes
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:46:  `{delay_ms:50}`, `{text:"<payload>"}`, `{key:"enter"}`, … (extensible).
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:55:The v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:56:`{text:payload}{key:enter}` with no choreography.
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:61:  `ctrl+s → delay 50 → text(payload) → enter → delay 50 → ctrl+s` on stdout. Perri builds it
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:116:## Amendment (2026-06-23, v0.14.2) — raw-inject removed entirely
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:128:  raw-injected.
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:131:  so it read as silent in practice. That silent degrade-to-raw-inject is precisely what masked F-019
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:132:  (below). Removing the raw-inject fallback makes a failed binary a visible no-delivery (spooled +
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:135:**Why:** the silent raw-inject fallback hid a real bug for a multi-hour hunt, and raw `payload+\r`
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:141:Independent of the raw-inject removal: the daemon bringup did **not** resolve the translation
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:145:the (now-removed) silent raw-inject fallback → CC never submitted. Fixed by routing every
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:149:first as the immediate CC-on-Windows idle-delivery unblock; this raw-inject removal rides its own


704:[Omitted long matching line]

704:[Omitted long matching line]


ADR-0022 amendment (shipped @1eaeef1) already documents the removal as the override; the ADR body stays historical (ADR convention). MANIFEST.md:233's "degenerate case" is about a *minimal binary* (text+enter choreography), not the removed no-binary fallback — defensible, leave it. Docs consistent.

Check the daemon lib result:


Bash

sleep 1; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output" 2>/dev/null | tail -12; echo "[exit check]"

Read daemon lib background output


[exit check]


While daemon lib runs, run the traceable-reqs gate (fast, tag-scan) — confirm my tags resolve:


Run traceability check


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


traceable printed all-OK rows. Confirm exit code + no failures:


traceable-reqs check > /tmp/trace.txt 2>&1; echo "EXIT=$?"; grep -iE "MISSING|FAIL|ERROR|invalid|untagged|\[X\]" /tmp/trace.txt | head -20; echo "--- REQ-MSG-IDLE + REQ-SEND-SPT-HOSTED rows ---"; grep -E "REQ-MSG-IDLE-TRANSLATION-BINARY|REQ-SEND-SPT-HOSTED" /tmp/trace.txt

Traceable exit code + key REQ rows


EXIT=0
       An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)
       A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)
       `spt adapter translate-proof <adapter> --event <envelope> [--session <id>]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:"init",endpoint_id,node}` line then the `{type:"event",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x)
       `spt adapter version <name>` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6)
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6)
       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. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1)
       A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `<id>-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0)
       The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
       A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker).
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect("re-serve")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME / OPERATOR-STREAM BOUNDARY (the LOAD-BEARING fix — kitsubito RACEDIAG ~33% repro the keystones missed) — `Brain::subscribe_with` (shared by attach/attach_as) resets the resume-mode dedup cursor to `from_seq`. The handoff eager subscribe makes serve_attach's brain receive the replay's seq K BEFORE the operator Request is processed (`attached`=false); that frame is dropped by the if-attached forward gate but the snap-above cursor already advanced past K, and `attach_as(sid,0)`'s re-subscribe used to leave the cursor advanced → the broker's re-send of seq K arrives below it, deduped, never forwarded → operator viewport forward-gap (silent content loss in the real rc consumer). Resetting to from_seq on the attach_as re-subscribe re-delivers from 0 (operator dedups the overlap) so seq K reaches the viewport. The epoch gate (2) is sound (RACEDIAG: zero socket interleaving above K); cold-start brains (empty map — production dispatch serve) keep the legacy next_seq path, so production is unaffected. (v0.13.0)
       B3: `spt daemon stop` then an immediate `spt daemon start` does NOT race — stop fully completes before it returns. Today request_stop (seedmap.rs:240-255) returns on the KIND_STOPPING ack (sent seedmap.rs:174-176) BEFORE the seed socket unbinds, so a following is_running ping (daemon.rs:375) wins the exit window and start reports ALREADY_RUNNING (operator: daemon stop → STOPPED then start → ALREADY_RUNNING). FIX: unbind/stop-gate the seed socket BEFORE acking KIND_STOPPING, OR request_stop waits for a ping-to-fail before returning. Unit: stop then immediate is_running()==false. (v0.12.0)
       A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0)
       The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)
       A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)
       `spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)
       SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)
       B5: `spt daemon start` does NOT revive phantom Psyches for dead-but-online-latched perches. Today reconcile_once (livehost.rs:285) spawns a Psyche per status=online live_agent perch at boot WITHOUT verifying the harness child / {id}-psyche is actually alive — so a Cold start after an unclean stop revives N psyches for N dead-but-latched perches (3 psyches for 3 dead perches). FIX: gate the boot psyche-spawn on real child-liveness — a perch with NO live broker session (the B2 reconcile signal) is marked OFFLINE at boot instead of hosted, so a dead-harness perch is never revived. Shares the B2 reconcile loop (this is its boot-gate arm); composes with B2's honest latch. Also closes wall-a's psyche_host_error gap (residency-confirm does not run at boot tick-1, livehost.rs:395-441 / 257-263). (v0.12.0)
--- REQ-MSG-IDLE + REQ-SEND-SPT-HOSTED rows ---
       `spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)
  [OK] REQ-MSG-IDLE-TRANSLATION-BINARY  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-SEND-SPT-HOSTED  required: [impl, unit, int]  stages: -doc +impl +unit +int


traceable **EXIT=0**, both REQ-MSG-IDLE-TRANSLATION-BINARY and REQ-SEND-SPT-HOSTED `[OK]` with my new tags resolving. Check the daemon lib result:


cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output" 2>/dev/null | tail -8; echo "[done-check]"; ls -la "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output" 2>/dev/null

Check daemon lib bg output + size


[done-check]
-rw-r--r-- 1 decid 197609 0 Jun 23 16:36 C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output


Daemon lib still running (0 bytes, tail buffers until exit). While it finishes, review the full diff stat:


git -C "C:\Users\decid\Documents\projects\spt-core" diff --stat 2>&1 | tail -20

Diff stat of all changes


warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/bin/xlate_choreo_fixture.rs |  12 +--
 crates/spt-daemon/src/broker.rs                   | 112 ++++++++++++----------
 crates/spt-daemon/src/harnesshost.rs              |   6 +-
 crates/spt-daemon/src/msg.rs                      |   5 +-
 crates/spt-daemon/src/translation.rs              |   7 +-
 crates/spt-daemon/tests/broker.rs                 |  45 +++++----
 crates/spt-daemon/tests/inject_control_wedge.rs   |  34 ++++---
 crates/spt-runtime/manifest.schema.json           |   2 +-
 crates/spt-runtime/src/manifest.rs                |   4 +-
 9 files changed, 126 insertions(+), 101 deletions(-)


Diff is proportionate (broker.rs the bulk). While the lib test runs, retry doyle for the missing TESTS/REQ tail + report progress:


printf '%s' "v0.14.3 raw-inject-removal BUILT on branch fix/v0143-raw-inject-removal (off main @365fb0c). Done: spine flip in dispatch_endpoint_input (no working binary -> endpoint_injected_envelope FALSE -> caller spools; input writer no longer resolved); build_translation/fault_translation/read_translation log+doc rewords; full raw-inject->spool comment sweep across broker.rs (10 sites) + translation.rs + harnesshost.rs + msg.rs + manifest.rs; manifest.schema.json re-blessed. CHANGE 5 = NO-OP (try_broker_inject already returns inject_endpoint bool; delivered=false already falls through to deliver::send spool -> honest QUEUED). Tests flipped: broker.rs endpoint test (no-binary -> delivered=false + nothing on PTY, renamed endpoint_keyed_inject_without_binary_spools_not_pty) + inject_control_wedge g2 (post-fault: delivered2=false + marker NOT on PTY, was raw_fallback). GREEN so far: g1-g4+p0 int (9/9), broker endpoint test, runtime resolve 3/3, schema drift, traceable EXIT0 (REQ-MSG-IDLE-TRANSLATION-BINARY + REQ-SEND-SPT-HOSTED both +impl+unit+int). RUNNING: daemon --lib + clippy --workspace next. BLOCKER: your assignment msg truncated at part 8 (mid-TESTS). Resend parts 8-17 — did you mint a NEW REQ for this, or extend REQ-MSG-IDLE-TRANSLATION-BINARY? I tagged the latter. And any specific test/gate contract I should match." | spt send doyle 2>&1

Report progress + re-request truncated tail to doyle


Error: Exit code 1
NO_PERCH:doyle is not listening


doyle still offline. Proceeding — doyle gates async off the branch. Check daemon lib:


cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output" 2>/dev/null | tail -10; echo "[size:]"; wc -c < "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b2mj0f7yc.output" 2>/dev/null

Poll daemon lib output


[size:]
0


0 bytes for minutes — likely a lib unit spawning a broker that's hanging ([[delegate-build-preflight-hang]] pattern). Stop it and run targeted. Load TaskStop:


ToolSearch

Question: select:TaskStop,TaskList


Question: select:TaskStop,TaskList


Successfully stopped task: b2mj0f7yc (cargo test -p spt-daemon --lib 2>&1 | tail -12)


Stopped. List the lib unit tests to target (avoid a hanging one):


Bash

cargo test -p spt-daemon --lib -- --list 2>&1 | tail -30

List daemon lib unit tests


shellwake::tests::resolve_wake_revives_a_suspended_owner_without_double_launch: test
shellwake::tests::watcher_crash_exits_respawn_then_give_up: test
shellwake::tests::watcher_opcode_exit_fires_resolution_once: test
sync::tests::gate_decision_table: test
sync::tests::reconcile_driver_structurally_excludes_live_role: test
sync::tests::select_refs_scopes_two_tiers: test
translation::tests::inject_floor_buffers_while_held_then_drains_in_order: test
translation::tests::key_cmd_parses_each_wire_shape: test
translation::tests::key_to_bytes_send_keys_map: test
translation::tests::to_binary_serializes_to_wire_and_input_is_content_free: test
transport::tests::local_socket_handshake_and_frame_round_trip: test
transport::tests::local_socket_rejects_wrong_role: test
tunnelhub::tests::clear_evicts_the_entry: test
tunnelhub::tests::dead_generation_stream_is_never_surfaced: test
tunnelhub::tests::entries_are_per_owner: test
tunnelhub::tests::relink_overwrites_the_entry_with_a_fresh_generation: test
tunnelhub::tests::resolve_serves_each_end_under_the_opening_token: test
update::tests::apply_refuses_non_brain_only_plan: test
update::tests::classify_brain_only_when_broker_hosts_new_brain: test
update::tests::classify_broker_breaking_on_resource_abi_change: test
update::tests::classify_broker_compatible_when_brain_below_floor: test
update::tests::plan_verified_produces_no_plan_for_a_rejected_release: test
update::tests::plan_verified_update_set_uses_selected_platform_spec: test
update::tests::plan_verified_yields_a_plan_for_a_valid_release: test
wan::tests::origin_user_backed_matrix: test
wan::tests::wan_user_msg_restamped_unless_origin_user_backed: test
xfer::tests::recv_state_chunks_idempotent_and_commit_replays_safely: test
xfer::tests::resolve_under_confines_to_root: test

377 tests, 0 benchmarks


377 lib tests — the `tail` just buffers until exit (not hung, slow). Run the relevant modules targeted (broker + translation + harnesshost units) which my change could touch:


cargo test -p spt-daemon --lib translation:: 2>&1 | tail -12; echo "=== broker read_translation + endpoint ==="; cargo test -p spt-daemon --lib read_translation 2>&1 | tail -10

Run translation + read_translation lib units


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-ed074c08fe01e245.exe)

running 4 tests
test translation::tests::inject_floor_buffers_while_held_then_drains_in_order ... ok
test translation::tests::key_to_bytes_send_keys_map ... ok
test translation::tests::key_cmd_parses_each_wire_shape ... ok
test translation::tests::to_binary_serializes_to_wire_and_input_is_content_free ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 373 filtered out; finished in 0.00s

=== broker read_translation + endpoint ===
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-ed074c08fe01e245.exe)

running 2 tests
test broker::tests::read_translation_path_reads_declared_path ... ok
test broker::tests::read_translation_path_none_when_absent ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 375 filtered out; finished in 0.01s


translation + read_translation units green. Run the F-019 harnesshost unit, then kick the full lib suite to background (streaming, no tail-buffer):


cargo test -p spt-daemon --lib translation_binary_resolves_against_install_dir 2>&1 | tail -8

Run F-019 harnesshost resolve unit


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-ed074c08fe01e245.exe)

running 1 test
test harnesshost::tests::translation_binary_resolves_against_install_dir ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 376 filtered out; finished in 0.63s


cargo test -p spt-daemon --lib > /tmp/daemonlib.txt 2>&1; echo "LIB_EXIT=$?"; tail -4 /tmp/daemonlib.txt

Run full daemon lib suite to file


Command running in background with ID: b5advywec. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b5advywec.output. You will be notified when it completes. To check interim output, use Read on that file path.


Full lib suite backgrounded. While it runs, verify the `injecting_a_large_payload_into_a_stdin_ignoring_child` test (W1 PARK-B diagnostic) — it passed but its premise was raw-inject parking write_input; now it spools. Check for stale assertions/comments:


3-//! IPC (no mocks of the PTY or the transport).
4-//!
5://! ## The bug this gate locks down (park (a), the output-stall / wedge root)
6-//!
7-\! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the
--
11-//! `sync_channel` and can never stall the drain. So a slow / non-draining
12-//! controller (here a controller connection that stops reading while the child
13://! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output
14-//! stalled and any path needing that log lock (a fresh attach, or the global
15-//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)
--
136-}
137-
138:/// Join a teardown helper thread, but NEVER ride a permanently-parked helper to
139-/// the nextest slow-timeout backstop (240s). On the FAIL/wedge path a helper can
140:/// be parked forever inside a blocking socket op — e.g. the driver's `send_effect`
141-/// on a wedged broker never returns to observe `stop` — so a bare `.join()` would
142-/// hang the whole test until the 240s SIGKILL, MASKING which failure it was. The
--
171-
172-/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output
173:/// case (a real TUI redraw burst). Cross-OS, same shape as the broker B1 test's
174-/// `cat`/`findstr` echo child.
175-fn flood_spawn_req(endpoint: &str) -> SpawnReq {
--
310-    // ── A now STOPS reading. The child floods → A's socket recv buffer fills →
311-    //    the controller writer thread blocks on it → the controller channel fills
312:    //    → the drain's bounded send_timeout starts blocking. PRE-FIX this parked
313-    //    the drain inside append HOLDING the log lock (permanent wedge). POST-FIX
314-    //    the send is OFF the log lock, so the session must stay responsive even
--
320-    // log lock (proves the process is alive). KIND_SESSIONS enumerates every
321-    // session's log (`delivered_through`) under each log lock — PRE-FIX it hung on
322:    // the parked session's held lock; POST-FIX it answers because the drain no
323-    // longer holds the lock during the controller send.
324-    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
--
348-         answer because the controller write is delivered OFF the log lock on a \
349-         dedicated writer thread (W1 fix). Pre-fix this hung — the inline \
350:         controller write parked the drain holding the log lock."
351-    );
352-}
353-
354-#[test]
355:fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
356:    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
357-    \ PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
358:    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
359:    // small kernel input queue is expected to park (real there).
360-    let name = unique_name();
361-    let broker = Broker::bind(&name).expect("bind broker");
--
366-
367-    // Controller A spawns the flood child (which IGNORES stdin) and then DRAINS
368:    // its frames forever on a background thread — so the output-side park (a)
369:    // does NOT fire and we isolate the input-side park (b).
370-    let mut a = None;
371-    for _ in 0..200 {
--

[Showing results with pagination = limit: 60]


This test (`injecting_a_large_payload_into_a_stdin_ignoring_child`) is now a **vacuous park-(b) probe** — the endpoint inject spools instead of writing to write_input, so it no longer parks. Read its body/assertions to reframe honestly:


354	#[test]
355	fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
356	    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
357	    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
358	    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
359	    // small kernel input queue is expected to park (real there).
360	    let name = unique_name();
361	    let broker = Broker::bind(&name).expect("bind broker");
362	    let serve_broker = Arc::clone(&broker);
363	    thread::spawn(move || {
364	        let _ = serve_broker.serve();
365	    });
366
367	    // Controller A spawns the flood child (which IGNORES stdin) and then DRAINS
368	    // its frames forever on a background thread — so the output-side park (a)
369	    // does NOT fire and we isolate the input-side park (b).
370	    let mut a = None;
371	    for _ in 0..200 {
372	        match LocalSocketTransport::connect(&name) {
373	            Ok(c) => {
374	                a = Some(c);
375	                break;
376	            }
377	            Err(_) => thread::sleep(Duration::from_millis(10)),
378	        }
379	    }
380	    let mut a = a.expect("connect controller A");
381	    send_hello(&mut a, Role::Brain).expect("hello A");
382	    send(
383	        &mut a,
384	        KIND_SPAWN,
385	        serde_json::to_value(flood_spawn_req("inj-ep")).unwrap(),
386	    );
387	    let sid = loop {
388	        let f = read_frame(&mut a).expect("frame before spawned");
389	        if f.kind == KIND_SPAWNED {
390	            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
391	        }
392	    };
393	    // Keep the controller socket drained so append never parks.
394	    thread::spawn(move || while read_frame(&mut a).is_ok() {});
395
396	    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
397	    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
398	    // master; the child never reads stdin, so the input buffer fills and the
399	    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
400	    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
401	    let name_inj = name.clone();
402	    let (tx, rx) = std::sync::mpsc::channel::<bool>();
403	    thread::spawn(move || {
404	        let mut b = match LocalSocketTransport::connect(&name_inj) {
405	            Ok(c) => c,
406	            Err(_) => {
407	                let _ = tx.send(false);
408	                return;
409	            }
410	        };
411	        if send_hello(&mut b, Role::Brain).is_err() {
412	            let _ = tx.send(false);
413	            return;
414	        }
415	        send(
416	            &mut b,
417	            KIND_ENDPOINT_INPUT,
418	            serde_json::to_value(EndpointInputReq {
419	                endpoint: "inj-ep".to_string(),
420	                data_b64: encode_bytes(&big),
421	            })
422	            .unwrap(),
423	        );
424	        loop {
425	            match read_frame(&mut b) {
426	                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
427	                    let _ = tx.send(true);
428	                    return;

#[test]
fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
    // small kernel input queue is expected to park (real there).
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");
    let serve_broker = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve_broker.serve();
    });

    // Controller A spawns the flood child (which IGNORES stdin) and then DRAINS
    // its frames forever on a background thread — so the output-side park (a)
    // does NOT fire and we isolate the input-side park (b).
    let mut a = None;
    for _ in 0..200 {
        match LocalSocketTransport::connect(&name) {
            Ok(c) => {
                a = Some(c);
                break;
            }
            Err(_) => thread::sleep(Duration::from_millis(10)),
        }
    }
    let mut a = a.expect("connect controller A");
    send_hello(&mut a, Role::Brain).expect("hello A");
    send(
        &mut a,
        KIND_SPAWN,
        serde_json::to_value(flood_spawn_req("inj-ep")).unwrap(),
    );
    let sid = loop {
        let f = read_frame(&mut a).expect("frame before spawned");
        if f.kind == KIND_SPAWNED {
            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
        }
    };
    // Keep the controller socket drained so append never parks.
    thread::spawn(move || while read_frame(&mut a).is_ok() {});

    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
    // master; the child never reads stdin, so the input buffer fills and the
    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let _ = tx.send(true);
                    return;


Confirmed dead premise: 4 MiB endpoint-inject expecting `write_input` park, but no-binary now spools (never reaches write_input). Read the assertions tail:


428	                    return;
429	                }
430	                Ok(_) => continue,
431	                Err(_) => {
432	                    let _ = tx.send(false);
433	                    return;
434	                }
435	            }
436	        }
437	    });
438	    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);
439
440	    // Broker process still alive on a fresh connection (the park is the dispatch
441	    // thread serving the inject, not the whole process).
442	    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
443
444	    eprintln!(
445	        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
446	         (expect inject_acked=false = write_input parked on write_all) ==="
447	    );
448
449	    if let Some(pid) = broker.session_pid(sid) {
450	        kill_pid(pid);
451	    }
452
453	    assert!(broker_alive, "broker process must stay alive (per-session park)");
454
455	    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
456	    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
457	    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
458	    //   the operator's platform the input-side write is BENIGN; the operator wedge
459	    //   is park (a) (controller output delivery), reached on injection via the
460	    //   injection-INDUCED output backing up the controller — not the inject write.
461	    // - Unix forkpty has a small kernel input queue, so the same inject is
462	    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
463	    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
464	    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
465	    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
466	    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
467	    //   (and bounds the park) once CI actually shows it. The only hard invariant
468	    //   here is broker liveness (asserted above for both platforms).
469	    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
470	    #[cfg(windows)]
471	    assert!(
472	        inject_acked,
473	        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
474	         if this flips, the input buffer began blocking — re-examine park (b)."
475	    );
476	    #[cfg(unix)]
477	    eprintln!(
478	        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
479	         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
480	    );
481	}
482

                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);

    // Broker process still alive on a fresh connection (the park is the dispatch
    // thread serving the inject, not the whole process).
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
         (expect inject_acked=false = write_input parked on write_all) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    assert!(broker_alive, "broker process must stay alive (per-session park)");

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}


This stale-but-green test misleads (claims park-b/"ConPTY absorbs" but now tests spool). Honest fix: repurpose it to assert the new spool contract, capturing `delivered`. Rewrite the inject thread to capture the bool (untyped, matching the file's `deliver_endpoint_event` style):


Edit

#[test]
fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
    // small kernel input queue is expected to park (real there).
    let name = unique_name();

#[test]
fn large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging() {
    // ADR-0022 amendment: idle delivery is translation-binary-ONLY. This session
    // declares NO translation binary, so a `KIND_ENDPOINT_INPUT` — even a 4 MiB one
    // — SPOOLS: the broker acks delivered=FALSE PROMPTLY and never touches
    // `write_input`, so the dispatch thread cannot park on a full PTY input buffer.
    // (Was the v0.13.0 park-(b) probe via raw inject; raw inject was removed, so the
    // endpoint-inject path no longer reaches write_input at all. The OPERATOR-input
    // park-(b) bound now lives in `p0_paste_wedge_parked_write_does_not_starve_attach
    // _or_wedge_broker`, which drives write_input through dispatch_input.)
    let name = unique_name();


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

#[test]
fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
    // small kernel input queue is expected to park (real there).
    let name = unique_name();

#[test]
fn large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging() {
    // ADR-0022 amendment: idle delivery is translation-binary-ONLY. This session
    // declares NO translation binary, so a `KIND_ENDPOINT_INPUT` — even a 4 MiB one
    // — SPOOLS: the broker acks delivered=FALSE PROMPTLY and never touches
    // `write_input`, so the dispatch thread cannot park on a full PTY input buffer.
    // (Was the v0.13.0 park-(b) probe via raw inject; raw inject was removed, so the
    // endpoint-inject path no longer reaches write_input at all. The OPERATOR-input
    // park-(b) bound now lives in `p0_paste_wedge_parked_write_does_not_starve_attach
    // _or_wedge_broker`, which drives write_input through dispatch_input.)
    let name = unique_name();

-    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.

-    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY

-    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's

-    // small kernel input queue is expected to park (real there).

+    // ADR-0022 amendment: idle delivery is translation-binary-ONLY. This session

+    // declares NO translation binary, so a `KIND_ENDPOINT_INPUT` — even a 4 MiB one

+    // — SPOOLS: the broker acks delivered=FALSE PROMPTLY and never touches

+    // `write_input`, so the dispatch thread cannot park on a full PTY input buffer.

+    // (Was the v0.13.0 park-(b) probe via raw inject; raw inject was removed, so the

+    // endpoint-inject path no longer reaches write_input at all. The OPERATOR-input

+    // park-(b) bound now lives in `p0_paste_wedge_parked_write_does_not_starve_attach

+    // _or_wedge_broker`, which drives write_input through dispatch_input.)

let broker = Broker::bind(&name).expect("bind broker");


    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
    // master; the child never reads stdin, so the input buffer fills and the
    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let _ = tx.send(true);
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);

    // Broker process still alive on a fresh connection (the park is the dispatch
    // thread serving the inject, not the whole process).
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
         (expect inject_acked=false = write_input parked on write_all) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    assert!(broker_alive, "broker process must stay alive (per-session park)");

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}

    // Inject a LARGE payload into the no-binary session via a SECOND brain
    // connection. dispatch_endpoint_input finds the session but NO translation
    // binary → it SPOOLS: replies KIND_ENDPOINT_INJECTED{delivered:false} at once
    // and writes NOTHING to the PTY input (the 4 MiB never reaches write_input, so
    // it cannot park the dispatch thread — even with a stdin-ignoring child).
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    // Some(delivered) once the inject is acked; None on a connection failure.
    let (tx, rx) = std::sync::mpsc::channel::<Option<bool>>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(None);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(None);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let delivered = f
                        .payload
                        .get("delivered")
                        .and_then(|d| d.as_bool())
                        .unwrap_or(true);
                    let _ = tx.send(Some(delivered));
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(None);
                    return;
                }
            }
        }
    });
    // PROMPT: a spool ack returns at once (no PTY write to park on). An 8s ceiling
    // would catch a regression that re-introduced a parking write.
    let ack = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(None);
    let inject_acked = ack.is_some();
    let spooled = ack == Some(false);

    // Broker process still alive on a fresh connection.
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== NO-BINARY SPOOL GATE: inject_acked={inject_acked} spooled={spooled} \
         broker_alive={broker_alive} \
         (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, \
         never parked the dispatch thread) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    // [int->REQ-MSG-IDLE-TRANSLATION-BINARY]
    assert!(broker_alive, "broker process must stay alive");
    assert!(
        inject_acked,
        "a no-binary endpoint inject must be acked PROMPTLY (it spools, never parks on a PTY write)"
    );
    assert!(
        spooled,
        "a hosted session with no translation binary must SPOOL the inject \
         (delivered=false), not write it to the PTY — idle delivery is \
         translation-binary-only (ADR-0022 amendment)"
    );
}


    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
    // master; the child never reads stdin, so the input buffer fills and the
    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let _ = tx.send(true);
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);

    // Broker process still alive on a fresh connection (the park is the dispatch
    // thread serving the inject, not the whole process).
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
         (expect inject_acked=false = write_input parked on write_all) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    assert!(broker_alive, "broker process must stay alive (per-session park)");

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}

    // Inject a LARGE payload into the no-binary session via a SECOND brain
    // connection. dispatch_endpoint_input finds the session but NO translation
    // binary → it SPOOLS: replies KIND_ENDPOINT_INJECTED{delivered:false} at once
    // and writes NOTHING to the PTY input (the 4 MiB never reaches write_input, so
    // it cannot park the dispatch thread — even with a stdin-ignoring child).
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    // Some(delivered) once the inject is acked; None on a connection failure.
    let (tx, rx) = std::sync::mpsc::channel::<Option<bool>>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(None);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(None);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let delivered = f
                        .payload
                        .get("delivered")
                        .and_then(|d| d.as_bool())
                        .unwrap_or(true);
                    let _ = tx.send(Some(delivered));
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(None);
                    return;
                }
            }
        }
    });
    // PROMPT: a spool ack returns at once (no PTY write to park on). An 8s ceiling
    // would catch a regression that re-introduced a parking write.
    let ack = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(None);
    let inject_acked = ack.is_some();
    let spooled = ack == Some(false);

    // Broker process still alive on a fresh connection.
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== NO-BINARY SPOOL GATE: inject_acked={inject_acked} spooled={spooled} \
         broker_alive={broker_alive} \
         (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, \
         never parked the dispatch thread) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    // [int->REQ-MSG-IDLE-TRANSLATION-BINARY]
    assert!(broker_alive, "broker process must stay alive");
    assert!(
        inject_acked,
        "a no-binary endpoint inject must be acked PROMPTLY (it spools, never parks on a PTY write)"
    );
    assert!(
        spooled,
        "a hosted session with no translation binary must SPOOL the inject \
         (delivered=false), not write it to the PTY — idle delivery is \
         translation-binary-only (ADR-0022 amendment)"
    );
}

// Keep the controller socket drained so append never parks.

thread::spawn(move || while read_frame(&mut a).is_ok() {});

-    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain

-    // connection. dispatch_endpoint_input → write_input → write_all on the PTY

-    // master; the child never reads stdin, so the input buffer fills and the

-    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.

+    // Inject a LARGE payload into the no-binary session via a SECOND brain

+    // connection. dispatch_endpoint_input finds the session but NO translation

+    // binary → it SPOOLS: replies KIND_ENDPOINT_INJECTED{delivered:false} at once

+    // and writes NOTHING to the PTY input (the 4 MiB never reaches write_input, so

+    // it cannot park the dispatch thread — even with a stdin-ignoring child).

-    let (tx, rx) = std::sync::mpsc::channel::<bool>();

+    // Some(delivered) once the inject is acked; None on a connection failure.

+    let (tx, rx) = std::sync::mpsc::channel::<Option<bool>>();

-                let _ = tx.send(false);

-                    let _ = tx.send(true);

+                        .get("delivered")

+                        .and_then(|d| d.as_bool())

+                        .unwrap_or(true);

+                    let _ = tx.send(Some(delivered));

-                    let _ = tx.send(false);

+                    let _ = tx.send(None);

-    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);

+    // PROMPT: a spool ack returns at once (no PTY write to park on). An 8s ceiling

+    // would catch a regression that re-introduced a parking write.

+    let ack = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(None);

-    // Broker process still alive on a fresh connection (the park is the dispatch

-    // thread serving the inject, not the whole process).

+    // Broker process still alive on a fresh connection.

-        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \

-         (expect inject_acked=false = write_input parked on write_all) ==="

+        "=== NO-BINARY SPOOL GATE: inject_acked={inject_acked} spooled={spooled} \

+         (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, \

+         never parked the dispatch thread) ==="

-    assert!(broker_alive, "broker process must stay alive (per-session park)");

-    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):

-    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without

-    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on

-    //   the operator's platform the input-side write is BENIGN; the operator wedge

-    //   is park (a) (controller output delivery), reached on injection via the

-    //   injection-INDUCED output backing up the controller — not the inject write.

-    // - Unix forkpty has a small kernel input queue, so the same inject is

-    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED

-    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we

-    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =

-    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux

-    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert

-    //   (and bounds the park) once CI actually shows it. The only hard invariant

-    //   here is broker liveness (asserted above for both platforms).

-    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]

+    // [int->REQ-MSG-IDLE-TRANSLATION-BINARY]

+    assert!(broker_alive, "broker process must stay alive");

-        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \

-         if this flips, the input buffer began blocking — re-examine park (b)."

+        "a no-binary endpoint inject must be acked PROMPTLY (it spools, never parks on a PTY write)"

-        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \

-         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="

+        "a hosted session with no translation binary must SPOOL the inject \

+         (delivered=false), not write it to the PTY — idle delivery is \

+         translation-binary-only (ADR-0022 amendment)"


Repurposed to a hard spool assertion on both platforms; retagged `[int->REQ-MSG-IDLE-TRANSLATION-BINARY]`. Need to confirm I didn't drop the sole tag for REQ-HAZARD-INJECT-CONTROL-COEXIST. First check the full lib suite + compile/run the repurposed test:


Bash

cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b5advywec.output" 2>/dev/null | tail -6; echo "[libsize:]"; wc -c < "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b5advywec.output" 2>/dev/null

Check full lib suite result


[libsize:]
0


Lib suite still running (377 tests, slow). Verify the retag didn't orphan REQ-HAZARD-INJECT-CONTROL-COEXIST + compile the repurposed test:


crates\spt-daemon\tests\inject_control_wedge.rs:343:    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]


One COEXIST int tag remains (line 343, the park-(a) output-stall gate) — sufficient. Compile + run the repurposed test:


cargo test -p spt-daemon --test inject_control_wedge large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging -- --nocapture 2>&1 | tail -15

Run repurposed spool test


   Compiling spt-daemon v0.14.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.72s
     Running tests\inject_control_wedge.rs (target\debug\deps\inject_control_wedge-71a4df7bfa1b0c15.exe)

running 1 test
ENDPOINT_INJECT:inj-ep: no working translation binary (absent/faulted/worker-gone) -> SPOOLED, not injected
=== NO-BINARY SPOOL GATE: inject_acked=true spooled=true broker_alive=true (acked promptly + delivered=false → the 4 MiB spooled, never reached write_input, never parked the dispatch thread) ===
test large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.46s


Repurposed test green (spooled=true, prompt). Re-run traceable to confirm the retag didn't break coverage:


traceable-reqs check > /tmp/trace2.txt 2>&1; echo "EXIT=$?"; grep -iE "MISSING|FAIL|ERROR|invalid|untagged" /tmp/trace2.txt | head; grep -E "REQ-HAZARD-INJECT-CONTROL-COEXIST|REQ-MSG-IDLE-TRANSLATION-BINARY|REQ-SEND-SPT-HOSTED" /tmp/trace2.txt | grep -E "\[OK\]|\[X\]"

Re-verify traceability after retag


EXIT=0
       An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)
       A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)
       `spt adapter translate-proof <adapter> --event <envelope> [--session <id>]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:"init",endpoint_id,node}` line then the `{type:"event",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x)
       `spt adapter version <name>` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6)
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6)
       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. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1)
       A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `<id>-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0)
       The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
  [OK] REQ-HAZARD-INJECT-CONTROL-COEXIST  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-MSG-IDLE-TRANSLATION-BINARY  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-SEND-SPT-HOSTED  required: [impl, unit, int]  stages: -doc +impl +unit +int


All three REQs `[OK]`, traceable EXIT=0. Check the lib suite, then clippy:


wc -c < "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b5advywec.output" 2>/dev/null; echo "--- if >0, tail: ---"; tail -5 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\22d5a4f6-16ae-4552-82b1-0f69cfb556b0\tasks\b5advywec.output" 2>/dev/null; echo "--- /tmp/daemonlib.txt tail: ---"; tail -5 /tmp/daemonlib.txt 2>/dev/null

Check lib suite progress via target file