{"id":51172098,"url":"https://github.com/yusukeshib/looop","last_synced_at":"2026-07-02T04:01:12.603Z","repository":{"id":365065148,"uuid":"1269181247","full_name":"yusukeshib/looop","owner":"yusukeshib","description":"A tiny, portable, Kubernetes-shaped control loop for your work: senses your world and makes one LLM-driven move per beat toward your goals.","archived":false,"fork":false,"pushed_at":"2026-06-26T11:08:33.000Z","size":981,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T01:23:27.380Z","etag":null,"topics":["ai-agents","automation","bash","cli","control-loop","kubernetes","llm","reconciliation"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yusukeshib.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-14T11:59:40.000Z","updated_at":"2026-06-26T11:06:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yusukeshib/looop","commit_stats":null,"previous_names":["yusukeshib/loop","yusukeshib/looop"],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/yusukeshib/looop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukeshib%2Flooop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukeshib%2Flooop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukeshib%2Flooop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukeshib%2Flooop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yusukeshib","download_url":"https://codeload.github.com/yusukeshib/looop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukeshib%2Flooop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35032145,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","automation","bash","cli","control-loop","kubernetes","llm","reconciliation"],"created_at":"2026-06-27T01:09:46.700Z","updated_at":"2026-07-02T04:01:12.597Z","avatar_url":"https://github.com/yusukeshib.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# looop\n\nA tiny, portable, autonomous control loop for agent-driven work. One\nself-contained binary — no database, no server, no helper files.\n\n## The idea\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\n## How the human stays in the loop\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```sh\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```sh\nlooop _ wait --only-asks          # block cheaply until the loop needs you\nlooop _ answer \u003cid\u003e \"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 _ …`), 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 **bare terminal** — you typing the verbs yourself (the thinnest client);\n- an **agent concierge** — a `claude`/`codex`/`opencode`/`pi` session that relays\n  asks in plain language and answers on your behalf;\n- a **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\n  and a crashed pulse just re-reads its files on restart. A pending ask survives\n  restarts — no queues, no lost work.\n- **One move per beat.** Each beat does at most one thing; a daily budget caps\n  spend. Behavior stays legible and cheap — an unchanged world costs no LLM call.\n\n## One beat: sense → decide → act\n\n1. **SENSE** — run every `sensors/*.sh`, refreshing `snapshots/`. World unchanged\n   since last beat → stop here, no LLM call.\n2. **DECIDE** — on change, hand the PLAYBOOK + goals + readings + pending asks to\n   the LLM, which returns **one** typed move.\n3. **ACT** — execute it: write a goal/sensor/PLAYBOOK, run one reversible command,\n   or spawn a worker. Irreversible moves are gated — they wait for your `answer`\n   (see above), and so does any worker that hits a human-only decision.\n\n## Three layers\n\n| Layer        | What it is                                                            |\n| ------------ | --------------------------------------------------------------------- |\n| **core**     | the autonomous pulse + the durable state behind it. Decides and acts. |\n| **contract** | the `looop _ …` verbs — the one stable, backend-agnostic surface to read and steer core. |\n| **client**   | anything 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`      | your judgment, priorities, guardrails                   |\n| `goals/*.md`       | desired state — one declarative spec per thing you push |\n| `sensors/*.sh`     | observers — each prints **one JSON object**             |\n| `journal.md`       | action log — one line per move                          |\n| `asks/` `answers/` | the worker ↔ human mailbox                              |\n\n## Install\n\n```sh\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` is the default; `codex`,\n`opencode`, and `pi` also work — pick one with `looop init`. (Workers that touch\ncode isolate their own sandbox via `git worktree` — a worker convention, not a\nlooop dependency.)\n\n## Usage\n\n```sh\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 client   # non-agent TUI: pending asks always on screen, answer by hand\nlooop down     # stop the pulse and all workers\n```\n\n`looop init` is **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`looop _ …` verbs by hand (`looop _ state`, `_ wait`, `_ answer`, `_ goal write`)\nor let a client drive them for you.\n\n### First run\n\nlooop runs headless, so it can't interview you. A fresh data dir is seeded with a\nstarter PLAYBOOK, a `setup` goal, and a real pending `setup` ask so a client\nwaiting on asks wakes immediately. The simplest way to answer is an **agent\nclient** (\"concierge\") — a `claude`/`codex`/`opencode`/`pi` session you talk to in\nplain language:\n\n```sh\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`, 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` goal;\nlooop runs from there.\n\nYou can skip the concierge entirely and steer by hand — either the raw `looop _\n…` verbs, or `looop client`, a minimal TUI that keeps the pending ask list on\nscreen and lets you answer each one yourself. It is intentionally more spartan\nthan an agent concierge (no plain-language framing, no recommendations, no\nsteering) — its value is making looop's design legible: the loop drives itself,\nand the one thing it defers to a human is a worker's blocking ask. See `looop\nhelp` for the full command reference and design manual.\n\n## Configuration\n\nThe config (`$LOOOP_CONFIG`, default `~/.config/looop/config.json`) is just **two\nshell commands**. `looop init` lets you pick `claude`, `codex`, `opencode`, `pi`,\nor `custom`; after that looop treats the result as plain runner wiring:\n\n| Key              | Role                                                                                     |\n| ---------------- | ---------------------------------------------------------------------------------------- |\n| `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. |\n| `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.) It may also carry `{{model}}` and `{{thinking}}` placeholders for per-worker model selection (see below). |\n\n**Per-worker model selection.** The `worker_command` may include `{{model}}` and `{{thinking}}` placeholders. When a worker starts they are substituted with, in precedence order: the `looop _ worker start --model M --thinking L` flags, then the optional top-level `worker_model` / `worker_thinking` config keys, then the empty string. A template that omits a placeholder is left untouched, so configs and flag-less starts behave exactly as before; passing `--model` against a template with no `{{model}}` placeholder logs a warning and is ignored. Example wiring:\n\n```json\n{\n  \"worker_command\": \"pi --model {{model}} --thinking {{thinking}} @{{prompt_file}}\",\n  \"worker_model\": \"claude-opus-4-8\",\n  \"worker_thinking\": \"medium\"\n}\n```\n\nThe built-in presets are:\n\n**claude** (default)\n\n```json\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```json\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```json\n{\n  \"tick_command\": \"opencode run \\\"$(cat {{prompt_file}})\\\"\",\n  \"worker_command\": \"opencode \\\"$(cat {{prompt_file}})\\\"\"\n}\n```\n\n**pi**\n\n```json\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`/`opus` are aliases that always\nresolve to the latest of each; pin a specific version (e.g.\n`--model claude-opus-4-1`) if you need reproducibility.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusukeshib%2Flooop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyusukeshib%2Flooop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusukeshib%2Flooop/lists"}