looop
A tiny, portable, Kubernetes-shaped control loop for your work.
looop watches the things you care about (GitHub, Linear, Grafana, …), and once
per beat asks an LLM to make exactly one move toward your goals — then stops.
It's a single self-contained binary with no daemon, no database, no server.

One full beat (sense → decide → journal), then the next beat skips the LLM entirely because nothing in the world changed.
How it works
Like a Kubernetes controller, every tick reconciles desired state against observed state and takes one step to close the gap:
┌─────────────────────────────────────────────┐
│ sense → diff → decide ONE move → act → log │
└─────────────────────────────────────────────┘
one tick
1. SENSE run every sensors/*.sh → each prints one JSON snapshot of the world
2. DIFF hash (PLAYBOOK + goals + snapshots + workers). Unchanged since
last tick? → skip, no LLM call (cheap, level-triggered)
3. DECIDE hand the PLAYBOOK + goals + snapshots + live workers to the LLM;
it picks THE single most important move
4. ACT a small reversible action, edit a goal/sensor, or start a worker
5. LOG append one line to journal.md, surface anything that needs you
Each tick is stateless and disposable: the process carries nothing in memory between beats — all state lives in files (goals, snapshots, journal, claims). Because of that the loop is level-triggered, not edge-triggered: every tick re-derives what to do from the current world (snapshots are wiped and re-sensed each beat), so a crashed tick, renamed sensor, or dead worker just self-heals on the next beat. Kill the pulse anytime; the next tick picks up exactly where the world is, not where a remembered cursor left off.
Concepts
Everything lives as plain files in the data dir (a git repo = the loop's memory):
| File / dir | Role (Kubernetes analogy) |
|---|---|
PLAYBOOK.md |
the controller logic — your judgment, priorities, guardrails |
goals/*.md |
desired state — one declarative spec per thing you're pushing |
sensors/*.sh |
observers — each prints one JSON object describing the world |
journal.md |
the action log — one line per move |
claims/ |
leases — a worker writes one to own a task; stale ones auto-reap |
reports/ |
deliverables a human reads (persists across ticks) |
Workers are the hands. When a move needs real, multi-step work, the loop spawns an agent session that runs detached, in parallel, and reconciles its task on its own. Workers that touch code provision their own sandbox first; the loop itself knows nothing about repos.
Humans in the loop. Workers never guess and never send OS notifications. When one needs a decision it raises a flag and waits; the pulse pops a tmux window you can't miss. You attach, answer, and it continues. Irreversible actions (merges, deploys, deletes) always require your explicit approval.
Quick start
The pulse always runs detached now (supervised in the background) — there is no foreground
looop run. Watch it live with looop up --watch / looop watch pulse, or add
--json for a machine-readable NDJSON stream.
On the first run the loop seeds a starter PLAYBOOK and a setup goal whose only
job is to interview you and rewrite the PLAYBOOK, goals, and sensors to match
your real work. After that it just runs.
Commands
(; )
(for)
)
)
|
||;
|)
| )
Shell integration
# Zsh (~/.zshrc)
# Bash (~/.bashrc)
This adds tab completion for every subcommand plus dynamic completion for
looop run <goal-id> (your goals/*.md) and looop attach|kill|flag|unflag <id> (this profile's live worker sessions). Completions resolve LOOOP_DATA_DIR
the same way the binary does, so an isolated profile completes its own goals and
sessions.
To change judgment: edit PLAYBOOK.md — it takes effect next tick.
Install
curl (recommended)
Downloads a prebuilt binary from GitHub Releases — no Rust toolchain needed:
|
Installs looop to ~/.local/bin/looop (override with LOOOP_INSTALL_DIR). The
script falls back to cargo install / nix profile install if no prebuilt
binary matches your platform. Make sure the install dir is on your PATH:
Cargo
Nix (flakes)
From git (latest main)
Verify
Runtime deps: just an LLM runner (pi or claude). looop is a single
self-contained binary — spawning, listing, attaching, killing, flagging and
pruning worker sessions all run in-process, no extra executable required.
Sessions are stored under $LOOOP_DATA_DIR/sessions, self-contained per profile:
looop sets no extra environment and shares no global state, and session ids are
bare (the pulse is pulse). (Workers that touch code also need git or box
to sandbox themselves, but that's a worker concern, not a prerequisite for the
pulse.)
Config & data
- Config —
$XDG_CONFIG_HOME/looop.json(overrideLOOOP_CONFIG). One file: runner wiring and tick cadence. Default runner ispi;claudeis built in. - Data / memory —
$XDG_STATE_HOME/looop/(overrideLOOOP_DATA_DIR). A git repo holding the PLAYBOOK, goals, journal, and sensors. Worker and pulse sessions live undersessions/in the same dir, so a profile is fully self-contained. PointingLOOOP_DATA_DIRelsewhere gives you an isolated profile with its own sessions.
LLM spend is metered automatically (ticks, manual runs, and self-reporting
workers) into an append-only ledger; see looop cost.