looop — a tiny, portable control plane 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 runs a control plane with the judgment OUTSIDE it. looop senses the
world, runs the worker fleet, and carries messages between workers and you. The
JUDGMENT lives in a ROOT AGENT — a pi/claude session YOU start and tell to
observe looop. looop does NOT launch or manage that agent; it only provides the
state and the verbs the agent drives. looop itself never calls an LLM — it is
the unbreakable plumbing; the root agent is the brain.
role who what it does
---- --- ------------
sensing the pulse (looop) run sensors every beat; keep snapshots fresh
judgment the root agent block on `looop _ wait`, decide ONE
(a pi/claude YOU run) move, drive looop via `looop _ …` verbs
hands worker sessions real agents doing multi-step work; when they
need a decision they `ask` and wait
memory the data dir PLAYBOOK + goals + journal + mailbox = files
HOW IT RUNS
1. `looop up` — start the pulse (a detached, judgment-free sensing loop that
keeps `snapshots/` fresh). That is all looop runs.
2. Start your agent yourself, in another window, and tell it to observe looop:
pi # then: "observe looop — loop on `looop _ wait --json` and
# act on it; read `looop --help` first."
3. `looop down` — stop the pulse and every live worker.
The human talks to that agent. There is no looop-managed root session and no
attach — you run the agent; looop just feeds it state.
ONE BEAT (the pulse, judgment-free)
Each beat the pulse wipes + re-runs every `sensors/*.sh` so `snapshots/`
reflects the world (cheap, no LLM). It is the SOLE senser (no two beats race)
and a single-instance loop (flock). Cadence is one interval (config `interval`,
default 60s). The pulse does nothing else — it doesn't decide and doesn't poke;
the root agent watches the world hash the pulse keeps fresh.
============================================================================
ROOT AGENT OPERATING CONTRACT (read by the AGENT SESSION, not by you-the-human)
============================================================================
This contract is for the autonomous pi/claude session you launch and point at
looop — IT is what emits every `looop _ …` verb below, in a running loop. You
(the human) only run `up`/`down` and talk to that agent in chat; you rarely type
a `_` verb yourself. "You" from here down means that agent session, not you.
Its loop is one line: while true; do state=$(looop _ wait --json); act on it; done
• `looop _ wait --json` BLOCKS until there is something to act on (a
pending ask, or the world changed) and returns the state:
{ world_hash, snapshots{name:reading}, asks[{id,worker,prompt,reference,
options}], workers[{id,state,alive}], goals[id], journal_tail[] }
(drop --wait for a one-shot read, e.g. when the human asks you something.)
• Decide the SINGLE most important move and act with a verb below.
• If a worker raised an `ask`: decide it yourself when you safely can, otherwise
ask the human in chat, then `looop _ answer <ask_id> "<answer>"`.
VERBS (you call these; they mutate the world and journal the move):
looop _ state [--json] | _ wait [--json] read state (blocking with --wait)
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]
looop _ run <cmd…> [--reason T] ONE ad-hoc REVERSIBLE shell command
looop _ worker start <id> <prompt…> spawn a worker for hands-on work
looop _ worker kill <id> end a worker session
looop _ notify <message…> surface a notice to the human (journaled)
(multi-line bodies: omit the inline body and pipe it on stdin / heredoc.)
INVARIANTS (never weaken these):
• NEVER do irreversible things (merge, deploy, delete, publish, pay) without the
human's explicit approval in chat. `_ run` is ONE reversible command only;
anything irreversible goes to a worker that `ask`s the human first.
• SINGLE-WRITER: change PLAYBOOK / goals / sensors ONLY through the verbs above,
never by editing files directly (the pulse reads them every beat).
• ASK, DON'T GUESS: when you lack info or authority, ask the human in chat.
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. Workers write only to
claims/, reports/ and their own code sandbox; they end themselves with `_ kill`
and lease shared resources with `_ claim`/`_ unclaim`; they 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 ONE command — `interactive` (how to launch an agent
session for each worker). Optional: `interval` (pulse cadence),
`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 <-> root-agent mailbox
snapshots/ prompts/ .lock .last-tick-hash <- 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 PLAYBOOK's top priority is for the root agent to interview you and
rewrite the seed into your real config.
DEPENDENCIES
Just the configured agent CLI (claude or pi) — used to launch workers (and it
is what you run as the root agent). 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).