Loop.define(config) is a pure function: it does no I/O and claims nothing. It returns a
LoopDefinition — the handle whose methods do the work:
import { Loop } from "@loop.js/core"
const loop = Loop.define({
goal: { file: "GOAL.md" },
limits: { rounds: 5, usd: 2, timeout: "10m" },
})
const run = loop.run() // claims the Lock and drives Rounds
const status = await loop.status() // one snapshot read from disk
LoopConfig
Only goal is required.
| Field | Type | Default | Meaning |
|---|
goal | Prompt | required | The set-once objective — injected into every Execute Round, and the bar Verify judges against when no verify prompt is given. |
execute | Prompt | { prompt?: Prompt; model?: string; permissions?: Permissions } | — | What to work on. A bare Prompt is shorthand for { prompt }; the object form adds an optional model and a per-phase Permissions override. Prompt omitted → the Goal stands in. |
verify | Prompt | { prompt?: Prompt; model?: string; permissions?: Permissions } | — | The success criteria. A bare Prompt is shorthand for { prompt }; the object form adds an optional model (the judge can run cheaper) and a Permissions override. Prompt omitted → the Goal stands in. |
limits | Limits | see below | Runaway guards plus the per-Round timeout. |
permissions | Permissions | — | The loop-level Permissions — raises both phases at once (e.g. "bypass" for a contained Run). Per-phase overrides win. |
workspace | string | "./workspace" | Points the work tree elsewhere. |
{ file } next to a binding key ({ file: "./verify.md", model: "…" }) is refused with
a teaching error at startup — write { prompt: { file }, model }.
Prompt
The judgment-bearing text you own — one shape for its three homes (goal, execute,
verify):
type Prompt = string | { file: string } | ((ctx: PromptCtx) => string | Promise<string>)
- Literal string — never touches disk.
{ file } — re-read fresh at each Round start, so a mid-loop edit retargets from the
next Round. Missing or unreadable is a loud error naming the path, never a silent literal.
- Function — called per Round with the deliberately starved
PromptCtx:
{ round, previous? }. round is 1-based across the whole Loop; previous (absent only
when nothing precedes) always carries feedback: string, plus verdict iff the last
Round produced one. Every function-form Prompt receives the same ctx, whichever home it
lives in.
Limits
Every field optional; omitted, each falls back to an engine default.
| Field | Type | Default | Meaning |
|---|
rounds | number | 3 | Rounds across the whole Loop (runaway guard). |
usd | number | 1 | Total $ across the whole Loop (step-granular cutoff). |
timeout | number | Duration | "5m" | Per-Round wall-clock timeout. |
Time values use the Duration grammar — a whole number with a unit: "45s", "90m",
"36h", "7d". Fields typed number | Duration also read a bare number of seconds.
Permissions
How tool calls are gated while no Sandbox contains the Run, resolved per phase:
phase override > loop-level > phase default (execute "auto", verify "read").
| Value | Meaning |
|---|
"read" | Read tools plus sandboxed commands; the write tools are denied at the adapter. |
"auto" | Work-tree edits auto-approved, shell in the provider’s command sandbox, the rest denied. |
"bypass" | Full autonomy, no gating — safe only inside a Sandbox. |