Supervision
Harness's supervision model is deliberately two-tier (ADR-0005):
init (systemd/launchd) ──supervises──▶ harness daemon ──supervises──▶ each harness
The daemon supervises harnesses; your init system supervises the daemon. This
keeps the tier that owns sensitive state (PTYs, scrollback, restart policy)
small and well-tested, and lets systemd/launchd be the crash recovery for
the daemon itself.
Running the daemon as a service
As a systemd --user unit:
# ~/.config/systemd/user/harness.service
[Unit]
Description=Harness agent supervisor
[Service]
ExecStart=%h/go/bin/harness daemon
Restart=on-failure
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now harness.service
Running the daemon as a service on macOS
On macOS the init tier is launchd, and the daemon runs as a LaunchAgent (owned by your user, started at login). The full equivalent of the systemd unit above:
<?xml version="1.0" encoding="UTF-8"?>
<!-- ~/Library/LaunchAgents/dev.harness.daemon.plist -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.harness.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/go/bin/harness</string>
<string>daemon</string>
</array>
<!-- systemd's Restart=on-failure analogue: relaunch on a non-zero exit.
launchd throttles restarts to at most once per 10s by default. -->
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>RunAtLoad</key>
<true/>
<!-- stdout/stderr of the daemon itself (not of harnesses — those are
owned by the daemon's own PTY + scrollback ring). -->
<key>StandardOutPath</key>
<string>/tmp/harness-daemon.log</string>
<key>StandardErrorPath</key>
<string>/tmp/harness-daemon.log</string>
</dict>
</plist>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.harness.daemon.plist
launchctl enable gui/$(id -u)/dev.harness.daemon
Day to day:
launchctl print gui/$(id -u)/dev.harness.daemon # status (the launchd 'journalctl')
tail -f /tmp/harness-daemon.log # daemon log
launchctl bootout gui/$(id -u)/dev.harness.daemon # stop (survives reboot of the plist)
Two launchd specifics worth knowing:
- No login shell, no environment. A LaunchAgent does not source your
~/.zshrc; if your harnesses need env vars (API tokens,PATHadditions), set them with anEnvironmentVariablesdict in the plist or load them from an env file the daemon layers per-harness (see the harnessenv_filefield in Configuration). - GUI session, not SSH sessions. LaunchAgents run inside your GUI login session. A daemon started this way is up while you are logged in at the console — for an always-on box that auto-logins, that is the same as always-on; for a headless Mac you want that auto-login or an SSH-started daemon instead.
What the daemon guarantees
- Kept alive: a harness configured
enabled = true(or part of an autostart profile) is brought up at daemon start and re-attachable thereafter. - Restart on exit: the
restartpolicy andrestart_delayin Configuration control whether and how fast a harness is brought back after it exits. - Provider failover is not supervision: the restart budget only helps when
the process crashes. A quota wall or provider outage leaves the harness
running— dead on the inside. See Configuration → Model routing for the mitigation. - State persistence (ADR-0007): the daemon persists intent to
state.json, restores it on boot, and re-attaches to intended running set regardless of how it restarted. - Daemon-owned scrollback (ADR-0007): each harness's PTY output is tee'd
into an in-memory ring (depth configurable with
harness daemon --scrollback N) plus a durable log, so you can read back output even while detached.
Flapping a.k.a. crash-loop protection
Under repeated quick restarts, the daemon escalates backoff so a crash-looping
harness can't burn the CPU. harness describe <name> surfaces this with a
flapping field and, when applicable, a restart to apply config prompt.
One-shot vs. supervised
For a quick session, run harness daemon in a terminal or --detach into the
background. That's fine for development. For anything you want to survive logout
or reboot, wire the unit above — a shell that closes is a supervision gap the
init service closes.