feat(agent-queue): macOS LaunchAgent boot-persistence (auto-start + KeepAlive)

Adds agent-queue-boot.sh (PATH repair + ~/.agent-queue.env overrides + caffeinate
wrap) and launchd/ (install.sh + README) so the run loop auto-starts on login and
survives reboot/crash — the persistence layer tmux+caffeinate alone cannot give.
No secrets tracked (host config lives in untracked ~/.agent-queue.env).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-06-01 00:25:16 -07:00
parent f7999fb11b
commit d574f5dda3
3 changed files with 223 additions and 0 deletions

48
agent-queue/agent-queue-boot.sh Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
#
# agent-queue-boot.sh — boot/login entrypoint for the agent-queue run loop.
#
# Launched by the macOS LaunchAgent (see launchd/) so the folder-kanban worker
# auto-starts on login AND survives reboot/crash (LaunchAgent KeepAlive). This is
# the reboot-persistence layer that tmux + caffeinate alone cannot provide.
#
# It does three things launchd's minimal environment needs:
# 1. Repairs PATH so the agent CLIs (codex/devin/claude) + caffeinate are found.
# 2. Loads optional overrides from ~/.agent-queue.env.
# 3. Wraps `agent-queue run` in caffeinate (macOS) so the Mac won't sleep while
# a job is running. NOTE: because the run loop is long-lived, this keeps the
# machine awake for as long as the LaunchAgent runs — intended for a dedicated
# overnight runner. Set AGENT_QUEUE_NO_CAFFEINATE=1 to allow idle sleep.
#
set -uo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
# launchd hands processes a bare PATH — prepend the usual CLI install locations
# (Homebrew arm64/intel, ~/.local/bin for devin, system dirs) ahead of it.
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
# Optional per-machine overrides (engine, concurrency, tokens, NETWORK, etc.).
# This file is NOT tracked — keep secrets/host-specific config here.
if [ -f "$HOME/.agent-queue.env" ]; then
# shellcheck disable=SC1091
. "$HOME/.agent-queue.env"
fi
# Recommended default for a local monorepo overnight runner (see long-running-jobs
# cheat sheet): codex runs in-repo so @bytelyst/* workspace links resolve locally.
: "${AGENT_QUEUE_ENGINE:=codex}"
export AGENT_QUEUE_ENGINE
echo "[agent-queue-boot] $(date '+%Y-%m-%d %H:%M:%S') starting run loop" \
"(engine=$AGENT_QUEUE_ENGINE, max=${AGENT_QUEUE_MAX:-3})"
# Keep the Mac awake for the lifetime of the loop unless explicitly opted out.
keep=""
if [ "${AGENT_QUEUE_NO_CAFFEINATE:-0}" != "1" ] && command -v caffeinate >/dev/null 2>&1; then
keep="caffeinate -dimsu"
fi
# exec so the LaunchAgent tracks the real worker PID (clean KeepAlive restarts).
# shellcheck disable=SC2086
exec $keep "$SCRIPT_DIR/agent-queue.sh" run

View File

@ -0,0 +1,70 @@
# Boot-persistence: agent-queue as a macOS LaunchAgent
Auto-start the `agent-queue` run loop on login and keep it alive across
**reboot / crash / logout** — the one failure mode that `tmux` + `caffeinate`
alone can't cover.
| Layer | Survives terminal close | Survives sleep | Survives reboot |
| ----- | :---------------------: | :------------: | :-------------: |
| plain shell | no | no | no |
| `tmux` | yes | no | no |
| `caffeinate` | n/a | yes | no |
| **LaunchAgent (this)** | yes | yes (via caffeinate) | **yes** |
## Install
```bash
bash launchd/install.sh # render plist, load, start now (RunAtLoad + KeepAlive)
tail -f ~/Library/Logs/agent-queue/agent-queue.out.log
```
It renders `~/Library/LaunchAgents/com.bytelyst.agent-queue.plist` from the
resolved repo path (works on any clone) and bootstraps it into your GUI session.
## Use
The LaunchAgent runs `agent-queue-boot.sh`, which wraps `agent-queue run` in
`caffeinate`. Just drop prompt `.md` files into `queue/inbox/` — they get picked
up automatically, now or after the next reboot.
```bash
aq add ~/jobs/phase3-overnight.md --engine codex # or drop the file in queue/inbox/
aqs # status
```
## Configure (no need to edit the plist)
Put overrides in `~/.agent-queue.env` (untracked — also the place for tokens):
```bash
AGENT_QUEUE_ENGINE=codex # codex (recommended: local repo) | devin | claude
AGENT_QUEUE_MAX=1 # concurrent jobs on this host (default 3)
# AGENT_QUEUE_NO_CAFFEINATE=1 # allow the Mac to idle-sleep (NOT for overnight runs)
# DEVIN_BIN=/custom/path/devin # if a CLI isn't on the default PATH
```
## Stop / uninstall
```bash
bash launchd/install.sh --uninstall # bootout + remove plist (queued jobs stay put)
```
## Notes & gotchas
- **codex vs devin:** for a local monorepo overnight runner, **codex** is the
default — it runs in-repo so `@bytelyst/*` workspace links resolve locally and
logs/token-usage parsing already work. Use **devin** when you want a cloud
sandbox doing the heavy lifting (and ACUs/network aren't a concern).
- **Power:** caffeinate wraps the long-lived loop, so the Mac stays awake the
whole time the LaunchAgent runs. That's intended for a dedicated runner. Set
`AGENT_QUEUE_NO_CAFFEINATE=1` if you'd rather let it idle-sleep when no job is
active. Keep it plugged in with the lid open for true overnight runs.
- **PATH:** launchd starts processes with a minimal `PATH`. Both the plist
(`EnvironmentVariables`) and the wrapper repair it, but if a CLI lives
somewhere unusual, point at it explicitly via `~/.agent-queue.env`.
- **Dangerous mode:** jobs run `--yolo` (auto-approve) by default. The safety net
is the agent-queue lifecycle itself — jobs land in `review/``testing/` and
**shipping is always a manual human gate**. Never let an unattended run touch
`main`; push to a branch and open one PR.
- **Auth:** cache `gh auth login` / git credentials and the agent CLI's auth
before relying on it overnight, or the first `push` will block forever.

105
agent-queue/launchd/install.sh Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env bash
#
# install.sh — install (or remove) the macOS LaunchAgent that auto-starts the
# agent-queue run loop on login and keeps it alive across reboot/crash.
#
# bash launchd/install.sh # render plist, load, and start now
# bash launchd/install.sh --uninstall # stop, unload, and remove the plist
#
# The plist is generated from the resolved repo path so it works on any clone.
# Logs land in ~/Library/Logs/agent-queue/.
#
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
AQ_DIR="$(cd -- "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd -P)"
WRAPPER="$AQ_DIR/agent-queue-boot.sh"
LABEL="com.bytelyst.agent-queue"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
LOG_DIR="$HOME/Library/Logs/agent-queue"
UID_NUM="$(id -u)"
DOMAIN="gui/$UID_NUM"
if [ "$(uname -s)" != "Darwin" ]; then
echo "install.sh: macOS only (LaunchAgents). On Linux use a systemd --user unit." >&2
exit 1
fi
uninstall() {
echo "[launchd] booting out $LABEL ..."
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
rm -f "$PLIST"
echo "[launchd] removed $PLIST"
echo "[launchd] (the run loop is stopped; queued jobs stay in queue/inbox/)"
}
if [ "${1:-}" = "--uninstall" ] || [ "${1:-}" = "-u" ]; then
uninstall
exit 0
fi
[ -f "$WRAPPER" ] || { echo "install.sh: missing $WRAPPER" >&2; exit 1; }
chmod +x "$WRAPPER" "$AQ_DIR/agent-queue.sh" 2>/dev/null || true
mkdir -p "$HOME/Library/LaunchAgents" "$LOG_DIR"
echo "[launchd] writing $PLIST"
cat > "$PLIST" <<EOF
<?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>$LABEL</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$WRAPPER</string>
</array>
<!-- Start on login and restart if it ever exits non-zero (crash/reboot). -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<!-- Guard against tight crash loops. -->
<key>ThrottleInterval</key>
<integer>30</integer>
<key>WorkingDirectory</key>
<string>$AQ_DIR</string>
<key>StandardOutPath</key>
<string>$LOG_DIR/agent-queue.out.log</string>
<key>StandardErrorPath</key>
<string>$LOG_DIR/agent-queue.err.log</string>
<!-- launchd's PATH is minimal; the wrapper also repairs PATH defensively. -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>AGENT_QUEUE_ENGINE</key>
<string>codex</string>
</dict>
</dict>
</plist>
EOF
# Reload cleanly (bootout first so a re-run picks up plist changes).
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
launchctl bootstrap "$DOMAIN" "$PLIST"
launchctl enable "$DOMAIN/$LABEL"
launchctl kickstart -k "$DOMAIN/$LABEL"
echo "[launchd] installed + started: $LABEL"
echo "[launchd] status : launchctl print $DOMAIN/$LABEL | sed -n '1,20p'"
echo "[launchd] logs : tail -f $LOG_DIR/agent-queue.out.log"
echo "[launchd] stop : bash $SCRIPT_DIR/install.sh --uninstall"
echo
echo "Drop prompt .md files into: $AQ_DIR/queue/inbox/"
echo "Override engine/concurrency/secrets in ~/.agent-queue.env (e.g. AGENT_QUEUE_MAX=1)."