diff --git a/docs/superpowers/specs/2026-07-31-xy-start-on-login-design.md b/docs/superpowers/specs/2026-07-31-xy-start-on-login-design.md index 65be295..deed339 100644 --- a/docs/superpowers/specs/2026-07-31-xy-start-on-login-design.md +++ b/docs/superpowers/specs/2026-07-31-xy-start-on-login-design.md @@ -80,7 +80,10 @@ XML, the daemon gains its own log file (see below). **Accepted limitation.** Failures occurring before the daemon's logger exists — missing binary, dyld error, malformed plist — appear in neither `xy.log` nor -`xy service status` beyond a bare `Stopped`. Documented recourse: +`xy service status`. This is worse than first assumed: `LaunchdServiceManager:: +status()` returns `ServiceStatus::Stopped(None)` unconditionally +(`launchd.rs:288`), so the `Option` reason carried by the enum is always +`None` on macOS and cannot be surfaced. Documented recourse: launchctl print gui/$UID/se.aceofba.xy @@ -128,10 +131,12 @@ Two new modules in `crates/xy`, Rust-2018 layout (`foo.rs` + `foo/`): | `restart_policy` | `Always { delay_secs: Some(10) }` | `KeepAlive` | | `username` | `None` — runs as the invoking user | — | -launchd collapses `RestartPolicy` to a `KeepAlive` boolean: `Never` maps to -false, `Always` and `OnFailure` both map to true, and `delay_secs` is ignored. -`Always` is chosen for the explicit `KeepAlive: true` it produces; the delay is -carried only so the value stays meaningful if this spec is extended to systemd. +Verified against `service-manager-0.11.0/src/launchd.rs`. `Always` emits +`KeepAlive: true` (a plain boolean); `OnFailure` instead emits a `KeepAlive` +*dictionary* with `SuccessfulExit: false`; `Never` omits the key. `delay_secs` +has no launchd equivalent and is discarded with a `log::warn!`. `Always` is +chosen deliberately: the daemon should come back regardless of how it exited. +`delay_secs` is set to `None`, since passing a value only produces a warning. The label is the reverse-DNS form of the `git.aceofba.se` remote. It lives on `AgentSpec` rather than being a constant so tests can install under a distinct @@ -164,6 +169,49 @@ want. Result: `~/.local/state/xy/logs/` becomes uniform — `xy.log` for the daemon, `.log` per supervised server, all rotated by the same code. +## launchd mechanics + +Verified by reading `service-manager-0.11.0/src/launchd.rs`. Three behaviours +constrain the CLI and are not obvious from the crate's public documentation. + +**`install()` deliberately produces a disabled agent.** Whenever `KeepAlive` is +present, `make_plist` also writes `Disabled: true` (`launchd.rs:440`) so that +`install()` never auto-starts, for cross-platform consistency. A `Disabled` +LaunchAgent does not start at login either, so **install alone does not deliver +start-on-login**. The crate's `start()` is what removes the `Disabled` key, +rewrites the plist, and reloads (`launchd.rs:179-194`). `xy service install` +therefore always calls `install()` *then* `start()`; after that the on-disk +plist is permanently free of `Disabled` and `RunAtLoad` works at next login. + +There is no way to opt out: setting `LaunchdInstallConfig::keep_alive = +Some(true)` still takes the `has_keep_alive` branch that writes `Disabled`. + +**The crate's `stop()` is unusable for our agent.** It runs `launchctl stop +