yusukeshib/looop: A tiny, portable, Kubernetes-shaped control loop for your work: senses your world and makes one LLM-

github.com · 2026-06-28 · by Long Nguyen ·github
A tiny, portable, Kubernetes-shaped control loop for your work: senses your world and makes one LLM-driven move per beat toward your goals. - yusukeshib/looop

yusukeshib/looop

A tiny, portable, Kubernetes-shaped control loop for your work: senses your world and makes one LLM-driven move per beat toward your goals. - yusukeshib/looop

3 stars · 0 forks · 0 watchers · Rust Topics: bash, kubernetes, cli, automation, reconciliation, control-loop, ai-agents, llm

README

looop A tiny, portable, autonomous control loop for agent-driven work. One self-contained binary — no database, no server, no helper files. The idea 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. How the human stays in the loop 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/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.

One beat: sense → decide → act

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.

Three layers

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

Install curl -fsSL https://raw.githubusercontent.com/yusukeshib/looop/main/install.sh | bash

or

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.) Usage 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. First run looop runs headless, so it can't interview you. A fresh data dir is seeded with a starter PLAYBOOK and a setup goal whose top priority is exactly that: on the first changed beat, looop journals an invitation to configure it. The simplest way to answer is an agent client ("concierge") — a claude/pi/codex session you talk to in plain language: claude # then say:

"be my looop concierge: run `looop up`, relay the setup goal, and interview

me to write my goals, sensors, and PLAYBOOK."

The concierge runs looop up, surfaces pending asks, and edits your goals/PLAYBOOK via the write verbs — speaking plain language while driving the contract. Once customized, archive the setup goal and 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. Configuration The config ($LOOOP_CONFIG, default ~/.config/looop/config.json) is just two shell commands — looop is glue and knows nothing about any specific runner:

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.)

looop init just lets you edit these two strings. The built-in default is claude; paste one of the wirings below (or your own) to switch runner. 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.

source

github.com · 107 links

Comments (0)

No comments yet