ADR-0005 — Supervision: daemon self-supervises harnesses; init only supervises the daemon
- Status: Proposed
- Date: 2026-07-18
Context and problem statement
Today supervision is per harness: each is a systemd --user unit (or launchd
plist) whose ExecStart is harness-run <name>, and a while true; …; sleep
loop inside the tmux pane restarts the command on crash. Two layers (init + pane
loop) keep one harness alive.
With a resident daemon (📝 ADR-0002) that owns every PTY, this arrangement no longer fits: the daemon, not systemd, is now the thing spawning and watching harness processes. So: who restarts a crashed harness, and who restarts the daemon?
Decision drivers
- A crashed harness must come back automatically, honoring
restart_delayand not hot-looping on a command that instantly fails. - The daemon itself must survive logout and come back on boot/crash.
- Keep the boot/login integration familiar (systemd
--useron Linux, launchd on macOS) — the current design already does this well and Joe's infra expects it. - Don't create a supervision hall-of-mirrors (init watching a loop watching a loop).
Considered options
- Daemon supervises harnesses; init supervises only the daemon. ✅
- Keep per-harness init units (each harness still a systemd/launchd unit that somehow registers with the daemon).
- Daemon supervises everything including itself (double-fork, PID file, its own boot hook), no systemd/launchd.
Decision outcome
Chosen: Option 1 — two clean layers.
Layer 1 — the daemon supervises harnesses (in-process)
Each running harness has a supervisor goroutine:
- Spawn
cmd argsunder a PTY inworkdir, withenv_fileloaded (📝 ADR-0008). - On exit, record exit code + timestamp, transition the state machine
(spec-harness-lifecycle.md), and if the harness is enabled, restart after
restart_delay. - Crash-loop backoff: if a harness exits "too fast, too often" (e.g. N exits
within a window), escalate
restart_delay(capped exponential) and mark the harness degraded/flapping in the UI rather than silently thrashing. This is a strict improvement over today's fixedsleep $HR_DELAYloop. restart_delaysemantics from the current TOML are preserved.
This replaces both today's per-harness init unit and the in-pane
while true loop with one well-instrumented supervisor the UI can actually see
into (last exit code, restart count, flapping state).
Layer 2 — init supervises the daemon (one unit)
A single long-lived service:
- Linux: a systemd
--userunitharnessd.service—ExecStart=harness daemon,Restart=on-failure,RestartSec=…, the samePATH=%h/.local/bin:…fix the current template carries (so agent CLIs finduv/npx/go).WantedBy=default.target.loginctl enable-linger $USERto keep it running while logged out — unchanged guidance from today. - macOS: a single launchd LaunchAgent
rocks.stump.harnessdwithRunAtLoad/KeepAlive— replacing the per-harness plists the plugin generates today.
The template drops from N per-harness units to one. harness (the client)
never needs systemd/launchd — it just connects to the socket.
Harness enable/disable ≠ init
"Enabled on boot" moves from systemd enablement to daemon state: a harness
marked enabled (or belonging to an autostart profile — 📝 ADR-0006) is started by
the daemon when the daemon starts. harness start/stop talk to the daemon, not to
systemctl. This is simpler and uniform across Linux/macOS (today the code branches
systemctl vs launchctl everywhere).
Consequences
Positive
- One init unit total; the daemon owns harness lifecycle with visibility (flapping detection, restart counts, last exit) the shell loop never had.
- Uniform cross-platform behavior — the OS-specific branching collapses to "how do we keep one daemon alive," which systemd/launchd each do well.
- Boot/login autostart stays in the familiar systemd/launchd idiom Joe's infra already uses.
Negative / costs
- Blast radius: if
harnessdcrashes, every harness dies with it (📝 ADR-0002 raised this). Mitigations: (a) keep the daemon small and heavily tested; (b)Restart=on-failurebrings it right back and it restarts enabled harnesses; (c) optional hardening — spawn harnesses in their own process group / session so a daemon crash can leave them orphaned-but-alive and let the restarted daemon re-adopt them (advanced; a v2 consideration, noted not committed). - Losing per-harness systemd units means losing
journalctl --user -u harness@foomuscle memory; we replace it withharness logs foo(daemon-served, 📝 ADR-0007) and can also emit to the journal/syslog if we want that back. - The daemon must persist "which harnesses were enabled" so a restart restores the right set (📝 ADR-0007).
Rejected options
- Per-harness init units (Opt 2) — recreates today's N-units sprawl and splits authority: init thinks it owns the process, but the daemon owns the PTY. Two bosses. Rejected.
- Daemon supervises itself, no systemd/launchd (Opt 3) — we'd hand-roll daemonization, PID files, and boot hooks that systemd/launchd already do better and that Joe's environment already standardizes on. Rejected.
Related
📝 ADR-0002 (daemon owns processes), 📝 ADR-0006 (autostart profiles), 📝 ADR-0007 (persisting enabled-state + logs), spec-harness-lifecycle.md.
Related Artifacts
Direct relationships declared in YAML frontmatter (per ADR-0023 / SPEC-0018). Run /sdd:graph chain ADR-0005 for the transitive view.