diff --git a/agent-queue/agent-queue-boot.sh b/agent-queue/agent-queue-boot.sh new file mode 100755 index 0000000..6844e14 --- /dev/null +++ b/agent-queue/agent-queue-boot.sh @@ -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 diff --git a/agent-queue/launchd/README.md b/agent-queue/launchd/README.md new file mode 100644 index 0000000..0f1018e --- /dev/null +++ b/agent-queue/launchd/README.md @@ -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. diff --git a/agent-queue/launchd/install.sh b/agent-queue/launchd/install.sh new file mode 100755 index 0000000..5a82847 --- /dev/null +++ b/agent-queue/launchd/install.sh @@ -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" < + + + + Label + $LABEL + + ProgramArguments + + /bin/bash + $WRAPPER + + + + RunAtLoad + + KeepAlive + + SuccessfulExit + + + + ThrottleInterval + 30 + + WorkingDirectory + $AQ_DIR + + StandardOutPath + $LOG_DIR/agent-queue.out.log + StandardErrorPath + $LOG_DIR/agent-queue.err.log + + + EnvironmentVariables + + PATH + $HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + AGENT_QUEUE_ENGINE + codex + + + +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)."