# W20 — Project registration verbs + node-owned daemon start

Wave goal: close the two Requests the Operator scheduled (#21, #22) and publish the
result as a release. Both are about the same gap seen from two sides — *nothing but a
shell instance is responsible for the Hub's own composition and liveness*.

Operator rulings taken into this wave (2026-07-25):

- Release signing for spt-alchemy is **cut**, not deferred. Remove it from open tracking.
- **#21 authz: any owner may add a Project.** The daemon node is a trusted single-user
  node and `bind-repo` is already ungated; requiring the Operator reintroduces the wait
  that #21 exists to remove.
- **#21 and #22: build both, drive to a release publish.**

## Scope

### T1 — `daemon project <list|add|remove>` (Request #21)

`REQ-DAEMON-PROJECT-REGISTRY` (new).

- `daemon project list` — the configured Projects, in config order.
- `daemon project add <owner/repo>` — validates `owner/repo`, refuses duplicates
  idempotently (reports, exit-clean), appends to `daemon.toml` preserving `guild_id` and
  `poll_interval_secs` by rewriting the parsed struct.
- `daemon project remove <owner/repo>` — removes the entry. **Refuses to empty the
  list**: `config::load` treats an empty `projects` as invalid and a present-but-invalid
  file must never demote the daemon node (`config.rs`). Discord channels are **left in
  place** — this verb un-registers, it never deletes a channel or its history.

Not the Operator's to gate, but stated because it is a real edge: `remove` stopping the
Watch engine from polling a repo is the point of `remove`; live Watches on that repo stop
being delivered. The verb says so in its reply.

### T2 — the added Project takes effect without a daemon restart (Request #21, second half)

The issue names the cost of the restart path directly: a restart drops the gateway for
every other Project on the node. So the registry change is picked up live.

- **Watch engine** (`engine::run_loop`): reload `daemon.toml` at the top of each cycle,
  falling back to the last-good config on a read/parse error (an invalid edit must not
  kill the engine). Cheap: one file read per poll interval.
- **Discord side** (`run::poll_loop`): reload per tick; when the project set differs from
  the set last provisioned, re-run `provision::ensure_guild_structure` (already
  idempotent — it creates only what is missing) and publish the refreshed `GuildIndex`.
  `Handler.index` becomes an `Arc<RwLock<Option<GuildIndex>>>` shared with the loop, so
  capture/mint on the new channel work in the same process without a Ready refire.

### T3 — OUTCOME: withdrawn, and replaced by the real fix

> **What follows is the plan as written. It was wrong, and the record keeps both.**
> T3 shipped in v0.3.0 and was withdrawn the same day in v0.3.1 (ADR-0012 → Rejected).
> The Operator's ruling: no autostart mechanism — the daemon's start rides whichever
> endpoint's linked alchemy shell comes up first, as specced. Then the Operator's
> question found the root cause: a force-close cannot kill a process it never spawned
> and does not know about, so how was it reaching the daemon? Because `taskkill /T`
> walks **ParentProcessId**, and the console-detach flags never severed parentage — the
> daemon was still the shell's child in the process table. `CONTEXT.md` was right and the
> spawn was not implementing it. **Fixed by a relay hop** inside the one existing start
> path (ADR-0013), field-verified: daemon runs with a dead ParentProcessId, and a
> `taskkill /T` on a stand-in shell leaves it alive. Residual, from doyle: job membership
> inherits to every descendant regardless of parentage, so a CLI-launched shell in a
> kill-on-close job would still take the daemon with it — terminal-coupled until proven
> otherwise, tracked upstream as `REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE`.

### T3 (as planned — superseded) — node-owned daemon start + supervision

`REQ-DAEMON-AUTOSTART` (new); activates `REQ-HAZARD-DAEMON-ORPHAN-DEATH`
(KNOWN-HAZARDS 1.5).

The tree kill is spt-core's and the option ruling upstream is doyle's — this task does not
pre-empt it. It removes alchemy's exposure to it: `daemon autostart <install|remove|status>`
registers a Windows Scheduled Task that runs `alchemy daemon ensure`

- at logon, so the daemon's lifetime is the node's, not the first shell that happens to
  bind; and
- on a repeat interval, so a daemon lost to *anything* — a tree kill, a crash, an
  overzealous cleanup — comes back without a human noticing first.

`ensure` is already the correct supervision primitive: idempotent, version-aware, spawns
only when the node lock is free. The task's spawn parent is the Task Scheduler, so no
shell is an ancestor and `taskkill /T` on a shell cannot reach the daemon.

Installing the task on this node is a machine-level change and is asked for separately.

### T4 — release

`v0.3.0` (feature adds, no break) by `docs/RELEASE-RUNBOOK.md`: CHANGELOG, version bump,
`adapter.spt` + `SHA256SUMS`, publish on `BigscreenVR/spt-progress-tool`. **Publish only.**
Deployment is separate — it needs every alchemy instance on the node down at once (shared
install dir), which means asking every owner.

## Gate

`cargo build` + `cargo test` + `traceable-reqs check` all green, docs updated in the same
commits as the evidence (CONTEXT.md for the model change, ADR for the autostart decision,
KNOWN-HAZARDS 1.5 promoted from inactive).

## Closed — 2026-07-25

- **v0.3.0 published**, then **v0.3.1 published and deployed on this node.** 0.3.1 is what
  the node runs; 0.3.0's only distinct content was the withdrawn autostart verb.
- **#21 done**, promoted by the v0.3.1 release sweep. Field-proven on the live Hub:
  `daemon project "add BigscreenVR/spt-rebound-tool"` → the running daemon logged
  `registry reloaded — 4 projects; reprovisioning` and created the channel, its threads
  and its contents post **in place**, no restart, no gateway drop for the other three
  Projects. perri's Project is on the board — this morning's diagnosis closed by one verb.
- **#22 stays open** for spt-core's half (tree-kill breadth, job exposure). alchemy's side
  is fixed and the Request records the corrected root cause.
- **#23 filed** (Operator, during the deploy): upgrading needs every instance on the node
  down at once; the design should allow in-place upgrade. Needs-routing — the install path
  is spt-core's.
- **Release signing: cut permanently** (Operator). Removed from the runbook and the
  manifest as an open item.
- **STALE-ONLINE field test run** against doyle's branch CLI: liveness-after-kill PASS,
  relink gate PASS, and the leg only alchemy can assert — id, perch, repo binding and tag
  cursor byte-identical across kill + relink — **PASS**. `shell cmd` failing to wake a
  corpse is a reported FAIL, pending doyle's read (likely untestable against a resident
  daemon still running main). Node-wide swap offered and pending his schedule.

## Out of scope

- spt-core's tree kill (upstream, Request #22 body, doyle's option ruling).
- The STALE-ONLINE branch field test — authorized by the Operator this session, runs on
  doyle's schedule when his build lands.
