{"slug": "looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent", "title": "Looop – A tiny, portable, Kubernetes-shaped control loop for your LLM agent", "summary": "Looop, a new open-source tool, launches as a tiny, portable autonomous control loop for LLM agents, operating as a single binary without external dependencies. It monitors sources like GitHub and Linear, runs worker agents, and introduces a human-in-the-loop design with two distinct interaction modes: async steering and sync answering. The project aims to make agent-driven work dependable by using level-triggered state and a one-move-per-beat policy.", "body_md": "A tiny, portable, autonomous control loop for agent-driven work. One self-contained binary — no database, no server, no helper files.\n\n**looop is the brain, not a task runner.** It watches the things you care about\n(GitHub, Linear, Grafana, …) and runs a fleet of worker agents. Each beat it\nsenses the world and, if something changed, decides the *single* most important\nmove and executes it — including spawning workers. The judgment lives *inside*\nlooop (a small, gated LLM call per beat).\n\nAn autonomous loop is easy. The hard part — and the whole point of looop's\ndesign — is **where and how a human enters the loop.** Too much human and it\nisn't autonomous; too little and it's reckless. looop's answer is to pull you in\nat exactly two kinds of moments, and nowhere else.\n\nThere are two distinct ways you touch the loop — and that split *is* the design.\n\n**Steer — async, you initiate.** You are a peer, not a driver. You shape *what*\nlooop pursues by editing goals and the PLAYBOOK; it observes them next beat. This\nnever blocks the loop — you set direction and walk away.\n\n```\nlooop _ goal write ship-v2 -      # declare desired state (effective next beat)\nlooop _ playbook write -          # your judgment, priorities, guardrails\n```\n\n**Answer — sync, the loop initiates.** looop reaches back for *you* only when it\ngenuinely must: a worker hits a decision only a human can make, or an\nirreversible action — merge, deploy, delete — needs an explicit yes. It blocks\nand waits for your call.\n\n```\nlooop _ wait --only-asks          # block cheaply until the loop needs you\nlooop _ answer <id> \"yes\"         # unblock the worker / approve the gate\n```\n\nThe key move: **the intervention point is decoupled from any UI.** Asks and\nanswers are a durable file mailbox reached through one backend-agnostic contract\n(`looop _ …`\n\n), so the loop never blocks on a particular terminal, tmux, or stdin\n— it just needs an answer *eventually*, from whatever channel reaches you:\n\n- a\n**bare terminal**— you typing the verbs yourself (the thinnest client); - an\n**agent concierge**— a`claude`\n\n/`codex`\n\n/`opencode`\n\n/`pi`\n\nsession that relays asks in plain language and answers on your behalf; - a\n**notify script**— a loop that pushes asks to Slack/SMS and relays your reply.\n\nA client is an *interface*, never a decision-maker. looop decides; the client\njust carries the question to you and your answer back.\n\nTwo properties make all this dependable:\n\n**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.\n\n**SENSE**— run every`sensors/*.sh`\n\n, refreshing`snapshots/`\n\n. 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`\n\n(see above), and so does any worker that hits a human-only decision.\n\n| Layer | What it is |\n|---|---|\ncore |\nthe autonomous pulse + the durable state behind it. Decides and acts. |\ncontract |\nthe `looop _ …` verbs — the one stable, backend-agnostic surface to read and steer core. |\nclient |\nanything that drives the contract for a human (terminal / concierge / notify). An interface, never a decision-maker. |\n\nState is plain files in the data dir, reached *through* the contract — not a\npublic interface:\n\n| File / dir | Role |\n|---|---|\n`PLAYBOOK.md` |\nyour judgment, priorities, guardrails |\n`goals/*.md` |\ndesired state — one declarative spec per thing you push |\n`sensors/*.sh` |\nobservers — each prints one JSON object |\n`journal.md` |\naction log — one line per move |\n`asks/` `answers/` |\nthe worker ↔ human mailbox |\n\n```\ncurl -fsSL https://raw.githubusercontent.com/yusukeshib/looop/main/install.sh | bash\n# or\ncargo install looop\n```\n\n**Only hard dependency:** an LLM runner. `claude`\n\nis the default; `codex`\n\n,\n`opencode`\n\n, and `pi`\n\nalso work — pick one with `looop init`\n\n. (Workers that touch\ncode isolate their own sandbox via `git worktree`\n\n, or `box`\n\nif available — a\nworker convention, not a looop dependency.)\n\n```\nlooop init     # interactive setup — required before `up`; pick the runner wiring\nlooop up       # start the autonomous pulse (detached)\nlooop watch    # live log + running-session selector (read-only)\nlooop down     # stop the pulse and all workers\n```\n\n`looop init`\n\nis **required before looop up**: the pulse refuses to start until\nthe runner wiring exists, so the agent CLI driving every tick and worker is an\nexplicit choice, never a silent default. To read and steer core, drive the\n\n`looop _ …`\n\nverbs by hand (`looop _ state`\n\n, `_ wait`\n\n, `_ answer`\n\n, `_ goal write`\n\n)\nor let a client drive them for you.looop runs headless, so it can't interview you. A fresh data dir is seeded with a\nstarter PLAYBOOK, a `setup`\n\ngoal, and a real pending `setup`\n\nask so a client\nwaiting on asks wakes immediately. The simplest way to answer is an **agent\nclient** (\"concierge\") — a `claude`\n\n/`codex`\n\n/`opencode`\n\n/`pi`\n\nsession you talk to in\nplain language:\n\n```\nclaude   # then say:\n# \"be my looop concierge: run `looop up`, relay the setup goal, and interview\n#  me to write my goals, sensors, and PLAYBOOK.\"\n```\n\nThe concierge runs `looop up`\n\n, surfaces the pending setup ask, and edits your\ngoals/PLAYBOOK via the write verbs — speaking plain language while driving the\ncontract. Once customized, answer the starter ask and archive the `setup`\n\ngoal;\nlooop runs from there.\n\nYou can skip the concierge entirely and steer by hand. See `looop help`\n\nfor the\nfull command reference and design manual.\n\nThe config (`$LOOOP_CONFIG`\n\n, default `~/.config/looop/config.json`\n\n) is just **two\nshell commands**. `looop init`\n\nlets you pick `claude`\n\n, `codex`\n\n, `opencode`\n\n, `pi`\n\n,\nor `custom`\n\n; after that looop treats the result as plain runner wiring:\n\n| Key | Role |\n|---|---|\n`tick_command` |\nrun 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. |\n`worker_command` |\nlaunch 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.) |\n\nThe built-in presets are:\n\n**claude** (default)\n\n```\n{\n  \"tick_command\": \"claude -p --output-format stream-json --verbose --dangerously-skip-permissions --model sonnet \\\"$(cat {{prompt_file}})\\\"\",\n  \"worker_command\": \"claude --dangerously-skip-permissions --model opus \\\"$(cat {{prompt_file}})\\\"\"\n}\n```\n\n**codex**\n\n```\n{\n  \"tick_command\": \"codex exec --json --dangerously-bypass-approvals-and-sandbox \\\"$(cat {{prompt_file}})\\\"\",\n  \"worker_command\": \"codex --dangerously-bypass-approvals-and-sandbox \\\"$(cat {{prompt_file}})\\\"\"\n}\n```\n\n**opencode** (best-effort — verify against your installed version)\n\n```\n{\n  \"tick_command\": \"opencode run \\\"$(cat {{prompt_file}})\\\"\",\n  \"worker_command\": \"opencode \\\"$(cat {{prompt_file}})\\\"\"\n}\n```\n\n**pi**\n\n```\n{\n  \"tick_command\": \"pi -p --mode json -ne --model claude-sonnet-4-5 --thinking low @{{prompt_file}}\",\n  \"worker_command\": \"pi --model claude-opus-4-8 --thinking medium @{{prompt_file}}\"\n}\n```\n\nModel ids above are examples. For claude, `sonnet`\n\n/`opus`\n\nare aliases that always\nresolve to the latest of each; pin a specific version (e.g.\n`--model claude-opus-4-1`\n\n) if you need reproducibility.", "url": "https://wpnews.pro/news/looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent", "canonical_source": "https://github.com/yusukeshib/looop", "published_at": "2026-06-30 15:05:18+00:00", "updated_at": "2026-06-30 15:20:26.533106+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["Looop", "GitHub", "Linear", "Grafana", "Claude", "Codex", "OpenCode", "Pi"], "alternates": {"html": "https://wpnews.pro/news/looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent", "markdown": "https://wpnews.pro/news/looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent.md", "text": "https://wpnews.pro/news/looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent.txt", "jsonld": "https://wpnews.pro/news/looop-a-tiny-portable-kubernetes-shaped-control-loop-for-your-llm-agent.jsonld"}}