Keeping fjx supervise alive

fjx supervise start <project-dirs...> is a long-running daemon. It drains in-flight ticks on SIGTERM and reloads each project's fjx.json on SIGHUP (see supervise). For unattended operation it needs a process supervisor that restarts it on crash and on host reboot.

Three concrete recipes follow. Pick the one that matches the host. Replace /srv/fjx/projects/{a,b} with the actual project directories and fjx with the absolute path to the binary (which fjx).

systemd (Linux)

User-level unit — runs as the invoking user without root. Drop into ~/.config/systemd/user/fjx-supervise.service:

[Unit]
Description=fjx supervisor
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/fjx supervise start /srv/fjx/projects/a /srv/fjx/projects/b
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
KillSignal=SIGTERM
TimeoutStopSec=120s
Environment=FJX_SUPERVISE_SOCK=%t/fjx-supervise.sock

[Install]
WantedBy=default.target

Enable and start:

systemctl --user daemon-reload
systemctl --user enable --now fjx-supervise.service
loginctl enable-linger $USER   # survive logout
journalctl --user -u fjx-supervise -f

systemctl --user reload fjx-supervise triggers SIGHUP — the daemon re-reads each project's fjx.json without restarting (see fjx.json). TimeoutStopSec=120s gives in-flight ticks room to drain before systemd escalates to SIGKILL; raise it if --tick-timeout is higher.

launchd (macOS)

User agent — drop into ~/Library/LaunchAgents/net.tfks.fjx-supervise.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!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>net.tfks.fjx-supervise</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/fjx</string>
    <string>supervise</string>
    <string>start</string>
    <string>/Users/me/fjx/projects/a</string>
    <string>/Users/me/fjx/projects/b</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key>
  <dict><key>SuccessfulExit</key><false/></dict>
  <key>StandardOutPath</key><string>/Users/me/Library/Logs/fjx-supervise.log</string>
  <key>StandardErrorPath</key><string>/Users/me/Library/Logs/fjx-supervise.log</string>
</dict>
</plist>

Load and inspect:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/net.tfks.fjx-supervise.plist
launchctl print gui/$(id -u)/net.tfks.fjx-supervise
tail -f ~/Library/Logs/fjx-supervise.log

launchd has no "reload" verb. Send SIGHUP directly to pick up fjx.json changes:

kill -HUP $(launchctl print gui/$(id -u)/net.tfks.fjx-supervise | awk '/pid =/ {print $3}')

Or use the control socket: fjx supervise reload.

tmux (development, ad-hoc hosts)

When systemd or launchd isn't available — a shared dev VM, a remote box accessed only over SSH — a detached tmux session is the lowest-ceremony option. It does not survive a host reboot; pair it with an @reboot cron entry or just re-launch by hand.

tmux new-session -d -s fjx-supervise \
  'fjx supervise start /srv/fjx/projects/a /srv/fjx/projects/b 2>&1 | tee -a ~/fjx-supervise.log'

tmux attach -t fjx-supervise        # watch it
tmux send-keys -t fjx-supervise C-c # graceful stop (SIGINT drains like SIGTERM)

For auto-restart on crash, wrap the command in a while true loop with a short sleep — but at that point systemd/launchd is the better tool.

Control socket

The daemon exposes a Unix socket for the status, pause, resume, kill, and reload subcommands. Default path is $XDG_RUNTIME_DIR/fjx-supervise-$USER.sock, overridable with FJX_SUPERVISE_SOCK or --socket. Pin the same path in the service file and in operator shell sessions so fjx supervise status finds the running daemon.

Verifying

After bringing the supervisor up, sanity-check from another shell:

fjx supervise status      # lists in-flight ticks; empty list is fine on a cold start
fjx supervise reload      # round-trip the control socket

If status errors with "connection refused", the daemon isn't running or FJX_SUPERVISE_SOCK doesn't match.