---
name: shell-launch-truth-seed
description: "SHELL-LAUNCH-TRUTH — the daemon's picture of a shell binary vs the truth on disk. Three legs (DOA child, wedged instance, missing {adapter_dir}), all root-verified in source 2026-07-25. Unranked; doyle ranks and owns the backlog entry."
metadata: 
  node_type: memory
  type: project
  originSessionId: 5a8a8aa7-3934-44d3-a95b-f63faeae446a
  modified: 2026-07-25T12:24:57.732Z
---

Umbrella seed born out of the v0.42.0 IDLE-EDGE W1 field-verify window ([[idle-edge-w1-progress]]).
doyle ratified the name and the root-cause convergence: **one seed, one mechanism.** All three
sightings are "the daemon's picture of a shell binary versus the truth on disk."
**doyle OWNS the findings-backlog entry — he holds parallel edits and does ONE reconcile pass at
liam-close. Do NOT re-edit `spt-core-findings-backlog.md` for these seeds meanwhile.**

## Leg 1 — DOA child (perri, verified)
`SHELL_SPAWNED:... pid=<N> status=offline (online at bind)` for a child that never ran. NOT "a
template spt cannot launch": `SHELL_SPAWN_FAIL` already exists and fires with rollback on real
launch failures. CreateProcess **succeeded**; the child exited immediately on its own. Nothing
observes it after that — `launch_shell` (spt-daemon/src/shellhost.rs:160-211) spawns detached with
stdio discarded BY DESIGN (KNOWN-HAZARDS 5.6: an inherited handle on an immortal child hangs any
capturing caller forever). Fix must not re-open 5.6 → per-instance spawn LOG FILE, never a pipe.

## Leg 2 — a dead child WEDGES the instance (perri, verified; the worse half)
Kill a HEALTHY resident → `shell list --json` still says `status: "online"`, and `shell relink` —
the DOCUMENTED way back — refuses `SHELL_ALREADY_ONLINE`. Only teardown+spawn recovers.
Root: `relink_shell` (spt-daemon/src/linkhost.rs:411-423) gates on `info.status ==
SHELL_STATUS_ONLINE`, a **stored flag** written once at bind (crates/spt/src/cli.rs:16808), never
reconciled against child liveness. doyle: this root explains ALL THREE sightings including flynn's
alchemy-0.

## The mechanism both legs want (doyle-ratified + sharpened)
Liveness reconciliation of `info.status` against the recorded child pid — **routed through the
shipped custody predicate `spt_store::proc::process_identity` (tri-state
Present(started)/Absent/Unproven), NEVER a bare pid-exists check.** This box recycles pids fast
(family-member-5 incident class); a recycled pid wearing the dead child's number would re-arm the
exact lie being killed.

**CONFIRMED WORK ITEM: `SHELL_PID_FILE` parks the pid ALONE.** Writes at shellhost.rs:206 and :254
(`pid.to_string()`, bare decimal, no sidecar); reads at :346 (kill_shell_at) and :392 (close_shell)
(`s.trim().parse::<u32>()`); resting.rs:1346 only `.exists()`. So the fix REQUIRES spawn-time
**(pid, started_at) pair parking**, with **legacy pid-only reading as Unproven** (every perch on
disk today holds a bare pid).

**Unproven inverts per caller** (doyle): reconcile-to-offline = NO on Unproven (leak-tolerant);
relink-unstick = also NO-KILL, ask teardown.

**⭐ SISTER TO MIRROR, DO NOT INVENT A PARALLEL IDIOM: `spt_store::resume_custody`.**
`resume_custody.rs:150-158` already does exactly this — parks pid + started_at, matches
`ProcIdentity::Present(started) if started == rec.started_at => Custody::Ours`, defers on Unproven,
and carries a has-already-deferred flag on the record (:66). ABA adversary rig already exists:
`spt-daemon/tests/resume_custody_aba.rs`.

**⚠ REQ-HAZARD-DEAD-REC-PID DOES NOT BLOCK THIS** (my rider, doyle RATIFIED). That hazard is about
WORKER perches, whose recorded pid is the ephemeral worker-start HOOK process — dead by design. A
SHELL's recorded pid IS the long-lived child. Two different pids. Say this out loud in the REQ or
the builder will talk themselves out of the only available signal.

**⚠ Unproven is a REGULARLY-TAKEN branch here, not a rare edge** (my rider): `process_identity`
returns Unproven for alive-but-unreadable too (proc.rs:604-606 — race with exit, or denied query),
and this box runs ~35 spt processes on a self-hosted runner. A shell stuck reading Unproven must
stay teardown-recoverable and must SAY WHY it will not un-stick — else we swap a silent wedge for a
silent refusal.

## Leg 3 — no `{adapter_dir}` in `[shell].spawn` — PARITY GAP (⚠ perri RETRACTED the "blocks my milestone" half)
**⚠ perri RETRACTED the packaging blocker 2026-07-25 — it never existed.** The documented install
path works today on 0.42.0 with their SHIPPED bare-name manifest untouched: `cargo install --path .`
(puts the exe on PATH) → `spt adapter add .` → `spt shell spawn rebound` → online. `shells/getting-started`
walks exactly that for spt-shell-notify, whose manifest declares a BARE-NAME spawn. Their rig skipped
cargo-install and registered a dir with the exe beside the manifest, never on PATH. True observation
about adapter-dir resolution; FALSE conclusion about installability. Their operator caught it by
pointing them at the published docs (localhost:5474) they had not been reading.
**WHAT SURVIVES — the parity gap is real for the RELEASE PATH ONLY:** an `adapter.spt` unpacks into
the adapter dir and nothing puts that binary on PATH, so a release-installed shell genuinely cannot
name its own binary without `{adapter_dir}`. Founding evidence reverts to **flynn's original alchemy
filing**. Lands cleanly under the ruling: a gh_release adapter derives Pointer → `source_dir` IS the
extracted dir holding the exe → the key resolves correctly by construction.
**✅ doyle RULED WAIT + rank BEHIND LAUNCH-TRUTH in the shell wave, no jump.** No consumer blocked
(perri's rig workaround is free, flynn shipped); legs share files; one leg alone buys a second gate
cycle for zero unblocking value. **My impl/unit/int shape is BANKED AS THE DISPATCH SPEC — wave-day
starts from it verbatim.** ⚠ **Int-rig premise CHANGED by doyle: model the RELEASE-INSTALLED layout,
NOT perri's dev rig** — that rig's failure was a skipped cargo-install, so an int case built on it
would pin a rig artifact as the contract. **REQ lean (final call at activation, registry in hand):
FOLD as a fourth surface onto REQ-MANIFEST-SUBST, not a new row** — the whole ruling is "one key, one
meaning, one helper" and traceability should say the same; mint a row only if that REQ's stage shape
fights the int evidence.

### Original filing detail (verified; kept for the mechanism, not the urgency)
`fill_spawn_command` (spt-daemon/src/shellhost.rs:127-139) offers exactly four keys: `id`,
`adapter_name`, `link_token`, `perch_dir`. perri's 4-way test: bare name → os error 2 (no
install-dir resolution); `{adapter_dir}` → "no value for substitution key" (**key does not exist**);
`{perch_dir}/..` → substitutes fine (**machinery works**); `./x.exe` → resolves against the DAEMON's
cwd. Consequence beyond one repo: **no `kind="shell"` adapter can ship a PORTABLE manifest.** Authors
are pushed to node-local absolute paths — alchemy did exactly that AND had to strip `[update]` so an
update could not re-register the portable manifest and re-break spawning.

**⭐ THE TRAP — the obvious fix does not fix the reported case.** A private resolver with the right
name semantics already exists (`manifest_dir`, registry.rs:170-175: Copy → `adapters_dir/<name>`,
Pointer → `source_dir`). But **Copy mode copies ONLY manifest.toml + strings/** (registry.rs:397-406)
— binaries are never copied. perri registered Copy, so that key would substitute cleanly into a dir
with no exe → os error 2 from a path that LOOKS right. Worse than the current failure.
**And the mode is not the author's choice:** registry.rs:353-354 derives it from the update avenue —
FilePull/none → Copy, Delegated/GhRelease → Pointer. So a properly-shipped adapter with a release
avenue is Pointer, where the obvious fix is already correct; perri is in Copy *because* the alchemy
pattern strips `[update]`. **The two halves feed each other** — that is why it filed as one
neighborhood, not a standalone key request.
**✅ RE-RULED 2026-07-25 — PARITY FIX RATIFIED, prior ruling SUPERSEDED. THIS is the live design:**
1. **PARITY:** `fill_spawn_command` gains `{adapter_dir}` via the SAME `inject_adapter_keys` helper
   the other three surfaces use. One key, one meaning (`source_dir`), zero new semantics; N-1-safe by
   the `{perch_dir}` opt-in argument. doyle: my consistency ground was decisive on its own — one key
   meaning two directories by manifest field is an author trap that READS CORRECT IN REVIEW, worse
   than everything else on the table.
2. **Bare-name rider aligns to the same truth:** resolve the program token against `source_dir` FIRST
   (the directory the key names), PATH fallback unchanged — key and bare-name agree by construction.
3. **Fleet-wide meaning of `{adapter_dir}` = SEPARATE UNRANKED SEED** at reconcile. Hazard already
   ships on three surfaces; shell spawn does not get to answer it unilaterally (my framing, adopted
   verbatim).
4. **Teaching-refusal design → DRAWER, explicitly NOT built.** Re-arms only if the fleet-wide question
   someday makes the key legitimately binaryless.
5. perri's three grounds + dev-loop caveat: moot under parity. Their `--pointer` decouple idea rides
   the drawer note as context. doyle told perri directly so M2 un-builds from the dead premise with
   his authority, not just my relay.

⭐⭐ **SHARED PROCESS LESSON, doyle's words for the reconcile record: "two rulings this wave were
issued on builder-relayed premises while my own banked evidence held the correcting fact — the gater
checks the ledger BEFORE ruling, that is what the ledger is for."** His own SHELL-SPAWN-SELF-CONTAINED
seed (banked from flynn days earlier) ALREADY recorded `{adapter_dir}` as shipped machinery
"available wherever substitution runs" (runtime.rs `inject_adapter_keys`, ADR-0029). Both ends missed
it: I didn't grep, he didn't re-read his own ledger. The stop-before-built catch is the gate working
in the correct direction.

**⛔ HISTORICAL — the ruling below rested on the wrong premise. Kept only so nobody re-derives it.**

**`{adapter_dir}` IS NOT A NEW KEY. It is SHIPPED, REQ-tagged (REQ-MANIFEST-SUBST /
REQ-INSTALL-11), and it ALREADY FILLS TO `source_dir` — i.e. option (b), the one ruled out.**
Three surfaces, one shared helper `spt_runtime::runtime::inject_adapter_keys(keys, install_dir,
adapter_name)`: harness session spawn (harnesshost.rs:144), `[digest]` extractor (digest.rs:267-269),
`[update.post].command` (cli.rs:10906/10910). Publicly advertised to authors in the deprecation line
at registry.rs:340-343 and documented at manifest.rs:479/:611. Fills to `record.source_dir` — asserted
at harnesshost.rs:97, cli.rs:2062-2067, and a unit at api/mod.rs:789.
**So perri's filing is a PARITY GAP:** `[shell].spawn` is the ONE template surface missing a key every
sibling surface has. Recommended fix = add it via the SAME helper; no new semantics; N-1-safe by the
same opt-in argument that got `{perch_dir}` in.
**Why (a) would be actively harmful here:** it would make ONE key mean TWO DIFFERENT DIRECTORIES
depending on which manifest field it appears in — source_dir on three surfaces, manifest_dir in
`[shell].spawn` — in a path whose failure mode is os error 2. Reads as correct in review.
**doyle's objection to source_dir is sound but is NOT NEW RISK** — that hazard already ships on three
surfaces. The real question ("should `{adapter_dir}` mean source_dir fleet-wide?") is bigger, has N-1
consequences for every adapter using it, and must NOT be answered by making the shell path the odd
one out. Parity also dissolves the Copy-mode binaryless-dir trap AND perri's dev-loop caveat (Copy
`source_dir` IS the author's real tree, where the exe sits). The teaching-refusal design stays in the
drawer — good mechanism, nothing to refuse under parity.

⭐ **MY PROCESS FAILURE, twice on the same reflex:** I framed a fork for the gater without first
grepping whether the key already existed elsewhere in the tree. Same root as the poll-vs-push scope
error earlier in this window — **asserting release/contract scope from a summary instead of from the
tree.** A wrong fork handed to a decision-maker is worse than a wrong answer: it gets RATIFIED, and
perri had already replanned M2 (`90fb068`) around it before I caught it. **Before handing anyone a
design fork: grep the whole tree for the identifier.**

**⚠ SUPERSEDED-PENDING-RERULE — doyle's ruling of 2026-07-25 on the WRONG premise: (a) + LOUD
TEACHING REFUSAL now; (c) DEFERRED-UNTIL-CONSUMER; (b) ruled OUT.** Grounds, verbatim-in-substance:
1. **The case that must work is the SHIPPING case.** A properly-released shell adapter derives
   Pointer, where `manifest_dir` = `source_dir` = the extracted release holding the exe — so (a) is
   correct exactly where portability matters. flynn's alchemy and perri's rebound both END Pointer
   once they ship releases; that is the whole point of the self-contained wave.
2. **(b) is out on my own evidence** — a spawn that silently breaks when the author's tree is deleted
   is the stale-truth class this entire seed neighborhood exists to kill.
3. **(c) changes Copy's semantics fleet-wide** (copied-but-never-updatable binaries, disk cost) for a
   consumer that does not exist: who legitimately runs a Copy-mode shell adapter? A dev rig with no
   release avenue — and a dev ITERATING wants Pointer-at-source behavior anyway, which they get the
   moment they add an avenue. Defer-until-consumer.
4. **The refusal must TEACH, not just refuse (F-1):** a Copy-mode shell manifest naming
   `{adapter_dir}` fails at `adapter add`, loudly, with the reason (Copy carries no binaries) AND
   BOTH escapes (declare an update avenue → Pointer; or absolute path for a node-local rig). That
   converts the two-halves-feed-each-other loop into a **signposted fork instead of a trap**.
**RIDER:** doyle's earlier install-dir-first bare-name resolution ruling gets the same mode nuance —
resolve against `manifest_dir` (correct in Pointer; still-not-found in Copy falls to the existing
loud os-error path, acceptable under the teaching refusal).

perri volunteered a correction IN OUR FAVOR: `SHELL_SPAWN_FAIL` fires correctly and rolls back on all
four of these WITH the OS error attached. That is the honest failure path working as documented, and
it keeps leg 3 cleanly separate from leg 1 — two failures that both end in an offline shell, only one
of them silent.

Related: [[idle-edge-w1-progress]] · [[spt-core-findings-backlog]] · [[consumer-verifies-core-decides]]
