# LIFECYCLE-TRUTH — milestone triage + JIT plan (operator-called 2026-07-07)

Field-hardening milestone off the 2026-07-06/07 incident night: the v0.27→v0.28 update wedge,
the mobile-gw orphan, the claude-spt 0.15.x delivery outage collateral, and the flynn wake
double-spawn. Every root below is PINNED (rig-reproduced or line-level) — evidence ledger in
doyle's findings backlog (seeds #5–#10) and this doc's per-wave notes. Owner: doyle (gate),
executor: todlando. Waves ordered by field pressure × dependency; **mint ALL REQs
`required_stages=[]` first, activate per wave as started** (traceable rule 5).

Gate discipline (every wave): isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR,
nextest (never bare cargo test on Win), `cargo clippy --workspace` preflight, behavior-change →
grep tests for OLD behavior as assertions, shared-seam → run ALL seam tests, gate suites
FOREGROUND-STAGED on hfenduleam (bg-task killer recurrence). No [twohost] tag — the earmarked
REQ-JOIN-DEFERRED-ELEVATION int does NOT activate this milestone.

---

## W1 — psyche echo unblock (smallest, live pressure: doyle + perri psyches stamped NOW)

**REQ-ECHO-DROP-DIR-RESOLVE** (impl, unit)
Root: `fire_echo` (spt-daemon lifecycle.rs:790) passes the RAW manifest `commune_dir` into
`run_echo_commune` → echo.rs:115-117 `create_dir_all`+join; relative `.claude` under the
WMI-launched daemon's System32 cwd → `os error 5` deterministic. Siblings already resolve
correctly (ingest ~:583, `psyche_drop_file` :1072 via `resolve_endpoint_drop_dir(raw, cwd)`).
Fix: fire_echo routes through the SAME resolver; relative-with-no-cwd = SKIP LOUD (stderr),
never a raw relative write. Latent-worse variant to kill with it: writable daemon cwd would
write the drop to a WRONG dir silently (echo communes lost, no error).
Unit: relative dir + cwd → resolved; absolute → as-is; relative no-cwd → loud skip.
Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write;
echo claude spawn gets explicit cwd = endpoint cwd (perri ask).

**REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS** (impl, unit)
Root: `psyche_host_error` clears only via the pulse-loop `note_turn_outcome` Ok leg
(lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it → stale FAILED
stamp over a healthy psyche (three field confirmations, perri). Fix: every successful psyche
operation (event turn, checkpoint/wake synthesis, signoff echo) clears the stamp.
Unit: checkpoint-success path clears a pre-existing stamp.

**REQ-PSYCHE-ROLE-OPTIONAL-SKIP** (impl, unit)
Root (perri filing, recovered): manifest with NO `[session.echo_commune]` role → commune-sync
hard-fails → 3-strike stamps the host ("manifest declares no [session.echo_commune] role").
Published contract presents the role as optional template. Fix: missing OPTIONAL role = SKIP
(no strike, debug-level note), not a turn failure.
Unit: role-less manifest ticks strike-free.

Field acceptance (doyle, gate-time): after fix lands on this node, next commune clears BOTH
live stamps (doyle + perri) with zero manual intervention.

## W2 — broker viewer/brain decouple (flagship — the update wedge)

**REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE** (doc, impl, unit, int)
Root, rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): PTY viewer fan-out
and control mutations depend synchronously on a live, DRAINING brain — brain-subscriber
session-output writes ride UNDER the per-session log lock (broker.rs:19-20). Failed writes are
handled (cursor freeze + detach, :3486); BLOCKED writes are not. Suspended brain ⇒ within
seconds: attached rc output freezes; detach does NOT release the control stamp (release routes
through the brain); reattach REFUSED (controlled-by); `rc --take` hangs; `daemon status` stays
healthy. Field: every brain cycle (incl. every `update apply`) has a freeze window; a stalled
or slow-draining new brain (6 sessions + psyches + WAN on the incident night) = permanent
wedge until bounce. `brain.ready` ≠ subscribers drained — why the 22:47 apply "promoted"
while frozen.
Fix shape (todlando proposes, doyle rules before impl): subscriber writes move OFF the log
lock (bounded/nonblocking, stall ⇒ detach-subscriber like viewer eviction — the broker already
buffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control
stamp release/take must complete against the BROKER without brain round-trip (or bounded with
loud timeout). doc = KNOWN-HAZARDS entry.
Int (the rig, encoded): suspend the brain child mid-session → attached viewer ticks CONTINUE +
`rc --take` completes; resume → no output lost (cursor replay).

## W3 — update finish truth (the "seamless live update" cornerstone)

**REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL** (impl, unit, int)
Rig-proven: `daemon stop`+`start` (the apply notice's OWN instruction) kills every hosted
endpoint; they stay OFFLINE after start (no resurrection). Fix: daemon start RE-RUNS
previously-online spt-hosted endpoints (records exist: info.json status + adapter + cwd);
alternatively stop performs a drain/handoff. Scope ruling (doyle): re-run-on-start, marked
`start-reason=daemon-restart`; agents' minds ride psyche re-host as today.
Int: endpoint online → daemon stop → start → endpoint back ONLINE, same id, harness respawned.

**REQ-UPDATE-ONE-SHOT-FINISH** (impl, unit)
Operator wart: `update fetch`/`update apply` run `ensure_daemon_announced` (cli.rs:4386) →
on a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain
pair + a manual bounce. Fix: apply works daemonless (swap + record, next start runs new bytes);
`update apply --finish` (name subject to docs-token gate) completes the cycle: swap → brain
cycle → broker restart onto new bytes (riding W3's endpoint re-run so the restart is no longer
a massacre). CLI change → xtask docs gen, no internal codes in clap ///.

**REQ-DAEMON-STOP-LIVE-SESSION-WARN** (impl, unit) — promoted old follow-wave seed: stop with
live hosted sessions warns + requires --force (or names the sessions it will kill).

**REQ-RC-RECONNECT-TRUTH** (impl, unit)
Rig-proven roots of the operator's long-standing "stop 2-4 times" bug: (a) an rc client's
reconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) —
resurrection fights the operator's stops; (b) rc freezes at "Reconnecting to local daemon…"
forever when its session died with the broker. Fix: rc NEVER auto-starts a daemon (reconnect
only to an already-up broker; loud "session lost — daemon down" exit otherwise), bounded
reconnect with visible countdown.

**REQ-DAEMON-STDERR-PERSIST** (impl, unit)
Observability: detached daemon nulls stdio → the incident window left ZERO logs (both RCAs ran
blind; rigs had to recreate everything). Fix: broker + brain stderr tee to a rotating file
under SPT_HOME (size-capped, e.g. 2×5MB), stamped per generation. KNOWN-HAZARDS note: never
inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.

## W4 — spawn/wake + listener lifecycle

**REQ-SPAWN-COLLISION-GUARD-LIVE-DUP** (impl, unit, int)
Root (perri parentage + recovered filing): the WAKE/RESUME respawn seam processed ONE wake
TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume
<sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. Damage mode:
duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a
fresh /clear rotation → injects routed to the contended record and lost). Fix: single-flight
wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second
wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim.
Int: two concurrent wake requests → exactly one launch tree.

**REQ-HAZARD-LISTEN-ORPHAN** (impl, unit)
Root (mobile-gw RCA): `api listen --parent-pid N` is auth-anchor ONLY — no liveness watch;
host death orphans the listener forever → perch held alive (false ONLINE), EVENTs stream to a
dead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). Fix: listener watches
`--parent-pid` liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud
on parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed
SPT-CORE-NEEDS §5.
Unit: parent-death → listener exits within one poll window.

## W5 — delivery integrity

> **Two-root split (doyle, 2026-07-07 — both live-reproduced tonight, distinct roots).**
> The night's delivery failures resolve into TWO separate defects, one per REQ below:
> **(a) parked-idle sleep** = REQ-IDLE-PARKED-DELIVERY — a QUEUED message to an idle
> endpoint PARKS (not lost) and flushes only when inbound activity wakes the target's
> delivery drain. Confirmed clean wake trigger: todlando's W2-proposal parts (part-1
> tail + 3/4) sat parked at doyle's perch for HOURS, then flushed the instant perri's
> idle-inject probe woke doyle's drain — parked, not truncated. **(b) live-SENT
> truncation/empty-body** = REQ-INJECT-MULTILINE-INTEGRITY (below) — a genuinely
> mangled payload on the typed leg, unrecoverable (no spool copy). Do NOT conflate:
> (a) is a scheduling/wake gap (bytes intact, late); (b) is byte corruption in transit.
> File-transport workaround (proposals/reports ride branch files, sends carry only the
> pointer) dodges BOTH legs and stays binding this milestone.

**REQ-INJECT-MULTILINE-INTEGRITY** (impl, unit, int)
> **ROOT PINNED (doyle + perri, 2026-07-07 — pre-activation).** Perri's 4-point
> verbatim series pinned it to a RAW TYPED-STREAM byte window: an inbound envelope
> over `EVENT_LINE_THRESHOLD` (400B, `chunk.rs`) is split into `EVENT-PART` lines;
> the TERMINAL-SIDE pre-settle window swallows a raw byte span of ~1148–1152B
> (incl. line framing) and cuts MID part-3 body — a ~20B tail survives, which is
> exactly why every artifact starts mid-word. Slot math verified on perri's verbatim
> `seq=1/4` line: first slot is exactly 321B, attrs `seq`/`id`/`type`/`from` only.
> Constant escaped head-loss across the series (992/990/988/992B). NOT a size cap and
> NOT the parked-idle wake gap (that is REQ-IDLE-PARKED-DELIVERY) — this is a
> pre-settle byte-window swallow on the typed leg. **Fix direction:** instrument the
> inject pre-settle window; settle-before-type or ack-paced parts (chunk-then-settle
> per EVENT-PART, or a per-part write+echo-verify). **Interim protocol (fleet-wide,
> binding this milestone):** sub-400B sends (unchunked, ride one slot) OR file
> transport; anything larger risks the swallow. **Open question (doyle):** the WIRE
> first-part attrs run ~20B longer than the on-paper count — the raw relay-log line
> will settle the exact slot math before the fix encodes a threshold.
>
> (Field instances this milestone, all live-SENT, no spool copy, unrecoverable:
> todlando's W1 gate report HEAD-TRUNCATED at doyle; a flynn EVENT fully EMPTY.)

Root: the idle-inject TYPED delivery leg eats HEAD bytes
nondeterministically — spool rows complete (1669B) vs ~322B received suffix; mid-turn poll
envelopes always intact; a 1854B body later rode the same leg intact ⇒ timing race
(terminal-readiness / enter-coalescing settle class), not a size cap. Fix direction (todlando
proposes on the broker/translate typed-inject seam): settle-before-head, bracketed-paste where
the harness supports it, or chunked write with echo-verify. Note: live-SENT injects leave NO
spool copy — truncation there is unrecoverable, which raises the stakes.
Int: repeated large multi-line injects into a real PTY session arrive byte-complete (loop N
times — the race is timing-dependent, single-shot green is not proof).

**REQ-IDLE-PARKED-DELIVERY** (impl, unit, int)
Root (live during this very dispatch, 2026-07-07): a message QUEUED to an ALREADY-idle
endpoint sleeps indefinitely — the idle-edge drain (F-023 leg 2) fires only on the
ACTIVE→IDLE transition, and no new edge ever comes for a parked session; the send-time inject
didn't carry it either (both doyle→todlando milestone dispatches sat delivered=0 in the spool
while the endpoint showed ONLINE). Fix: send-time inject must fire for an already-idle
spt-hosted target (the activity sense says idle ⇒ inject now, not spool), and/or a bounded
spool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop).
Int: send to a session idle for N minutes → delivered without any operator poke.
Live confirmation (2026-07-07): parked messages flush the moment inbound activity wakes
the target's delivery drain (doyle's parked W2-proposal parts flushed on perri's
idle-inject probe) — the wake trigger is real, so the fix is "make the send-time inject
fire for an already-idle target" / "sweep on the pulse tick", not a new wake mechanism.

**REQ-SPOOL-TAKE-AUDIT** (impl, unit)
RCA cost: proving WHO took delivered=1 rows burned an hour. Fix: spool records taker per row
(leg enum: relay-backlog / hook-poll / idle-inject / psyche, + sid/pid + taken_at ms); surfaced
by a `--json` debug read. Additive column, no schema break (delivered rows already retained).

## W6 — docs wave (publish the contracts perri built blind against)

**REQ-DOC-ECHO-COMMUNE-CONTRACT** (doc)
`[session.echo_commune]` I/O contract on the published docs-site: key catalog core fills; core
does NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR /
read_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1);
stdout ingestion expectations. This gap cost a full outage night — priority slot.

**REQ-DOC-DELIVERY-VOCAB** (doc)
The remaining flynn/perri gaps folded: full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED
window semantics / DEFERRED / NO_PERCH), `digest --json` row schema, api poll auth + MAC-stamp
prefix, remaining `--json` shapes checklist (seed #3). Public docs use VERSION numbers, never
wave codes; docs-publish drift gate applies.

---

## Sequencing + gate notes

- W1 ships alone and fast (three small REQs, one seam) — unblocks two live psyches; doyle
  field-accepts on this node same-day.
- W2 needs a doyle design ruling on the decouple shape BEFORE impl (todlando proposes; the
  log-lock/subscriber contract is KNOWN-HAZARDS-grade). Its int test IS tonight's rig.
- W3 composes: FINISH rides ENDPOINT-SURVIVAL. RC-RECONNECT-TRUTH is separable if the wave
  runs long.
- W4/W5 independent of W2/W3 — parallelizable if a second executor frees up, else sequential.
- Every wave: traceable activate → build → `traceable-reqs check` exit-0 → doyle gate
  (worktree of the wave sha) → next wave. Release cut at milestone end per RELEASE-RUNBOOK
  (bump-ON-milestone-branch pattern, deployah drives).
