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:
parent
f7999fb11b
commit
d574f5dda3
48
agent-queue/agent-queue-boot.sh
Executable file
48
agent-queue/agent-queue-boot.sh
Executable 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
|
||||||
70
agent-queue/launchd/README.md
Normal file
70
agent-queue/launchd/README.md
Normal 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
105
agent-queue/launchd/install.sh
Executable 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)."
|
||||||
Loading…
Reference in New Issue
Block a user