The problem
I had more work than hands, and agents that could do a lot of it. What I didn’t have was a safe place to put the work. Ideas lived in roadmaps, bug folders, voice notes and chat. Agent runs started from whatever terminal I happened to be in. If I wanted an agent to pick something up overnight, I had to trust it with the whole decision: what to do, where, and when it was finished.
I’d already built one answer, a pull-based autopilot on my home server. It shipped disabled. Loop absorbed its good parts — the lease, the kill switch, the repo allow-list, worktree-and-PR-only — and added the thing I actually needed: a state between “I wrote this down” and “go do this now”, owned by a person.
What I built
Loop is a work queue. You can put anything on it — a title is enough — and it sits there. Once a person marks an item ready, an agent (Claude Code, Codex, Gemini CLI or opencode) does the work in a fresh git worktree, and the runner opens a pull request. A person merges.
It runs as a small daemon, loopd, on each machine. A CLI, an HTTP API, an MCP server, a web view, a desktop app and a Collab plugin all call the same service, so they obey the same rules.
How it works
readyis the gate. It requires a repo and a “done when” sentence. The feed from my starred projects only ever queues; it never marks anything ready.- The queue orders itself LIFO by default, so it doubles as a brain dump. FIFO is a toggle. “Smart” mode scores each new item 0–100 against my goals with a one-sentence reason, and scores age upward so nothing starves.
- Dispatch has five gates that all have to be open: dispatch enabled, kill switch off, this node is the leader, today’s spend under the daily cap, and a free slot. The cap on my Mac is $40 a day and $4 per item.
- The agent runs with push credentials stripped from its environment. What actually changed decides the outcome: no diff means failed, and a diff over the size limit is refused. The runner, not the agent, commits, pushes the branch and opens the PR.
- Every change is an event and an op-log entry. Nodes replicate by pulling each other’s op-logs, so a node that was asleep catches up on its own.
- A daemon tick checks in-review PRs every five minutes. Merged means done; closed means failed and back in the queue.
Decisions worth noting
Leadership is a lease, and claims are epoch-fenced. The node order is Mac, then Flex, then Tower. The highest-ranked live node leads; a lower one promotes after 180 seconds of silence. Every claim carries the leader’s epoch, so if the Mac comes back after Tower promoted, the Mac can’t complete work Tower re-dispatched. Its token is older and the store refuses it.
One lease authority, and I accepted the cost. Tower’s loopd hands out leases. If Tower is unreachable, nobody takes a new lease — the current holder works until expiry. The alternative allows two leaders and leans entirely on the fence. I’d rather stall than double-run.
SQLite, not an embedded Postgres. Several processes share the store, and claims take microseconds in WAL mode. I looked at PGlite: single-process, hundreds of milliseconds to start, and its one advantage (the same SQL dialect as the remote) doesn’t matter much for about six tables and an op-log.
Zero runtime dependencies. TypeScript run directly by Node 24, Node built-ins only, and src/ held at 100% line coverage.
Where it fits
Collab is Loop’s first integration, not a dependency. Loop reads my goals and starred roadmaps from Collab and writes status back — ready, in progress, in review with the PR link, shipped. Loop publishes its events to Headlines, which decides whether I see them. Surface shows Loop in its menu bar. A job that ends failed or held becomes a job of its own for a follow-up agent, so failures are never silent.
Outcome
The screenshot is a normal day: 118 items queued, 105 of them missing a “done when”, none ready, two in review. That ratio is the rule doing its job. Most of what I write down isn’t specified well enough to hand to an agent, and Loop won’t pretend it is.
On 1 November the Mac went quiet and Tower promoted itself without being asked. Flex came up, couldn’t reach the lease authority, and took no lease. Both are the designed behaviour. The gap I haven’t closed yet: agent jobs only run on the Mac, so during a failover Loop keeps queueing them and runs none.