looop — a tiny, portable, autonomous control loop for agent-driven work.
This single binary IS the whole program: principles, rules, and defaults all
live inside it. Install one binary and run it — no README, no helper dir.
THE IDEA
looop is an AUTONOMOUS control loop. Each beat it senses the world and, when
something changed, decides the SINGLE most important move and executes it —
spawning worker agents for hands-on work. The judgment lives INSIDE looop (a
small, gated LLM call per beat); looop is the brain.
You are a PEER, not the driver. You steer by editing goals / the PLAYBOOK
(looop observes them next beat); a worker that hits a decision only a human can
make asks you and waits. To watch and steer in plain language you can run a
CONCIERGE — a pi/claude session pointed at looop that reads its state, relays
worker questions to you, and helps you edit goals/answer. The concierge is an
INTERFACE, not a decision-maker: looop decides, the concierge just talks.
role who what it does
---- --- ------------
judgment the pulse (looop) sense every beat; decide + execute ONE move
hands worker sessions real agents doing multi-step work; when they
need a human decision they `ask` and wait
interface the concierge OPTIONAL human-facing pi/claude: read state,
(a pi/claude YOU run) relay asks, help you steer — never decides
memory the data dir PLAYBOOK + goals + journal + mailbox = files
HOW IT RUNS
1. `looop up` — start the pulse: a detached, autonomous loop that senses,
decides ONE move per changed beat, and runs the worker fleet. That is looop.
2. (optional) start a concierge to watch + steer in chat:
pi # then: "you are my looop concierge — show me `looop _ state`,
# relay any pending asks, and help me edit goals; looop
# itself decides. read `looop --help` first."
3. `looop down` — stop the pulse and every live worker.
You can skip the concierge entirely and just edit goals/PLAYBOOK files +
answer asks with `looop _ answer`.
ONE BEAT (sense → decide → act)
Each beat the pulse wipes + re-runs every `sensors/*.sh` so `snapshots/`
reflects the world. If the world is UNCHANGED since last beat it stops there —
no LLM call, nearly free. When it changed, looop hands the PLAYBOOK + goals +
readings + pending asks to the configured `tick` runner, which emits ONE typed
action; looop executes it (and gates risky ones). It is the SOLE senser +
decider (single-instance flock), so beats never race. Cadence is one interval
(config `interval`, default 60s); a move may nudge the next beat sooner. A
daily budget breaker (`max_daily_usd`) caps spend.
============================================================================
HOW YOU STEER (you-the-human, or the concierge acting for you)
============================================================================
looop decides on its own — you do NOT drive it beat by beat. You shape WHAT it
pursues and answer what only you can:
• Edit goals / the PLAYBOOK — declaratively say what "good" looks like. looop
observes the change next beat and steers toward it. Do this with the verbs
below (single-writer) or let the concierge do it for you.
• Answer worker asks — a worker blocked on a human decision shows up as a
pending ask; resolve it with `looop _ answer <ask_id> "<text>"` (or pass `-`
/ omit the text to read a multi-line answer from stdin / a heredoc).
• Watch — `looop watch` (live colored log + session selector) or
`looop _ state --json` for a structured snapshot.
VERBS (for you / the concierge — looop does NOT need these to act):
looop _ state [--json] read the full world state
looop _ wait [--json] [--only-asks|--actionable] BLOCK until something changes,
then print state + a `changed: […]`
diff summary. --actionable wakes only
on asks/journal, --only-asks on asks.
looop _ asks [--json] pending asks only (concierge's narrow view)
looop _ answer <ask_id> "<text>"|- resolve a worker's pending ask
looop _ goal write <id> [body|stdin] | _ goal archive <id>
looop _ sensor write <name> [script|stdin]
looop _ playbook write [body|stdin]
(multi-line bodies — incl. `_ answer`: omit the inline body or pass `-`, then
pipe it on stdin / heredoc.)
WORKER CONTRACT (auto-injected into every worker — for reference)
A worker that needs a human decision runs ONE blocking call and waits:
answer=$("$LOOOP_BIN" _ ask "$LOOOP_SESSION_ID" --prompt "…" [--ref P] [--options a,b])
It needs no terminal/attach — `ask` returns when answered (you reply with
`looop _ answer`). Workers write only to claims/, reports/ and their own code
sandbox; they end themselves with `_ kill`, lease shared resources with
`_ claim`/`_ unclaim`, and self-report spend with `_ cost`.
CODE / CONFIG / DATA are cleanly separated (all overridable by env)
EXEC this one binary the program (portable)
CONFIG <DATA>/config.json one file: runner wiring
└ override with $LOOOP_CONFIG (seeded inline if absent)
A runner needs TWO commands — `tick` (run one disposable decide move,
prompt on stdin) and `interactive` (launch a worker session;
`{{prompt_file}}` is its brief). Optional: `interval` (cadence),
`max_daily_usd` (budget breaker), `session_ttl` (corpse retention).
DATA ${XDG_STATE_HOME:-~/.local/state}/looop/ the file-based memory
└ override with $LOOOP_DATA_DIR
PLAYBOOK.md goals/ journal.md sensors/*.sh <- durable memory
asks/ answers/ <- the worker <-> human mailbox (relayed by concierge)
snapshots/ runs/ .lock .last-tick-hash .tick-backoff <- scratch
reports/ <- worker deliverables a human reads (persist)
sessions/<id>/ <- worker + pulse sessions (babysit state, per
profile; auto-reaped after session_ttl, 3d).
BOOTSTRAP
A fresh data dir is seeded once with a starter PLAYBOOK + goals/setup.md +
goals/playbook-daily.md + sensors/today.sh, all embedded in this binary. The
starter goal's top priority is for looop to invite you (via a journal note the
concierge relays) to configure it, then rewrite the seed into your real goals +
PLAYBOOK.
DEPENDENCIES
Just the configured agent CLI (claude or pi) — used BOTH for looop's per-beat
decide (`tick`) and to launch workers (and, if you run one, the concierge).
Session management (babysit) is LINKED IN as a library and runs in-process; no
babysit binary is required. A worker that touches code makes its own sandbox
(box if available, else git worktree).