An agent loop is the pattern behind "loop engineering": instead of prompting a session by hand, you design a system that starts work, checks it, advances it, and repeats until a goal is met or a human is needed. The Lanes Lanes Desktop MCP server gives you every piece a loop acts on. This page is the practical reference. For the background and the industry context, see the blog post Loop Engineering: Stop Prompting, Start Looping.
Prerequisites
- Lanes Desktop MCP enabled. Turn it on in Lanes under Settings, Local MCP, and connect your agent. See Lanes Desktop MCP.
- The
lanes-desktopskills (optional but recommended). Thelanes-sessionsskill teaches the tool surface and the multi-session model, so you rarely call tools by hand. - Lanes running. The server is served by the desktop app on
http://localhost:5353/sse.
The loop, in one rule
Every loop hangs off one signal: the session's runtime status, returned by
lanes_get_session_status.
| Status | Meaning |
|---|---|
none | Issue exists, no session has run |
starting | Spawned, no output yet |
busy | Working, producing output |
awaiting_input | Paused for a human (prompt, picker, permission dialog) |
stopped | Terminal closed cleanly |
exited | Process exited with code 0 |
error | Process exited non-zero |
exited, stopped, and error are terminal and never flip back. So:
A session is done, or needs you, when its status is
awaiting_input,exited, orstopped. Everything else means keep waiting.
Poll the status, not the terminal. Do not poll lanes_read_terminal in a tight loop.
Poll the cheap, structured lanes_get_session_status, and read the terminal only once,
when a session flips to awaiting_input or error and you need to know why.
Patterns
Each pattern below is a prompt you can paste to an agent that has the Lanes MCP connected.
Wait until done (goal loop)
Start a session on Lanes issue 12 in plan mode. Poll its status every few seconds and tell
me the moment it flips to awaiting_input, exited, or stopped. When it does, read the last 40
terminal lines and summarize what it is asking or what it finished.Babysit running sessions (cadence loop)
/loop 5m Check every running Lanes session with lanes_get_session_status. For any that are
awaiting_input or error, read the last 50 terminal lines, tell me in one line what each one
needs, and stop the loop once nothing is left running.Drive a column (scheduled loop)
/loop 10m Look at the implementation column in Lanes. Take the top issue that has no running
session, make sure it has a worktree, and start a plan-mode session on it. When a running
session reaches awaiting_input or exits, check lanes_get_issue_changes; if there is a real
diff, move the issue to review. Stop the loop when the column is clear.Board columns are backlog, planning, implementation, review, done, and misc.
Drain the backlog, one PR per issue
Go through every issue in my Lanes backlog. Give each one its own worktree, start a session
to implement it, and wait until it finishes or asks for input before starting the next. Have
each session open a pull request when it is done, then move the issue to review. List the PRs
at the end.Two notes on this one. Lanes has no "open a PR" tool of its own: the session opens the PR
itself, since it has a shell and its own worktree branch, and you can post the link back onto
a linked GitHub issue with lanes_github_comment_on_issue. And a worktree only appears if the
issue has worktreeStrategy: create and a worktreeName set before the session starts, so
have the loop set those first. Drop the wait between starts to run the backlog in parallel
instead; the per-issue worktrees keep the sessions from colliding.
Fan-out with a checker
For each Lanes issue labelled "ready", make sure it has worktreeStrategy set to create and a
worktreeName, then start a plan-mode session on each. Poll them all and give me a status
table. When one produces a diff, start a second session on that same issue whose only job is
to review the diff and run the tests, and report whether it passes.Splitting the maker from the checker matters: a fresh session with different instructions catches what the first one talked itself into.
Fix until green (verification loop)
Start a session on Lanes issue 20 to make the tests pass. When it goes idle, run the test
suite and check lanes_get_issue_changes. If tests fail, resume the session with the failing
output pasted in and tell it to fix exactly those. Repeat at most 3 times, then stop and
report whether it is green.A runnable driver
For unattended runs, talk JSON-RPC to the local server directly. This is the shape of the
loop, adapted from core/scripts/idle-probe.ts in the Lanes source. Note the two stopping
conditions and the single verification step.
const MCP = process.env.LANES_MCP_URL ?? "http://localhost:5353/message";
const POLL_MS = 2000;
const TIMEOUT_MS = 15 * 60_000; // hard stop: never loop forever
const DONE = new Set(["awaiting_input", "exited", "stopped"]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let id = 0;
async function call(name, args) {
const res = await fetch(MCP, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method: "tools/call", params: { name, arguments: args } }),
});
const json = await res.json();
if (json.error) throw new Error(`${name}: ${json.error.message}`);
const text = json.result?.content?.[0]?.text ?? "";
try { return JSON.parse(text); } catch { return text; }
}
async function driveIssue(issueId) {
await call("lanes_start_session", { issueId, cli: "claude", planMode: true });
const started = Date.now();
while (Date.now() - started < TIMEOUT_MS) { // brake #1: timeout
const sessions = (await call("lanes_get_session_status", { issueId })) ?? [];
const s = sessions.find((x) => x.ptyActive) ?? sessions[0];
if (s && DONE.has(s.status)) { // brake #2: terminal status
const changes = await call("lanes_get_issue_changes", { id: issueId });
return { status: s.status, changes };
}
await sleep(POLL_MS);
}
return { status: "timeout" };
}Guardrails
- Two brakes, always. A wall-clock timeout and a max-iteration cap. A terminal status is a third, natural brake. Never write a loop whose only exit is success.
- Verify before you advance. Gate
lanes_move_issuetoreviewordoneonlanes_get_issue_changesplus a real test run, not on the model saying it is done. Where it matters, let a separate checker session grade the work. - Keep a human checkpoint.
awaiting_inputhands control back; thereviewcolumn is where a person confirms before anything ships. - Mind the context budget. One issue per worktree keeps each session's context clean. Keep the orchestrator lean by polling status, not scrollback.
- Wait on events, not on sleep. For "wake me when idle," prefer a watcher that fires on the status change over a tight polling loop.
Further reading
- Loop Engineering: Stop Prompting, Start Looping: the why, with the 2026 sources that defined the practice.
- Lanes Desktop MCP: enabling the server and the full tool surface.
- GitHub Integration and Linear Integration: start and finish loops on your team's source of truth.