ref:main

feat(runner): tier-1 production follow-up — status/logs/doctor/--detach/multi-instance/launchctl-modern/runner_shutdown #28

merged colechristensen cole.christensen@gmail.com wants to merge feat/runner-production-ready into main

Summary

Lands the tier-1 production-ready follow-up work that PR #22 deferred, all in one PR per the "each PR is a release" convention.

  • `runner status` / `runner logs` / `runner doctor` subcommands
  • `runner start –detach` with size-based log rotation + new session (setsid via pre_exec defends the SIGHUP startup-race window)
  • launchctl bootstrap/bootout/print with fallback to legacy load/unload/list; idempotent across casing and EEXIST(17)
  • gui/ → user/ fallback for headless ssh
  • `–service-name ` everywhere; per-instance state at `~/.anvil-runner/services/.json`; per-instance PID files; `runner service list`
  • systemd ExecStart embeds `–pid-file` + `–parallel`; scope-aware `ProtectHome` (read-only for user, full for system+StateDirectory)
  • Atomic-rename PID file with drop-only-if-still-ours
  • `EndReason` (Finished / RunnerShutdown / Timeout) flows into the job-status update as `cancel_reason`, but only when the runner actually cancelled the job

122 tests pass; `cargo fmt && cargo clippy –all-targets – -D warnings` clean.

Closes #23.

Test plan

  • `anvil runner doctor` returns non-zero when the PID is stale
  • `anvil runner doctor –json` machine-readable on success and failure
  • `anvil runner start –detach` survives `exit` on the launching terminal; `runner logs` tails the same file
  • `anvil runner service install –service-name gpu1` co-installs next to a `default` install without colliding on PID file or unit
  • `anvil runner service start –service-name gpu1` survives ssh logout
  • `anvil runner stop –service-name gpu1` only stops the named instance
  • On macOS: bootstrap fails over from gui/ to user/ on a headless ssh login
  • Server receives `cancel_reason: runner_shutdown` when the runner is drained mid-job; does NOT receive cancel_reason on a job that fails normally or times out

🤖 Generated with Claude Code

Created Jun 05, 2026 at 01:56 UTC | Merged Jun 07, 2026 at 00:41 UTC by colechristensen cole.christensen@gmail.com