looop — a tiny, portable, Kubernetes-shaped control loop for your 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
Arrive in the morning, look at the state of the world, make the single most
important move, repeat. Now a machine does that. It is a reconciliation loop,
exactly like Kubernetes — with one deliberate twist (see RULE 1):
Kubernetes looop
---------- -----
desired state (YAML) goals/*.md how things should be (inert files)
etcd the data dir PLAYBOOK + goals + journal = memory
informers / watch sensors/*.sh scripts that print the world as JSON
reconcile loop the pulse (looop up) observe -> diff -> one move per tick
scheduler policy PLAYBOOK.md priorities + guardrails, in prose
Job / Pod worker session a real agent doing hands-on work
Pod sandbox box workspace a git worktree per session
THE THREE NOUNS
PLAYBOOK.md judgment in prose: priorities, guardrails, rules. Humans AND the
AI grow it. It is data (lives with goals + journal).
goals/*.md the desired state. One file per goal: a 'goal:' frontmatter line
+ free-form dated notes. The AI creates/updates/archives them;
they are evaluated every tick but NEVER executed.
session an agent actually working — born only for hands-on work, runs in
a box workspace under babysit, gone when finished.
ONE BEAT (a tick)
1. run sensors/*.sh -> snapshot the world as JSON (fresh every tick)
2. if nothing changed since last tick -> do nothing (cheap, no AI call)
3. else hand PLAYBOOK + goals + sensor readings + sessions to the AI, which
makes EXACTLY ONE move and appends ONE line to journal.md
4. the AI process is disposable — all memory is the files in the data dir
RULES (the invariants that keep this safe and legible)
1. ONE TICK = ONE MOVE. The highest-priority move only; "do nothing" counts.
A runaway can do at most one thing per tick, and leaves one journal line.
2. THE PULSE IS UNBREAKABLE SHELL, JUDGMENT IS THE AI, MEMORY IS FILES. This
program never "thinks"; it only gathers context and runs the AI once.
3. NEVER DO IRREVERSIBLE THINGS AUTOMATICALLY (merge, public comments, closing
issues, deleting data). Prepare fully, then a worker session raises a
flag (looop flag) and waits for a human. Encode this in the PLAYBOOK.
4. BE INQUISITIVE. When a worker lacks context it asks (flag + wait) rather
than guessing. Asking is cheaper than a wrong irreversible move.
5. ADD STRUCTURE LATER. Only harden a drift into a PLAYBOOK rule or a check
once it actually hurts. Never build machinery in advance.
6. LEVEL-TRIGGERED. Each tick re-observes from scratch; the loop keeps no
"what I was waiting for" state. Re-running after days off is just a tick.
7. SINGLE-WRITER POLICY FILES. The pulse (the tick AI) is the sole writer of
PLAYBOOK.md, goals/ and sensors/. Workers write only to claims/, reports/
and their own code sandbox — never racing the pulse on policy files. The
lone exception is a human-gated meta session (setup / playbook grooming),
which shows a diff and waits for approval (RULE 3) before touching policy.
DRIVING A SESSION (a session is a live terminal, not a black box)
A worker runs under a real PTY, so you can both WATCH it and TALK to it from
outside — the same primitives a human or a script would use:
observe looop shot <id> the current screen
looop log <id> -f follow its output (tail -f)
looop status pulse + all sessions at a glance
drive looop send <id> TEXT type into its stdin
looop key <id> KEY named keys (Enter, C-c, Up, …)
looop expect <id> RE block until a regex appears (scriptable)
looop wait[-idle] <id> block until exit / until output settles
manage looop resize / restart / attach / detach
This makes a session driveable by hand, by a shell script (expect-style), or by
the pulse itself as one move. Human-gated decisions still go through ⚑flags
(RULE 3/4) — driving is for steering and automation, not for answering a flag.
CODE / CONFIG / DATA are cleanly separated (all overridable by env)
EXEC this one binary the program (portable)
CONFIG ${XDG_CONFIG_HOME:-~/.config}/looop.json one file: runner wiring
└ override with $LOOOP_CONFIG (seeded inline if absent)
DATA ${XDG_STATE_HOME:-~/.local/state}/looop/ the file-based memory
└ override with $LOOOP_DATA_DIR
PLAYBOOK.md goals/ journal.md sensors/*.sh <- the durable memory
snapshots/ prompts/ runs/ .lock .last-tick-hash <- scratch
sessions/<id>/ <- worker + pulse sessions (babysit state, per
profile; no $BABYSIT_DIR, no shared ~/.babysit).
System scratch: auto-reaped after a retention
window ($LOOOP_SESSION_TTL / session_ttl, def 3d).
Deliverables go to reports/, NOT here.
(runs/<id>/{prompt.md,output.log} = a replayable archive per beat)
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 an interactive setup session (goals/setup.md)
that interviews you and rewrites the seed into your real config. Setup is just
a goal; the program only seeds.
DEPENDENCIES
Just the configured agent CLI (claude or pi). Session management (babysit) is
LINKED IN as a library and runs in-process — no babysit binary is required.
The pulse provisions NO workspaces; a worker that touches code makes its own
sandbox (box if available, else git worktree), so box/git are worker concerns,
not pulse prerequisites. (gh and other tools are used only by sensor scripts /
worker agents — looop never requires them.)