# Daemonless ticket-queue poller for the sonu plugin — cron wrapper that wakes a headless agent session only when a trigger label or in-flight ticket branch exists

> Source: <https://gist.github.com/PrabhdeepSingh/ab71a961d46e9d6e72f202fb8c5bd06b>
> Published: 2026-08-01 00:47:18+00:00

| #!/usr/bin/env bash | |
| # factory-poll.sh — the entire "daemon" for the sonu plugin's ticket queue. | |
| # | |
| # Polls a repo's ticket queue and, only when there is something to do, spawns | |
| # one headless agent session that runs /sonu:factory (scan, claim, route, | |
| # build in a worktree, sweep). All the intelligence lives in the plugin; | |
| # this script only decides whether it's worth waking the agent. | |
| # | |
| # Requires: the sonu plugin installed (github.com/PrabhdeepSingh/claude-plugins), | |
| # an authenticated `gh` CLI, and a tracker configured via /sonu:factory init. | |
| # | |
| # Install (cron, every 15 minutes): | |
| # */15 * * * * $HOME/.sonu/factory-poll.sh | |
| # Or on macOS, a launchd plist with StartInterval 900. | |
| # | |
| # Notes: | |
| # - flock keeps runs serialized (one at a time), matching one-ticket-per-pass. | |
| # - The trigger labels are queried separately: `gh issue list` with two | |
| # --label flags is AND semantics, and a ticket only ever carries one trigger. | |
| # - In-flight ticket/* branches also wake the agent, or the close-the-loop | |
| # sweep would starve once triggers are consumed. | |
| # - Empty queue costs two API calls and zero tokens. | |
| set -u | |
| REPO_DIR="/path/to/repo" # <-- set me | |
| cd "$REPO_DIR" || exit 1 | |
| mkdir -p "$HOME/.sonu" | |
| exec 9>"$HOME/.sonu/factory.lock" | |
| flock -n 9 || exit 0 # a previous run is still going | |
| queued=0 | |
| for label in factory-ready-for-spec factory-ready-to-implement; do | |
| n=$(gh issue list --state open --label "$label" --json number --jq 'length' 2>/dev/null || echo 0) | |
| queued=$((queued + n)) | |
| done | |
| # Tickets mid-flight carry no trigger by definition; their ticket/* branches | |
| # are what the sweep needs to see (status flips, worktree cleanup). | |
| inflight=$(git branch --list 'ticket/*' | wc -l | tr -d ' ') | |
| [ "$((queued + inflight))" -gt 0 ] || exit 0 # nothing queued, nothing to sweep | |
| timeout 2h claude -p "/sonu:factory" >> "$HOME/.sonu/factory.log" 2>&1 |
