A tiny, portable, autonomous control loop for agen…
A tiny, portable, autonomous control loop for agent-driven work. One self-contained binary — no database, no server, no helper files.
looop is the brain, not a task runner. It watches the things you care about (GitHub, Linear, Grafana, …) and runs a fleet of worker agents. Each beat it senses the world and, if something changed, decides the single most important move and executes it — including spawning workers. The judgment lives inside looop (a small, gated LLM call per beat).
An autonomous loop is easy. The hard part — and the whole point of looop's design — is where and how a human enters the loop. Too much human and it isn't autonomous; too little and it's reckless. looop's answer is to pull you in at exactly two kinds of moments, and nowhere else.
There are two distinct ways you touch the loop — and that split is the design.
Steer — async, you initiate. You are a peer, not a driver. You shape what looop pursues by editing goals and the PLAYBOOK; it observes them next beat. This never blocks the loop — you set direction and walk away.
looop _ goal write ship-v2 - # declare desired state (effective next beat) looop _ playbook write - # your judgment, priorities, guardrails
Answer — sync, the loop initiates. looop reaches back for you only when it genuinely must: a worker hits a decision only a human can make, or an irreversible action — merge, deploy, delete — needs an explicit yes. It blocks and waits for your call.
looop _ wait --only-asks # block cheaply until the loop needs you looop _ answer < id > " yes " # unblock the worker / approve the gate
The key move: the intervention point is decoupled from any UI. Asks and answers are a durable file mailbox reached through one backend-agnostic contract ( looop _ … ), so the loop never blocks on a particular terminal, tmux, or stdin — it just needs an answer eventually , from whatever channel reaches you:
a bare terminal — you typing the verbs yourself (the thinnest client);
an agent concierge — a claude / codex / opencode / pi session that relays asks in plain language and answers on your behalf;
a notify script — a loop that pushes asks to Slack/SMS and relays your reply.
A client is an interface , never a decision-maker. looop decides; the client just carries the question to you and your answer back.
Two properties make all this dependable:
Level-triggered. All state is plain files, so the loop re-senses every beat and a crashed pulse just re-reads its files on restart. A pending ask survives restarts — no queues, no lost work.
One move per beat. Each beat does at most one thing; a daily budget caps spend. Behavior stays legible and cheap — an unchanged world costs no LLM call.
SENSE — run every sensors/*.sh , refreshing snapshots/ . World unchanged since last beat → stop here, no LLM call.
DECIDE — on change, hand the PLAYBOOK + goals + readings + pending asks to the LLM, which returns one typed move.
ACT — execute it: write a goal/sensor/PLAYBOOK, run one reversible command, or spawn a worker. Irreversible moves are gated — they wait for your answer (see above), and so does any worker that hits a human-only decision.
Layer What it is
core the autonomous pulse + the durable state behind it. Decides and acts.
contract the looop _ … verbs — the one stable, backend-agnostic surface to read and steer core.
client anything that drives the contract for a human (terminal / concierge / notify). An interface, never a decision-maker.
State is plain files in the data dir, reached through the contract — not a public interface:
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
asks/ answers/ the worker ↔ human mailbox
curl -fsSL https://raw.githubusercontent.com/yusukeshib/looop/main/install.sh | bash
cargo install looop
Only hard dependency: an LLM runner. claude is the default; codex , opencode , and pi also work — pick one with looop init . (Workers that touch code isolate their own sandbox via git worktree , or box if available — a worker convention, not a looop dependency.)
looop init # interactive setup — required before up; pick the runner wiring
looop up # start the autonomous pulse (detached)
looop watch # live log + running-session selector (read-only)
looop down # stop the pulse and all workers
looop init is **required before looop up **: the pulse refuses to start until the runner wiring exists, so the agent CLI driving every tick and worker is an explicit choice, never a silent default. To read and steer core, drive the looop _ … verbs by hand ( looop _ state , _ wait , _ answer , _ goal write ) or let a client drive them for you.
looop runs headless, so it can't interview you. A fresh data dir is seeded with a starter PLAYBOOK, a setup goal, and a real pending setup ask so a client waiting on asks wakes immediately. The simplest way to answer is an agent client ("concierge") — a claude / codex / opencode / pi session you talk to in plain language:
claude # then say:
looop up, relay the setup goal, and interviewThe concierge runs looop up , surfaces the pending setup ask, and edits your goals/PLAYBOOK via the write verbs — speaking plain language while driving the contract. Once customized, answer the starter ask and archive the setup goal; looop runs from there.
You can skip the concierge entirely and steer by hand. See looop help for the full command reference and design manual.
The config ( $LOOOP_CONFIG , default ~/.config/looop/config.json ) is just two shell commands. looop init lets you pick claude , codex , opencode , pi , or custom ; after that looop treats the result as plain runner wiring:
Key Role
tick_command run ONE disposable decision. The prompt is passed via the {{prompt_file}} placeholder (substituted with the prompt file path — read it with $(cat {{prompt_file}}) or @{{prompt_file}} ). If you omit the placeholder the prompt is piped in on stdin instead. Must run unattended (no permission prompts — the detached pulse can't answer them) and emit a structured event stream looop can render.
worker_command launch a worker agent. Same {{prompt_file}} placeholder, substituted with the worker's prompt file path. (A worker can't use the stdin fallback — stdin is its live attach TTY.)
The built-in presets are:
claude (default)
{ "tick_command" : " claude -p --output-format stream-json --verbose --dangerously-skip-permissions --model sonnet " $(cat {{prompt_file}}) " " , "worker_command" : " claude --dangerously-skip-permissions --model opus " $(cat {{prompt_file}}) " " }
codex
{ "tick_command" : " codex exec --json --dangerously-bypass-approvals-and-sandbox " $(cat {{prompt_file}}) " " , "worker_command" : " codex --dangerously-bypass-approvals-and-sandbox " $(cat {{prompt_file}}) " " }
opencode (best-effort — verify against your installed version)
{ "tick_command" : " opencode run " $(cat {{prompt_file}}) " " , "worker_command" : " opencode " $(cat {{prompt_file}}) " " }
pi
{ "tick_command" : " pi -p --mode json -ne --model claude-sonnet-4-5 --thinking low @{{prompt_file}} " , "worker_command" : " pi --model claude-opus-4-8 --thinking medium @{{prompt_file}} " }
Model ids above are examples. For claude, sonnet / opus are aliases that always resolve to the latest of each; pin a specific version (e.g. --model claude-opus-4-1 ) if you need reproducibility.
本条由桃子采集流水线(启发式模式)自动整理,原文见文末信源。