looop
A tiny, portable control plane for agent-driven work. One self-contained binary — no database, no server.
looop watches the things you care about (GitHub, Linear, Grafana, …) and runs a
fleet of worker agents. Every beat it senses the world and, if something changed,
makes the single most important move — including spawning workers for hands-on
work. The judgment lives inside looop (a small, gated LLM call per beat).
You don't drive it; you steer it. You shape what it pursues by editing goals and the PLAYBOOK, and answer questions only a human can decide. Irreversible actions (merges, deploys, deletes) always wait for your explicit yes.

How it works
Each beat the pulse runs three steps:
- SENSE — run every
sensors/*.sh, refreshingsnapshots/. If the world is unchanged, stop here — no LLM call, nearly free. - DECIDE — when something changed, hand the PLAYBOOK + goals + readings + pending asks to the LLM, which returns one typed move.
- ACT — execute that move: write a goal/sensor/PLAYBOOK, run one reversible shell command, or spawn a worker. One move per beat; a daily budget caps spend.
State lives entirely in files, so the loop is level-triggered: it re-senses from scratch every beat, and a crashed pulse just re-reads its files on restart.
When a worker needs a human decision, it runs a blocking looop _ ask and waits;
you reply with looop _ answer. This durable mailbox works for headless
workers — no tmux, no stdin.
Concepts
Everything is plain files in the data dir (the loop's memory):
| File / dir | Role |
|---|---|
PLAYBOOK.md |
your judgment, priorities, guardrails |
goals/*.md |
desired state — one declarative spec per thing you push |
sensors/*.sh |
observers — each prints one JSON object |
journal.md |
action log — one line per move |
claims/ |
leases — a worker writes one to own a task |
reports/ |
deliverables a human reads |
asks/ answers/ |
the worker ↔ human mailbox |
Workers are the hands: detached agent sessions that do multi-step work in parallel. Workers that touch code provision their own sandbox; looop knows nothing about repos.
The contract is the boundary. You steer through the looop _ … verbs — they
are validated, journaled, and atomic. The file layout is the current backend,
reached through the contract. Editing a goal/PLAYBOOK file by hand still works
(seen next beat) but skips the journal — an escape hatch, not the main surface.
Quick start
On first run, looop seeds a starter PLAYBOOK and a setup goal that invites you to
configure it. Replace it with your real work (edit goals/PLAYBOOK, or use the STEER
verbs below), and looop runs from there.
Optionally drive looop in plain language from an agent session:
# asks, help me edit goals; read `looop --help`"
Commands
# HUMAN — looop runs itself; this is nearly all you touch
)
|)
| )
# STEER — the contract; driven by you or any client
|
| |
# WORKER self-callbacks — auto-injected, not human commands
| | |
Sensors
A sensor is a script in sensors/ that prints one JSON object each beat —
looop's window onto the world. looop knows nothing about GitHub, Linear, etc.; you
teach it by dropping a sensor. Output is stored in snapshots/<name>.json.
Split the JSON into two keys:
signal— the part that should WAKE the loop. A change here triggers a re-decide. Keep it small: counts, states, ids — not timestamps.detail— context that reaches the prompt but NEVER wakes the loop.
Without a signal key, the whole object is treated as the wake signal.
Example: GitHub PR-review sensor
# sensors/gh.sh (requires an authenticated `gh`)
#!/usr/bin/env bash
cr=
open=
Install with looop _ sensor write gh < sensors/gh.sh (or drop the file in the
data dir's sensors/). Its reading then appears in looop _ state, and a change
to changes_requested wakes the loop so the PLAYBOOK can react.
Install
curl (recommended)
Prebuilt binary from GitHub Releases — no Rust toolchain needed:
|
Installs to ~/.local/bin/looop (override with LOOOP_INSTALL_DIR); falls back to
cargo install / nix profile install if no prebuilt binary matches.
Other
Verify with looop version.
Runtime deps: an LLM runner (pi or claude) for the per-beat decide and
workers. Everything else (spawning, listing, killing sessions) runs in-process.
Workers that touch code also need git or box to sandbox themselves.
Shell integration
Config & data
- Config —
$LOOOP_DATA_DIR/config.json(overrideLOOOP_CONFIG). Runner wiring (tickcommand for decide,interactivecommand for workers), the pulseinterval, and an optionalmax_daily_usdbudget. Default runner ispi;claudeis built in. - Data —
$XDG_STATE_HOME/looop/(overrideLOOOP_DATA_DIR). Holds the PLAYBOOK, goals, journal, sensors, and sessions. Not versioned for you —git initthe data dir yourself if you want history. PointingLOOOP_DATA_DIRelsewhere gives an isolated profile.
LLM spend is recorded in an append-only ledger (looop cost). Set max_daily_usd
to arm a daily budget breaker that skips the AI once today's spend hits the cap
(clears at local midnight).