# Start on Login (macOS) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Give `xy` a `service` subcommand that installs a user-level launchd agent so the daemon starts at login and restarts on crash. **Architecture:** A new `service.rs` in the `xy` crate wraps the `service-manager` crate's `LaunchdServiceManager::user()` and returns plain data; a new `cli/service.rs` renders that data and maps it to exit codes. Separately, the daemon gains its own rotating log file so login-time failures are diagnosable. Nothing crosses the IPC boundary — `xy service *` talks to launchd directly, which is what makes it work when the daemon is dead. **Tech Stack:** Rust 2024, `service-manager` 0.11, `clap` derive, `tracing-subscriber`, `etcetera`, existing `xy_supervisor::logs::RotatingLogWriter`. **Spec:** `docs/superpowers/specs/2026-07-31-xy-start-on-login-design.md` ## Global Constraints - Format with `cargo +nightly fmt` before every commit. Never `cargo fmt`. - Fix all `cargo clippy` warnings before moving to the next task. - Run tests with `cargo nextest run`, not `cargo test`. - TDD: write the failing test, run it, confirm it fails for the expected reason, then write the minimal implementation. - No `mod.rs` for new modules — `foo.rs` module root plus a `foo/` directory for submodules. - All `#[cfg(test)] mod tests` blocks go at the END of the source file. - Naming: short but descriptive. `err` not `e`, `result` not `r`. Single chars only for loop indices (`i`, `j`, `k`) and counts (`n`). - Blank line after every statement by default; multi-line `let` bindings ALWAYS get a blank line after. Adjacent asserts stay grouped with no blanks between them. - No comments unless the *why* is non-obvious. - `pub(crate)` for internals; `pub` only at a crate's API boundary. - The agent label is `se.aceofba.xy`. The test label is `se.aceofba.xy-test`. - Exit codes: `0` success, `1` operational error. Codes `2` and `3` are not used by `xy service`. ## File Structure | File | Responsibility | |---|---| | `Cargo.toml` (workspace) | Declare `service-manager = "0.11"` in `[workspace.dependencies]` | | `crates/xy/Cargo.toml` | Consume `service-manager` | | `crates/xy-supervisor/src/logs.rs` | *Modify:* add `impl std::io::Write for RotatingLogWriter` | | `crates/xy/src/logging.rs` | *New:* build the daemon's tee'd subscriber writer | | `crates/xy/src/service.rs` | *New:* `AgentSpec`, `AgentStatus`, launchd install/uninstall/start/stop/status. Returns data, never prints | | `crates/xy/src/cli/service.rs` | *New:* verb dispatch, human output, exit codes | | `crates/xy/src/cli/mod.rs` | *Modify:* declare `mod service;` and re-export | | `crates/xy/src/main.rs` | *Modify:* `Service` subcommand arm; reorder path resolution before logger init | | `crates/xy/src/daemon/mod.rs` | *Modify:* drop the now-duplicated `ensure_dirs()` call | | `README.md` | *Modify:* document the five verbs | No integration test is planned. Driving a real launchd cycle would either hard-code the live `se.aceofba.xy` label and risk clobbering a working installation, or require a test-only `--label` flag on the CLI. The load/unload mechanism is covered instead by the *Manual acceptance* section at the end of this plan, which is the only check that can actually prove `RunAtLoad` works. --- ### Task 1: `io::Write` for `RotatingLogWriter` `RotatingLogWriter` already tracks bytes written and rotates, but only exposes `write_line(tag, line)`, which prefixes a tag the daemon's own log must not have. An `io::Write` impl makes the type usable as a `tracing` writer. **Files:** - Modify: `crates/xy-supervisor/src/logs.rs` - Test: `crates/xy-supervisor/src/logs.rs` (existing `#[cfg(test)] mod tests` at end of file) **Interfaces:** - Consumes: nothing. - Produces: `impl std::io::Write for RotatingLogWriter`, with `write(&mut self, buf: &[u8]) -> io::Result` and `flush(&mut self) -> io::Result<()>`. Task 2 depends on this. - [ ] **Step 1: Write the failing tests** Append to the `#[cfg(test)] mod tests` block at the end of `crates/xy-supervisor/src/logs.rs`: ```rust #[test] fn write_trait_appends_bytes() { use std::io::Write; let tmp = tempfile::tempdir().unwrap(); let base = tmp.path().join("daemon.log"); let mut writer = RotatingLogWriter::open(&base, 1024, 3).unwrap(); writer.write_all(b"hello\n").unwrap(); writer.flush().unwrap(); let contents = std::fs::read_to_string(&base).unwrap(); assert_eq!(contents, "hello\n"); } #[test] fn write_trait_rotates_at_threshold() { use std::io::Write; let tmp = tempfile::tempdir().unwrap(); let base = tmp.path().join("daemon.log"); let mut writer = RotatingLogWriter::open(&base, 8, 3).unwrap(); writer.write_all(b"0123456789").unwrap(); writer.write_all(b"after\n").unwrap(); writer.flush().unwrap(); let rotated = tmp.path().join("daemon.log.1"); assert!(rotated.exists()); assert_eq!(std::fs::read_to_string(&rotated).unwrap(), "0123456789"); assert_eq!(std::fs::read_to_string(&base).unwrap(), "after\n"); } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo nextest run -p xy-supervisor -E 'test(/write_trait/)'` Expected: FAIL to compile, with `no method named 'write_all' found for struct 'RotatingLogWriter'`. - [ ] **Step 3: Write the minimal implementation** Add to `crates/xy-supervisor/src/logs.rs`, after the existing `impl RotatingLogWriter` block. Note `Write` is already imported at the top of the file (`use std::io::Write;`). ```rust impl std::io::Write for RotatingLogWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.file.write_all(buf)?; self.written += buf.len() as u64; if self.written >= self.max_bytes { self.rotate()?; } Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { self.file.flush() } } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `cargo nextest run -p xy-supervisor -E 'test(/write_trait/)'` Expected: PASS, 2 tests. - [ ] **Step 5: Verify nothing else broke, format, lint** ```bash cargo nextest run -p xy-supervisor cargo +nightly fmt cargo clippy -p xy-supervisor ``` Expected: all tests pass, no clippy warnings. - [ ] **Step 6: Commit** ```bash git add crates/xy-supervisor/src/logs.rs git commit -m "feat(logs): impl io::Write for RotatingLogWriter" ``` --- ### Task 2: Daemon log file The daemon currently logs only to stderr, which launchd discards. Give it `log_dir/xy.log` using the same rotation as per-server logs, and reorder `main.rs` so paths resolve before the logger is built. **Files:** - Create: `crates/xy/src/logging.rs` - Modify: `crates/xy/src/main.rs` - Modify: `crates/xy/src/daemon/mod.rs` (remove the duplicated `ensure_dirs()` call) - Test: `crates/xy/src/logging.rs` **Interfaces:** - Consumes: `impl io::Write for RotatingLogWriter` from Task 1. - Produces: `pub(crate) fn daemon_writer(log_dir: &Path) -> std::io::Result>>`. No later task depends on this. `tracing-subscriber` 0.3 already implements `MakeWriter` for `Mutex where W: io::Write` and for `Arc`, so `Arc>` satisfies `with_writer` directly. No adapter type is needed. - [ ] **Step 1: Write the failing test** Create `crates/xy/src/logging.rs` containing only the test module: ```rust #[cfg(test)] mod tests { use super::*; #[test] fn daemon_writer_creates_and_appends_to_xy_log() { use std::io::Write; let tmp = tempfile::tempdir().unwrap(); let writer = daemon_writer(tmp.path()).unwrap(); writer.lock().unwrap().write_all(b"line\n").unwrap(); let contents = std::fs::read_to_string(tmp.path().join("xy.log")).unwrap(); assert_eq!(contents, "line\n"); } } ``` Declare the module in `crates/xy/src/main.rs` alongside the existing `mod` declarations: ```rust mod logging; ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo nextest run -p xy -E 'test(/daemon_writer/)'` Expected: FAIL to compile, with `cannot find function 'daemon_writer' in this scope`. - [ ] **Step 3: Write the minimal implementation** Prepend to `crates/xy/src/logging.rs`, above the test module: ```rust use std::path::Path; use std::sync::{Arc, Mutex}; use xy_supervisor::logs::RotatingLogWriter; const LOG_FILE_MAX_BYTES: u64 = 10 * 1024 * 1024; const LOG_FILE_KEEP: usize = 5; pub(crate) fn daemon_writer(log_dir: &Path) -> std::io::Result>> { let writer = RotatingLogWriter::open(&log_dir.join("xy.log"), LOG_FILE_MAX_BYTES, LOG_FILE_KEEP)?; Ok(Arc::new(Mutex::new(writer))) } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo nextest run -p xy -E 'test(/daemon_writer/)'` Expected: PASS. - [ ] **Step 5: Reorder `main.rs` so paths resolve before the logger** Replace the top of `main` in `crates/xy/src/main.rs`. The current body initialises `tracing_subscriber` first and resolves paths second; this inverts that order and tees the daemon's output to a file. ```rust #[tokio::main] async fn main() -> std::process::ExitCode { let cli = Cli::parse(); let paths = match paths::Paths::resolve() { Ok(p) => p, Err(err) => { eprintln!("xy: failed to resolve XDG paths: {err}"); return std::process::ExitCode::from(3); } }; let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); if matches!(cli.cmd, Cmd::Daemon) { use tracing_subscriber::fmt::writer::MakeWriterExt; if let Err(err) = paths.ensure_dirs() { eprintln!("xy: failed to create state dirs: {err}"); return std::process::ExitCode::from(3); } let file = match logging::daemon_writer(&paths.log_dir) { Ok(w) => w, Err(err) => { eprintln!("xy: failed to open daemon log: {err}"); return std::process::ExitCode::from(3); } }; tracing_subscriber::fmt() .with_env_filter(filter) .with_ansi(false) .with_writer(std::io::stderr.and(file)) .init(); } else { tracing_subscriber::fmt() .with_env_filter(filter) .with_writer(std::io::stderr) .init(); } // ... existing `let result: anyhow::Result = match cli.cmd { ... }` unchanged } ``` `with_ansi(false)` is set on the daemon path because the tee writes the same bytes to both sinks, and escape codes in a log file are noise. - [ ] **Step 6: Remove the now-duplicated `ensure_dirs()` call** In `crates/xy/src/daemon/mod.rs`, delete the first line of `run`: ```rust paths.ensure_dirs().context("create state dirs")?; ``` `main` now does this before the logger is built, which is the only ordering that lets the logger open a file inside `log_dir`. - [ ] **Step 7: Verify the daemon actually writes its log** ```bash cargo build -p xy rm -f ~/.local/state/xy/logs/xy.log ./target/debug/xy daemon & sleep 2 cat ~/.local/state/xy/logs/xy.log kill %1 ``` Expected: `xy.log` exists and contains a `daemon listening` line. - [ ] **Step 8: Run the full suite, format, lint** ```bash cargo nextest run cargo +nightly fmt cargo clippy --workspace ``` Expected: all tests pass, no clippy warnings. - [ ] **Step 9: Commit** ```bash git add crates/xy/src/logging.rs crates/xy/src/main.rs crates/xy/src/daemon/mod.rs git commit -m "feat(daemon): write rotating xy.log alongside stderr" ``` --- ### Task 3: `AgentSpec` and install/uninstall The OS-facing core. `AgentSpec` is the single source of truth for what gets installed; it converts into `service-manager`'s `ServiceInstallCtx`. **Files:** - Modify: `Cargo.toml` (workspace) - Modify: `crates/xy/Cargo.toml` - Create: `crates/xy/src/service.rs` - Modify: `crates/xy/src/main.rs` (declare `mod service;`) - Test: `crates/xy/src/service.rs` **Interfaces:** - Consumes: nothing from earlier tasks. - Produces, all `pub(crate)`: - `const DEFAULT_LABEL: &str = "se.aceofba.xy"` - `struct AgentSpec { label: String, program: PathBuf, args: Vec, path_env: String, working_dir: PathBuf }` - `fn AgentSpec::for_current_exe() -> anyhow::Result` - `fn AgentSpec::install_ctx(&self) -> anyhow::Result` - `fn AgentSpec::plist_path(&self) -> anyhow::Result` - `fn plist_path_for(label: &str) -> anyhow::Result` - `fn is_build_tree_path(path: &Path) -> bool` - `fn ensure_supported() -> anyhow::Result<()>` - `fn install(spec: &AgentSpec) -> anyhow::Result<()>` - `fn uninstall(label: &str) -> anyhow::Result<()>` Task 4 adds status/start/stop to this same file; Task 5 consumes all of it. - [ ] **Step 1: Add the dependency** ```bash cargo add --package xy service-manager@0.11 ``` Then move the version to the workspace, matching the existing convention. In the root `Cargo.toml` under `[workspace.dependencies]` add: ```toml service-manager = "0.11" ``` And in `crates/xy/Cargo.toml` under `[dependencies]` set: ```toml service-manager = { workspace = true } ``` - [ ] **Step 2: Write the failing tests** Create `crates/xy/src/service.rs` containing only the test module: ```rust #[cfg(test)] mod tests { use super::*; fn sample_spec() -> AgentSpec { AgentSpec { label: "se.aceofba.xy-test".to_string(), program: PathBuf::from("/usr/local/bin/xy"), args: vec!["daemon".to_string()], path_env: "/usr/local/bin:/usr/bin".to_string(), working_dir: PathBuf::from("/Users/someone"), } } #[test] fn install_ctx_maps_every_field() { let ctx = sample_spec().install_ctx().unwrap(); assert_eq!(ctx.label.to_qualified_name(), "se.aceofba.xy-test"); assert_eq!(ctx.program, PathBuf::from("/usr/local/bin/xy")); assert_eq!(ctx.args, vec![std::ffi::OsString::from("daemon")]); assert_eq!(ctx.working_directory, Some(PathBuf::from("/Users/someone"))); assert!(ctx.autostart); assert_eq!( ctx.environment, Some(vec![("PATH".to_string(), "/usr/local/bin:/usr/bin".to_string())]) ); } #[test] fn install_ctx_uses_always_restart_without_delay() { let ctx = sample_spec().install_ctx().unwrap(); assert!(matches!( ctx.restart_policy, RestartPolicy::Always { delay_secs: None } )); } #[test] fn install_ctx_supplies_no_raw_contents() { let ctx = sample_spec().install_ctx().unwrap(); assert!(ctx.contents.is_none()); assert!(ctx.username.is_none()); } #[test] fn build_tree_paths_are_detected() { assert!(is_build_tree_path(Path::new("/home/me/xy/target/debug/xy"))); assert!(is_build_tree_path(Path::new("/home/me/xy/target/release/xy"))); } #[test] fn installed_paths_are_not_build_tree_paths() { assert!(!is_build_tree_path(Path::new("/Users/me/.cargo/bin/xy"))); assert!(!is_build_tree_path(Path::new("/usr/local/bin/xy"))); assert!(!is_build_tree_path(Path::new("/opt/targeted/bin/xy"))); } #[test] fn plist_path_sits_in_user_launch_agents() { let path = sample_spec().plist_path().unwrap(); assert!(path.ends_with("Library/LaunchAgents/se.aceofba.xy-test.plist")); } #[test] #[cfg(target_os = "macos")] fn macos_is_supported() { assert!(ensure_supported().is_ok()); } #[test] #[cfg(not(target_os = "macos"))] fn other_platforms_are_rejected() { let err = ensure_supported().unwrap_err().to_string(); assert!(err.contains("macOS-only")); } } ``` The last case in `installed_paths_are_not_build_tree_paths` is deliberate: a naive `to_string_lossy().contains("target/")` would match `/opt/targeted/bin/xy`. The implementation must compare whole path components. Declare the module in `crates/xy/src/main.rs`: ```rust mod service; ``` - [ ] **Step 3: Run tests to verify they fail** Run: `cargo nextest run -p xy -E 'test(/service::/)'` Expected: FAIL to compile, with `cannot find struct 'AgentSpec' in this scope`. - [ ] **Step 4: Write the minimal implementation** Prepend to `crates/xy/src/service.rs`, above the test module: ```rust use anyhow::{Context, Result}; use service_manager::{ LaunchdServiceManager, RestartPolicy, ServiceInstallCtx, ServiceLabel, ServiceManager, ServiceUninstallCtx, }; use std::path::{Component, Path, PathBuf}; pub(crate) const DEFAULT_LABEL: &str = "se.aceofba.xy"; pub(crate) struct AgentSpec { pub label: String, pub program: PathBuf, pub args: Vec, pub path_env: String, pub working_dir: PathBuf, } impl AgentSpec { pub fn for_current_exe() -> Result { let program = std::env::current_exe() .context("resolve current executable")? .canonicalize() .context("canonicalize current executable")?; let path_env = std::env::var("PATH").context("read PATH")?; let working_dir = etcetera::home_dir().context("locate home directory")?; Ok(Self { label: DEFAULT_LABEL.to_string(), program, args: vec!["daemon".to_string()], path_env, working_dir, }) } pub fn install_ctx(&self) -> Result { let label: ServiceLabel = self.label.parse().context("parse service label")?; Ok(ServiceInstallCtx { label, program: self.program.clone(), args: self.args.iter().map(std::ffi::OsString::from).collect(), contents: None, username: None, working_directory: Some(self.working_dir.clone()), environment: Some(vec![("PATH".to_string(), self.path_env.clone())]), autostart: true, restart_policy: RestartPolicy::Always { delay_secs: None }, }) } pub fn plist_path(&self) -> Result { plist_path_for(&self.label) } } pub(crate) fn plist_path_for(label: &str) -> Result { let home = etcetera::home_dir().context("locate home directory")?; Ok(home .join("Library") .join("LaunchAgents") .join(format!("{label}.plist"))) } pub(crate) fn is_build_tree_path(path: &Path) -> bool { let mut components = path.components().peekable(); while let Some(component) = components.next() { if component != Component::Normal("target".as_ref()) { continue; } if matches!( components.peek(), Some(Component::Normal(next)) if *next == std::ffi::OsStr::new("debug") || *next == std::ffi::OsStr::new("release") ) { return true; } } false } #[cfg(target_os = "macos")] pub(crate) fn ensure_supported() -> Result<()> { Ok(()) } #[cfg(not(target_os = "macos"))] pub(crate) fn ensure_supported() -> Result<()> { anyhow::bail!("start-on-login is macOS-only for now") } fn manager() -> LaunchdServiceManager { LaunchdServiceManager::user() } pub(crate) fn install(spec: &AgentSpec) -> Result<()> { manager() .install(spec.install_ctx()?) .context("install launchd agent") } pub(crate) fn uninstall(label: &str) -> Result<()> { let label: ServiceLabel = label.parse().context("parse service label")?; manager() .uninstall(ServiceUninstallCtx { label }) .context("uninstall launchd agent") } ``` - [ ] **Step 5: Run tests to verify they pass** Run: `cargo nextest run -p xy -E 'test(/service::/)'` Expected: PASS, 7 tests (six platform-independent, plus `macos_is_supported`). - [ ] **Step 6: Format and lint** ```bash cargo +nightly fmt cargo clippy -p xy ``` Expected: no warnings. `dead_code` warnings for `install`/`uninstall` are expected until Task 5 wires them; if clippy flags them, add `#![allow(dead_code)]` at the top of `service.rs` and REMOVE it in Task 5. - [ ] **Step 7: Commit** ```bash git add Cargo.toml Cargo.lock crates/xy/Cargo.toml crates/xy/src/service.rs crates/xy/src/main.rs git commit -m "feat(service): AgentSpec and launchd install/uninstall" ``` --- ### Task 4: Status, start, and stop `service-manager`'s `stop()` runs `launchctl stop`, which a `KeepAlive: true` agent survives, so start/stop are implemented directly against `launchctl` on the plist path. Status comes from the crate. **Files:** - Modify: `crates/xy/src/service.rs` - Test: `crates/xy/src/service.rs` **Interfaces:** - Consumes: `AgentSpec`, `plist_path_for`, `manager` from Task 3. - Produces, all `pub(crate)`: - `enum AgentState { NotInstalled, Stopped, Running }` - `struct AgentStatus { label: String, plist: PathBuf, state: AgentState, program: Option, path_env: Option, snapshotted: Option, pid: Option }` - `fn status(label: &str, pidfile: &Path) -> anyhow::Result` - `fn start(label: &str) -> anyhow::Result<()>` - `fn stop(label: &str) -> anyhow::Result<()>` - `fn read_pid(pidfile: &Path) -> Option` - [ ] **Step 1: Write the failing tests** Add to the `#[cfg(test)] mod tests` block in `crates/xy/src/service.rs`: ```rust #[test] fn status_reports_not_installed_when_plist_is_absent() { let tmp = tempfile::tempdir().unwrap(); let status = status("se.aceofba.xy-absent", &tmp.path().join("xy.pid")).unwrap(); assert!(matches!(status.state, AgentState::NotInstalled)); assert!(status.program.is_none()); assert!(status.pid.is_none()); } #[test] fn read_pid_parses_a_pidfile() { let tmp = tempfile::tempdir().unwrap(); let pidfile = tmp.path().join("xy.pid"); std::fs::write(&pidfile, "4821\n").unwrap(); assert_eq!(read_pid(&pidfile), Some(4821)); } #[test] fn read_pid_returns_none_for_missing_or_garbage() { let tmp = tempfile::tempdir().unwrap(); let missing = tmp.path().join("nope.pid"); let garbage = tmp.path().join("garbage.pid"); std::fs::write(&garbage, "not-a-pid").unwrap(); assert_eq!(read_pid(&missing), None); assert_eq!(read_pid(&garbage), None); } ``` `status_reports_not_installed_when_plist_is_absent` relies on the plist file being the authority, so it does not shell out to `launchctl` and is safe in CI. - [ ] **Step 2: Run tests to verify they fail** Run: `cargo nextest run -p xy -E 'test(/service::/)'` Expected: FAIL to compile, with `cannot find function 'status' in this scope`. - [ ] **Step 3: Write the minimal implementation** Add to `crates/xy/src/service.rs`, above the test module. Extend the existing `use service_manager::{...}` line to also import `ServiceStatus` and `ServiceStatusCtx`. ```rust use std::time::SystemTime; #[derive(Debug, PartialEq, Eq)] pub(crate) enum AgentState { NotInstalled, Stopped, Running, } pub(crate) struct AgentStatus { pub label: String, pub plist: PathBuf, pub state: AgentState, pub program: Option, pub path_env: Option, pub snapshotted: Option, pub pid: Option, } pub(crate) fn read_pid(pidfile: &Path) -> Option { std::fs::read_to_string(pidfile) .ok()? .trim() .parse::() .ok() } pub(crate) fn status(label: &str, pidfile: &Path) -> Result { let plist = plist_path_for(label)?; if !plist.exists() { return Ok(AgentStatus { label: label.to_string(), plist, state: AgentState::NotInstalled, program: None, path_env: None, snapshotted: None, pid: None, }); } let snapshotted = std::fs::metadata(&plist).and_then(|meta| meta.modified()).ok(); let parsed: ServiceLabel = label.parse().context("parse service label")?; let state = match manager().status(ServiceStatusCtx { label: parsed })? { ServiceStatus::Running => AgentState::Running, ServiceStatus::Stopped(_) => AgentState::Stopped, ServiceStatus::NotInstalled => AgentState::NotInstalled, }; let pid = if state == AgentState::Running { read_pid(pidfile) } else { None }; let (program, path_env) = read_plist_fields(&plist); Ok(AgentStatus { label: label.to_string(), plist, state, program, path_env, snapshotted, pid, }) } fn read_plist_fields(plist: &Path) -> (Option, Option) { let Ok(contents) = std::fs::read_to_string(plist) else { return (None, None); }; let program = contents .split("ProgramArguments") .nth(1) .and_then(|rest| rest.split("").nth(1)) .and_then(|rest| rest.split("").next()) .map(PathBuf::from); let path_env = contents .split("PATH") .nth(1) .and_then(|rest| rest.split("").nth(1)) .and_then(|rest| rest.split("").next()) .map(str::to_string); (program, path_env) } fn launchctl(verb: &str, plist: &Path) -> Result<()> { let output = std::process::Command::new("launchctl") .arg(verb) .arg(plist) .output() .with_context(|| format!("run launchctl {verb}"))?; if !output.status.success() { anyhow::bail!( "launchctl {verb} failed: {}", String::from_utf8_lossy(&output.stderr).trim() ); } Ok(()) } pub(crate) fn start(label: &str) -> Result<()> { launchctl("load", &plist_path_for(label)?) } pub(crate) fn stop(label: &str) -> Result<()> { launchctl("unload", &plist_path_for(label)?) } ``` `read_plist_fields` does string slicing rather than pulling in the `plist` crate as a direct dependency; it reads two well-known keys from a file this program wrote, and returns `None` rather than failing when the shape is unexpected. - [ ] **Step 4: Run tests to verify they pass** Run: `cargo nextest run -p xy -E 'test(/service::/)'` Expected: PASS, 9 tests. - [ ] **Step 5: Verify the `launchctl print` risk empirically** The spec flags this: `service-manager`'s `status()` calls `launchctl print `, but user agents normally need `gui/$UID/