008 cli
CLI reference
Every command dispatched by the looply binary, every flag it accepts, and which of them start a coding agent.
This page is the command surface of the harness: what a coding agent is put to work on, and what happens to the result. The binary is looply, installed from the looply-cli package, and every command below is one case in the switch statement in packages/cli/src/bin.ts. Nothing else is dispatched.
Every command
| Command | What it does |
|---|---|
looply | No arguments: the welcome screen. Whether this machine is paired, whether a config exists, how many queue items are ready and how many are still waiting to be checked, whether a run is live and what it is on. Then every command in three groups — getting set up, doing the work, keeping an eye on it — and one next step written as a line you can paste. |
looply init | Profiles the repo, asks a short set of questions, writes .looply/config.yaml. Refuses if that file already exists — move it aside for a fresh draft. |
looply doctor | Validates the config, runs the rails interlocks, then runs every gate step once. Reports each step and its duration. |
looply pair | Shows a six-digit code and waits for a phone to claim it. The machine token lands in ~/.looply/machine.json, outside the repo. |
looply daemon | Keeps this machine connected: heartbeats, ships the ledger, and carries out the commands the phone sends. |
looply scan | Reads the code through several narrow lenses and writes what each one finds into the queue as draft items. |
looply verify | Checks draft items against the code. The ones that survive become ready, which is the only state a run will pick. |
looply queue | Everything waiting, grouped by state, with where a round would start on each item, how many attempts it has had, and why it is where it is. Settled items are left out until you ask for them. |
looply stop | Writes a stop request to .looply/state/control.json. The run reads it between rounds, so the round in flight finishes and its work is kept. |
looply run | Starts a run: branch, pick an item, have the agent implement it, run the gate, have a second agent prove it, keep or discard, repeat until a budget limit or the queue stops it. |
looply status | One screen read from .looply/state/status.json and nothing else. |
looply report | Run report folded from .looply/state/events.jsonl. Alerts print first. |
looply agent | The machine-readable snapshot, written for another program rather than a person. |
looply version | Prints 0.1.0. --version and -v are the same case. |
looply help | Prints the usage text. --help and -h are the same case. |
Anything else exits 64 and prints the usage text to stderr. Two of the cases above are dispatched without appearing in that usage text: agent, deliberately, because it is a surface for tooling rather than a step in anyone's workflow, and help, which is the thing printing the text.
Which commands call an agent
Three commands start a coding agent, and those are the three that cost money. The rest read files, talk to the server, or run the commands you configured. If you are unsure whether a command will spend anything, this table is the answer.
| Command | Agent calls |
|---|---|
init, doctor, pair, status, report, queue, stop, agent, version, help | None. doctor says so on its own output line: it runs the interlocks and one full gate pass with no agent call. queue and stop only read and write files under .looply/state. |
daemon | None of its own. It can spawn looply run or looply scan as detached child processes when the phone sends start-run or scan, and those children cost. |
scan | One call per lens — six lenses by default — with an 8-minute timeout each. Lenses run one at a time, not in parallel. A lens that answers in prose instead of writing its findings file costs one extra call to reformat the answer, and says so. |
verify | One call per draft item, 5-minute timeout. --max N caps how many are checked; the rest stay draft. |
run | Per round: one implementer call with a 45-minute timeout, one prover call with a 10-minute timeout when proof.enabled is true, and a 5-minute verifier call whenever the item's cached evidence has gone stale. Before the first round, if nothing is ready, it verifies the drafts first. |
scan --dry-run still runs every lens and still costs money. It only skips writing to the queue and the ledger. run --dry-run is the opposite: it prints the plan, creates no branch, and calls nothing.
Flags
--no-color is the only global. It is stripped from the argument list before dispatch, so it works on any command in any position. Everything else is per-command, matched positionally by indexOf, so a flag that takes a value must be followed by that value. doctor, daemon, scan, verify and run all default --config to .looply/config.yaml; passing --config as the last argument with nothing after it prints the command's own refusal — run: --config needs a path — and exits 1. queue and stop take no --config at all: they read and write .looply/state directly and never load the config.
| Flag | Meaning |
|---|---|
--no-color | Global. Colour off. Honoured before anything prints, so a piped or redirected run is clean. |
init --yes, init -y | Take every default instead of asking. Assumed automatically when stdin is not a TTY, because prompting a pipe hangs forever. |
doctor --config <path> | Config to load. Default .looply/config.yaml. |
pair --server <url> | Server to pair against. Default https://looply-api-607184ed6acc.herokuapp.com. |
pair --name <name> | The name this machine shows as. Default: the basename of the current directory, or this machine if that is empty. |
daemon --config <path> | Default .looply/config.yaml. |
daemon --once | Send one heartbeat, print whether it succeeded, exit. Does not enter the loop. Exit 1 if the heartbeat failed. |
daemon --yes, daemon -y | Register this directory as a server-side project without asking. Registration is irreversible, which is why it is asked about otherwise. |
scan --config <path> | Default .looply/config.yaml. |
scan --lens a,b | Run only these lenses, comma-separated. If none match, the command fails and names the available keys. |
scan --per-lens N | Cap on findings per lens. Default 8. Must be a positive whole number. |
scan --dry-run | Report what the passes found and write nothing to the queue or ledger. The passes still run. |
verify --config <path> | Default .looply/config.yaml. |
verify --max N | Check at most N drafts. Positive whole number. |
queue --all | Include the settled states as well — duplicate, stale, done, closed. Without it they are counted and pointed at, not printed. |
stop --now | Takes no shortcut. The round in flight still finishes; the flag only adds a line saying why, so nobody is left believing the run died on the spot. |
run --config <path> | Default .looply/config.yaml. |
run --hours N | Override run.min_hours for this run without editing the config. Positive number; fractions are allowed. |
run --dry-run | Print the plan and stop. No branch, no agent call. |
agent --json | JSON snapshot. This is the default when neither flag is given. |
agent --text | The same snapshot as key=value lines, for shell callers. |
looply run
The branch name is looply/run- plus a 14-character UTC timestamp, computed in run-cmd.ts; the round runner is what actually creates it, so exactly one thing owns git. The run then loops until one of five things happens: the minimum hours elapse, the round cap is reached, the queue empties, a signal arrives, or a pause or stop is picked up from control.json between rounds — written there by looply stop on this machine, or by the daemon on a phone's behalf. Defaults from the config are 4 hours and 400 rounds. Two refusals happen before round one: a working tree with any changed path outside .looply stops the run, and so does a queue with nothing pickable — though if nothing is ready and there are drafts, the run verifies those first rather than refusing outright. When the run ends it prints the stop reason and the tally of rounds kept, discarded and abandoned — and if ten or more rounds ran with proof enabled and nothing was ever discarded, it says that a prover which never rejects is usually misconfigured.
# See what would happen. Costs nothing.
$ looply run --dry-run
project : tabsy
branch : looply/run-20260816120447 (would be created from a1b2c3d)
budget : 4h, up to 400 rounds
queue : 12 ready
proof : on
gates : typecheck, test
dry run: would start with "...".
dry run: no branch created, no agent called.
stopped : command
rounds : 0 — 0 kept, 0 discarded, 0 abandoned
# Then the real thing. The runner creates the branch.
$ looply run --hours 8
branch : looply/run-20260816121502
base : a1b2c3d
budget : 8h, up to 400 roundslooply scan
Six lenses ship, each a narrow question asked of the whole codebase: silent-failures, dead-ends, missing-states, untested-logic, accessibility, boundaries. Every finding is written as a draft item, never as a ready one, because a finding an agent volunteered is a claim and not yet a task. Each lens writes its findings to a file in the system temp directory rather than inside the repo — the same prompt tells the agent not to modify the project, and a findings file inside it is a contradiction the agent resolves by writing nothing.
# All six lenses, up to 8 findings each, written to the queue.
looply scan
# Two lenses, three findings each. Cheaper, still real agent calls.
looply scan --lens silent-failures,boundaries --per-lens 3
# Runs the passes, prints the report, writes nothing.
looply scan --dry-runlooply verify
The cheapest agent work looply does. Each draft is shown to the agent along with the titles and ids of the other open items, so a duplicate can be named rather than merely suspected. A draft is moved to verifying before the call and back to draft if the agent dies, so a failed pass leaves nothing stranded. Confirmed and amended items become ready; their evidence is cached against the current HEAD so the next pick starts at the right file and line instead of paying to find it again. A run does this by itself when it has nothing pickable — the standalone command exists so you can see what survives contact with the code before spending a round on anything.
looply queue
Reads .looply/state/queue.json and prints it grouped by state, in triage order: what can be worked on, then what needs you, then what is finished. Under each item go the three things a count cannot tell you — the first piece of evidence as path:line, which is exactly where a round would start; how many attempts the item has already had; and why it is in the state it is in, which is the blocked reason, the item a duplicate was merged into, or the reason the last attempt was thrown away.
The settled states — duplicate, stale, done, closed — are left out by default and replaced with a count and a pointer to --all, because a queue worked through a night otherwise buries its own live items under its history. An empty queue says so and names looply scan. A queue holding drafts and nothing ready says nothing is pickable yet and names looply verify, which is the command that changes that.
$ looply queue
2 ready to pick
itm_9f2c0a11 ItemForm image edits fail with no explanation
admin/src/menu/ItemForm.tsx:178
itm_4b70d3e2 the table charge is billed but never shown to the diner
web/src/menu/MenuTable.tsx:64 · 1 attempt
last attempt: the gate failed on typecheck
1 blocked — needs you
itm_0c81aa53 move pricing behind a flag
needs a schema change
6 more already dealt with — looply queue --alllooply stop
Writes a stop request to .looply/state/control.json — the same file the daemon writes when a phone sends one, and the only channel two separate processes share. The runner reads it between rounds, so the round in flight finishes and is kept or discarded on its own merits, and then the run ends with stop reason command. A run the phone started is detached and has no terminal to interrupt at all, which is the case this command exists for.
--now deliberately kills nothing. Nothing in this build can interrupt a round in flight, and a round cut off halfway leaves a working tree with changed paths in it — which is the first thing the next looply run refuses on. So the flag adds one line saying the round still has to finish, rather than accepting the word and then quietly waiting up to three quarters of an hour.
When no run is live, nothing is written at all and the command says so. A run clears any pending request immediately after it starts, so that last night's stop cannot wedge tonight's run before its first round; the same clearing is why a request written now would never be read by anything. A file left there would be an order nobody was going to obey, and a message is more honest than a write that changes nothing.
$ looply stop
stopping — the round in flight finishes first, then the run ends.
$ looply stop --now
stopping — the round in flight finishes first, then the run ends.
--now still lets the round in flight finish: cutting it off would leave the tree dirty.
$ looply stop
no run is going — nothing to stop.looply agent
Written for another program. It exits 0 whenever it could answer at all, including "nothing is running"; a non-zero exit means the question could not be answered, never that the answer was dull. Every field is present or explicitly null, and schemaVersion changes when a field's meaning changes. suggests is a stable token — init, answer-question, wait, scan, run — so a caller branching on it keeps working when the wording elsewhere changes.
$ looply agent --text
schema=1
project=tabsy
configured=true
paired=true
run.live=true
run.stale=false
round=7
phase=gating
queue.ready=11
queue.draft=4
queue.blocked=0
questions=0
totals.committed=5
totals.discarded=1
suggests=waitExit codes
| Code | Meaning |
|---|---|
| 0 | The command answered. For doctor that means every gate step passed; for run it means the loop ended for one of its normal reasons. queue and stop answer either way — stop exits 0 when there was no run to stop, because being told nothing is running is an answer and not a failure. |
| 1 | A refusal or a failure with a message. init when the config already exists; doctor when an interlock aborted or a gate step failed; status and report when their state file is missing, or when what is there cannot be read as looply's own; pair when nobody claimed the code; daemon when this machine has not paired, when registration was declined or had no terminal to ask on, or when the server was unreachable; run when the working tree has changes outside .looply, or when nothing is pickable even after the verification pass; any flag given a value it refuses. An uncaught error also lands here, with its message on stderr. |
| 64 | Unknown command. The usage text goes to stderr with it. |
One command is still named in the usage text under "not built yet", and it is item. It is not in the switch, so running it exits 64. Everything else the usage text lists is dispatched, and the bare looply screen now agrees with it: verify, queue and stop are all on its own command list, so it no longer offers anything that is missing. There are three ways to end a run and all three let the round in flight finish — looply stop from anywhere in the repo, a stop sent from a paired phone, which the daemon writes to the same control.json, and a Ctrl-C in the run's own terminal, which travels as a signal rather than a file and ends the run with stop reason signal instead of command.