# Daemon lifecycle UX plan — service-aware `start`/`stop` + cross-platform `run`

> Status: PLANNED (next session). Mint REQs before implementing (CLAUDE.md rule 3).
> Origin: live rollout 2026-06-08 — a manual `spt daemon run` on kitsubito
> fought the systemd `spt-daemon` user service over the broker socket
> (auto-restart-fail loop). User: "`start`/`stop` should manage the service
> when there is one; `run` must behave the same on every platform."

## Problems observed

1. **`start`/`stop` are service-blind.** `spt daemon start` (the alias added in
   `3392158`) just runs `Daemon::run()`; `spt daemon stop` just IPC-stops a
   running daemon. Neither touches the registered service manager. A user with
   a service installed expects `spt daemon start|stop` to drive *that*.
2. **`spt daemon run` is platform-inconsistent.**
   - Linux: runs the daemon **inline/foreground** (blocks the terminal).
   - Windows: ends up an **invisible background task** (a detached respawn).
   Same verb, different behavior.
3. **Auto-start competes with the service.** Any `spt` command that needs the
   daemon calls `ensure_running()` → `spawn_detached()`, which starts a *manual*
   daemon even when a managed service exists → two daemons fight for the socket
   (exactly the kitsubito loop).

## Current code (baseline)

- `crates/spt/src/cli.rs`: `cmd_daemon_run()` → `spt_daemon::Daemon::run()`
  (foreground loop); `cmd_daemon_stop()` → `is_running()` + `request_stop()`
  (IPC graceful); `DaemonCmd::Run` carries `visible_alias = "start"`.
- `crates/spt-daemon/src/daemon.rs`: `ensure_running()` → `spawn_detached()`
  (Linux fork-style; Windows `DETACHED_PROCESS | CREATE_NO_WINDOW`, de-elevated
  via `deelevate`). `request_stop()` = IPC stop.
- Installer-registered services:
  - Linux (`install.sh`): systemd **user** unit `~/.config/systemd/user/spt-daemon.service`,
    `ExecStart=<bin> daemon run`, `enabled`; `loginctl enable-linger` for pre-login boot.
  - Windows (`install.ps1`): at-logon **scheduled task** `spt-core daemon`
    (`schtasks /SC ONLOGON /TR "<bin> daemon run"`) — needs elevation to register.

## Target model (consistent across platforms)

| Verb | Semantics (BOTH platforms) |
|---|---|
| `spt daemon run` | **Foreground, blocking.** This process *is* the daemon; logs to stderr; Ctrl-C / SIGTERM stops it. No auto-detach, no respawn. Used by the service unit's `ExecStart` and by manual debugging. |
| `spt daemon start` | **Ensure up in background, idempotent, service-aware.** Service registered → delegate to the service manager. No service → detached spawn (today's `spawn_detached`). Already running → no-op ("already running"). Non-blocking. |
| `spt daemon stop` | **Bring down, service-aware.** Service-managed + active → stop via the manager (so it does not auto-restart-fight). Else → IPC `request_stop`. Idempotent. |
| `spt daemon status` | Unchanged + show **managed-by** (service vs manual) and the service's enable/boot state. |

- **`ensure_running()` routes through the service-aware `start` path** — the CLI
  must never spawn a competing manual daemon when a service is managing one.
- `start` is no longer an *alias* of `run`; it becomes its own verb with the
  background/service semantics. (`run` keeps the foreground meaning.)

## Platform service abstraction

New module (e.g. `crates/spt-daemon/src/service.rs`) — detect + control:

```
trait DaemonService { fn detected() -> bool; fn start()->Result; fn stop()->Result; fn is_active()->bool; fn managed_label()->&str; }
```

- **Linux — systemd user unit `spt-daemon`.**
  - detect: unit file present (`~/.config/systemd/user/spt-daemon.service`) or
    `systemctl --user list-unit-files spt-daemon.service` succeeds.
  - start/stop/is-active: `systemctl --user start|stop|is-active spt-daemon`.
  - This is what removes the double-daemon fight (stop the unit, don't IPC-kill
    a unit that will auto-restart).
- **Windows — open decision (see below).** Today only an at-logon scheduled task
  exists (a boot trigger, not a controllable service). Two routes:
  - (a) **Promote to a real Windows Service** (`sc.exe create` / a small service
    host) → clean `start`/`stop` parity with systemd. Heavier; needs elevation
    + a service-mode entry in the binary.
  - (b) **Keep the scheduled task for boot-start; `start` = detached spawn,
    `stop` = IPC stop.** Simpler; honest asymmetry (Windows has no per-user
    systemd equivalent). `status` says "manual (task auto-starts at logon)".

## Conflict guard

- `run` detects a managed service that is already active → **warn loudly** before
  taking the socket ("a managed spt-daemon service is active; running inline will
  fight it — `spt daemon stop` the service first, or use `spt daemon start`").
- `start` is idempotent: already-running (manual OR service) → no second daemon.

## Consistency fix detail (the Windows `run` respawn)

- Audit why Windows `spt daemon run` goes invisible-background (likely the
  `Daemon::run()` de-elevation respawn / a `spawn_detached` on the `run` path).
  Make `run` strictly foreground on Windows — the detached/invisible behavior
  lives ONLY in `start`. Keep `CREATE_NO_WINDOW` for the `start` detached spawn,
  not for foreground `run`.

## Requirements to mint first (rule 3)

Add to `traceable-reqs.toml` before coding (REQ-DAEMON-* family or extend
REQ-CLI-2 / REQ-DAEMON-*):
- `daemon start|stop` manage the registered service when one is present.
- `daemon run` is foreground-consistent on every platform.
- internal auto-start (`ensure_running`) prefers the service; never double-spawns.

## Test plan

- **unit:** service-detection truth table (unit present/absent); command routing
  (service vs detached); idempotent `start`; `stop` routing (managed→manager,
  manual→IPC); `run` is foreground on both (no fork).
- **int (kitsubito/Linux systemd rig):** `start`/`stop` drive the unit; no
  double-daemon; `ensure_running` from a cold CLI brings the unit up, not a
  manual daemon.
- **Windows:** `start` detached + idempotent; `run` foreground (no background
  task); `stop` IPC.

## Fleet follow-up

- v0.2.0 already registered the systemd unit on kitsubito + the scheduled-task
  attempt on HFENDULEAM (HFENDULEAM's task needs an elevated re-run — still open).
- After this ships: re-roll to the fleet so `start`/`stop` drive the managed
  service everywhere; document the daemon-lifecycle UX in the CLI reference +
  CHANGELOG.

## Open decisions for next session

1. **Windows service model:** real Windows Service (parity) vs scheduled-task +
   detached-spawn (simplicity). Leaning (b) for v1, (a) as a follow-up.
2. **Service detection source of truth** (unit-file probe vs `systemctl` query;
   the Windows equivalent).
3. Confirm `start` graduates from alias → first-class verb (and whether the
   `start` alias added in `3392158` is replaced wholesale).
