# W25 — The node owns the Hub Daemon (spt-core v0.44.0 adoption)

**Scope:** adopt the two v0.44.0 features alchemy has been blocked on — the
**ResidentService** (`[service]`) and install-dir program-token resolution in
`[shell].spawn` — and retire the two interim mechanisms they replace.

**Gate:** `cargo build` + `cargo test` green, `traceable-reqs check` exit 0, and a live
proof that the Hub Daemon comes up **under supervision** with alchemy's own start path
deleted. The last one is the whole point: everything else is bookkeeping.

## What v0.44.0 actually delivers (measured, not read)

Both were verified on this node before any code was written.

1. **`[service]` exists and is published.** `manifest.schema.json` defines it: `command`
   (required), `start` (required, `"boot"` | `"bind"`), `stop_grace_ms` (default 30000).
   The daemon spawns it **job-neutrally** — never a shell's child, never inside a
   launching terminal's Job Object.
2. **`[shell].spawn` resolves a bare program token from the install dir.** Probe: a
   copy-mode adapter whose install dir held `alchemy.exe`, with the shipped spawn line
   verbatim, spawned and came **ONLINE** (`alchemy-spawnprobe-0`, pid 39620, exe path
   confirmed). Under 0.43.0 the same manifest failed `os error 2`.
   **This is a docs gap, not a docs answer** — the published resolution list names
   `[session.psyche_resume]`, the `[digest]` extractor, `adapter digest-proof` and
   `[service].command`, and `[shell].spawn` appears in none of them. Raised with doyle;
   alchemy proceeds on the measurement.

## Design rulings (DRI, 2026-07-26)

These are not open questions — they were ratified on Request #22 before v0.44.0 landed
and are transcribed here so the implementation does not re-derive them.

1. **`start = "boot"`.** Required field, deliberately no default. Boot is
   desired-state-running: the supervisor reconciles at daemon boot, at adapter
   registration against a live daemon, at update-hold release, and at first shell bind as
   a defensive ensure. That covers every trigger auto-ensure-at-bind covered.
2. **`command` uses the bare program token**, not `{adapter_dir}/alchemy` — bare is the
   documented resolution primitive (REQ-INSTALL-11, "one resolution primitive, no
   parallel path") and keeps the archive self-contained.
3. **Alchemy's start-path count goes from one to zero.** ADR-0012's ban was on a *second*
   start mechanism inside alchemy; core becoming the single start authority honors it.
   The relay hop (ADR-0013) retires — alchemy stops re-parenting anything.
4. **The `daemon` verb keeps `status` and `project`; `ensure`, `restart` and `stop`
   retire.** Lifecycle belongs to the supervisor; fighting it from the shell is how you
   get two authorities and no owner. Alchemy keeps *reporting* on the daemon and
   *configuring* which Projects it serves.
5. **The shipped manifest's absolute-path interim retires.** `docs/RELEASE-RUNBOOK.md`'s
   standing note — that a node installing from a release must also register a local
   manifest whose `spawn` names the binary by absolute path — is withdrawn. Undoing it is
   this wave's acceptance test.

## The one real risk, and the order that defuses it

Deleting alchemy's start path is a one-way door on a live system: if supervision does not
actually fire, the Hub never comes up and Watch delivery is silently dead node-wide.

**`REQ-HAZARD-DOUBLE-DAEMON` is what makes the transition safe** — a second daemon exits 0
before any gateway connect when another holds the node lock. So both paths may be live at
once without harm, which buys a verify-before-delete order:

- **T3 lands `[service]` with the internal path still in place.** Both can fire; the lock
  arbitrates.
- **T4 proves supervision fires** (`spt adapter service status alchemy`, daemon pid owned
  by the supervisor, not by a shell bind).
- **T5 deletes the internal path** only after T4 is green.

Never reorder these. Verify-by-delete-first is the trap: the outage it produces is silent.

## Tasks

- **T1 — registry.** `REQ-DAEMON-SUPERVISED` in `traceable-reqs.toml`, activated
  `["doc","impl","unit"]`, W25 group: the Hub Daemon is declared as a ResidentService and
  alchemy contains no start path of its own.
- **T2 — doc.** CONTEXT.md: the node owns the daemon's start. ADR-0012 and ADR-0013 get
  superseding notes (they are not deleted — the reasoning in them is why this shape won).
  `docs/KNOWN-HAZARDS.md` 1.5 restated: the invariant is now satisfied *by construction*
  rather than by the relay hop.
- **T3 — manifest.** `[service]` with `command = "alchemy daemon run"`, `start = "boot"`.
  `min_spt_core_version = "0.44.0"`. Contract test pins all three.
- **T4 — live proof, supervision fires.** With T3 registered: the daemon is supervised,
  reported by `spt adapter service status`, and survives a shell teardown.

  *DONE (2026-07-27), on the restarted daemon (broker image 0.44.0):*

  ```
  $ spt adapter service status alchemy --json
  { "option": "alchemy", "start": "boot", "supervised": true, "held": false, "latch": "none" }

  $ alchemy daemon status                    → running (pid 34744)
  $ Stop-Process -Id 34744 -Force            (simulate a crash)
  $ alchemy daemon status     (8s later)     → running (pid 23964)
  ```

  Relaunch-after-kill is the ownership proof: nothing in alchemy asks for that, so the
  supervisor is driving the lifecycle. The shell-teardown leg is subsumed rather than run
  separately — the supervisor started this daemon itself (`ADAPTER_SERVICE:alchemy:
  started`, no shell in the call path), so no shell is in its ancestry to orphan it.

  **First read was a fault, and the reason matters for every adapter that adopts
  `[service]` after already having a start path of its own:**

  ```
  $ spt adapter service status alchemy
  alchemy: start = "boot" — not running: a configuration fault
                                       latch: "startup_fault", supervised: false
  ```

  At boot the supervisor spawned the daemon while alchemy's own self-started daemon
  (pid 25076, which survived the spt-core bounce) still held the node lock, so the
  supervised child did the correct thing and exited 0 — `REQ-HAZARD-DOUBLE-DAEMON`
  working exactly as specified. The supervisor read that immediate exit as a startup
  fault and latched relaunch off. The latch then persisted after the incumbent was
  stopped: `spt adapter service` is read-only by design, so no CLI clears it.
  Re-registering the adapter (`spt adapter add <dir>`) cleared it and started the
  service in the same breath. Filed to doyle — the migration window (old start path +
  new `[service]`) is exactly where a benign lock-exit gets misread as a fault, and T5
  is what removes it here.
- **T5 — impl, delete the start path.** `main.rs` auto-ensure-at-bind, `lifecycle::ensure`
  spawning, `spawn_relay`, `spawn_detached`, and the `ensure`/`restart`/`stop` ops. Unit
  tests for the relay hop retire with it.

  *DONE (2026-07-27), after T4 proved supervision fires.* Deleted: `RELAY_OP`,
  `spawn_relay`, `spawn_detached`, `daemon_command`, `stop_pid`,
  `wait_for_lock_release`, `ensure`/`ensure_inner`, `main.rs`'s relay-op branch and its
  auto-ensure-at-bind, and the relay-hop unit test. **Alchemy's start-path count is zero
  — grep for any of those symbols returns one comment and nothing else.**

  What replaced them, deliberately:
  - `lifecycle::status_line` — the read-only one-liner `main.rs` emits at bind and
    `daemon setup` closes with. It reports; it cannot start.
  - The retired ops answer **by name** (`ensure|start|stop|restart` → "retired at v0.5.0
    … use `spt adapter service status alchemy`") rather than falling through to "unknown
    op". An owner asking to restart the daemon is asking a question that now has an
    answer elsewhere, and saying so beats a vocabulary error.
  - `daemon setup` no longer starts what it configured, so it names the supervisor and
    the latch-clearing re-registration instead — the one place the adoption gap T4 found
    is reachable by a real user.

  Evidence moved with the mechanism: `REQ-HAZARD-DAEMON-ORPHAN-DEATH`'s impl/unit tags
  were on `spawn_relay` and its test. They now sit on the retired-op arm and its test —
  a shell that cannot start, stop or restart the daemon cannot make the daemon's lifetime
  a side effect of its own, and the construction half is pinned by the `[service]`
  contract test. Retitled two requirements whose titles asserted the deleted mechanism
  (`REQ-DAEMON-LIFECYCLE`, `REQ-DAEMON-SETUP`'s tail); `PHASE3-PLAN.md` ruling 3 carries
  a superseding banner and KNOWN-HAZARDS 1.1's mapping now credits the lock in the daemon
  process — which is *why* the invariant survived the deletion — instead of auto-ensure.

  Gate: **210 unit + 10 int green, zero warnings, `traceable-reqs check` exit 0
  (66/66)**. `REQ-DAEMON-SUPERVISED` is complete doc+impl+unit.
- **T6 — runbook.** Rewrite the install dance: the supervisor's update hold (quiesce →
  hold → swap → start) replaces "every instance on the node down at once", which is
  Request #23's whole complaint. The `daemon ensure` escape hatch section retires.

  *DONE (2026-07-27).* Four sections rewritten in `docs/RELEASE-RUNBOOK.md`:
  the "released shell is not yet spawnable" limitation (withdrawn — the 0.44.0
  measurement, with a standing instruction to re-measure rather than re-add a path on
  suspicion), the install pre-flight, the `daemon ensure` escape hatch (retired, replaced
  by `daemon status` + `spt adapter service status` + the latch reading), and *Getting
  back to the dev loop* (a release install is now a complete registration; a dev manifest
  is for iterating on `target/debug` and nothing else).

  **Request #23 is only HALF closed, and the runbook says which half.** The daemon side
  is genuinely gone — the supervisor holds and swaps it, `spt adapter service list`
  reports "whether an update is holding it", nothing is left for a person to run.
  Shell instances still hold the exe open and still have to be asked down, because shell
  quiesce detection has not landed upstream (`spt adapter remove --help` states it in as
  many words). Claiming the whole complaint closed would be the overclaim; the note the
  Operator or the release sweep needs to judge it is in the pre-flight.

  Evidence basis, stated because :5474 is down (broker-side bind failure, doyle
  2026-07-27, until the next full daemon stop+start): the update hold is documented from
  **CLI help**, not from the published docs, and its exact sequence is unproven on this
  node until T7's install exercises it.
- **T7 — release.** 0.5.0 (verbs retire — flag it under Changed), publish, install, and
  prove the loop closes with **no dev-manifest re-registration afterwards**.

  *DONE (2026-07-27).* `v0.5.0` published on `BigscreenVR/spt-progress-tool`
  (`adapter.spt` + `SHA256SUMS`), installed here with `spt adapter add --release`.

  **Acceptance test GREEN — the interim is dead.** A freshly minted `alchemy-1` spawned
  from the **release pointer registration** and came online, no dev manifest anywhere in
  the path; `daemon status`, the retired-op reply, `daemon project list` and `help` all
  answered through the shipped binary. The standing "a released shell adapter cannot
  launch" note is deleted from the runbook rather than deprecated.

  **What the install taught, which contradicted T6's runbook text an hour after I wrote
  it:**
  - spt-core **renames** the running exe to `alchemy.exe.old` and writes the new file
    beside it, so a live daemon does **not** block an install. The Windows file lock is
    not the obstacle anyone assumed — for the daemon. Shells still are, because they must
    relink onto the new image anyway.
  - **No update hold fired** (`ADAPTER_SERVICE:alchemy: already running`, `held: false`),
    and the daemon went on serving the renamed old image — reporting **v0.4.2 three
    minutes after a 0.5.0 install landed**. Process start 00:02:03 vs exe mtime 00:18:01
    was the tell; the version string alone would have lied.
  - Killing it let the supervisor relaunch on the new bits (pid 44284, v0.5.0).
  - **Then :5474 came back and sharpened it:** `ServiceStart::boot` is specified as
    desired-state-**running**, and a live process satisfies that target — so "already
    running" is correct behavior, and *nothing in the documented trigger list is "the
    binary underneath changed"*. The open question narrowed accordingly and went to
    doyle: does a registration that installs a payload take an update hold, or is
    "operator ends the process, desired-state brings it back" the ruled upgrade path?
    Either answer is shippable; the runbook states the measurement and the question
    rather than planning around an unobserved mechanism.

  Node state: three owners' shells (flynn, liam, perri) were down for the swap — all
  asked, none killed by me. All three hit the known corpse-handle relink refusal;
  perri cleared in ~2 min, mine on the retry after, liam's retry loop armed. Two
  independent specimens (mine pid 5756, liam's 25944) both showing
  `GetExitCodeProcess = 4294967295` rather than `STILL_ACTIVE` went to doyle.

## Not in scope

- **Request #6's masked-link render.** Still WIP and still wrong: escaping is not honored
  in the text half at all (proofs #30 and #32 both rendered literal). That fix needs its
  own wave once the baseline proof (#33) says whether masked links render on that surface.
  It does not ride this release.
- **Request #23's close.** T6 may satisfy it, but the Done transition is not the DRI's to
  self-serve — it goes to the release sweep or the Operator.
