loop
SkillProductivityRun a continuous goal-loop: work a prioritized RICE-scored backlog toward a standing goal, cycle after cycle, consuming/emitting signals. Use for ongoing autonomous improvement toward a goal; not for a single discrete task (use run).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the loop skill
What this skill tells your AI
The instructions your AI receives, as published by nilswidal/loobster in .agents/skills/loop/SKILL.md and read by ahel’s review.
Run a dedicated goal-loop: pursue a standing goal by repeatedly working down a prioritized backlog, learning each cycle, until the goal is met. This is the outer loop that wraps RePPITS (/run is its "act" step).
Trigger (next backlog item) → Investigate & Act → Backlog gen / Assign → Review & learn → ↺
How to run it (so it actually loops)
- Invoke:
/loobster:loop <goal>(plugin commands are namespaced; the bare/loopwon't resolve unless you alias it as a project command). - One invocation runs cycles back-to-back in this session — after each Review & learn, immediately start the next cycle without asking, and keep going until an exit condition (below). Do not stop after one item; running many cycles is the whole point.
- Self-driving — no wrapper needed.
/loobster:loop <goal>arms its own durable re-entry; you do not prefix it with the/loopscheduler. At setup (and re-armed at the end of each turn whilestatus: active) it schedules a re-invocation of itself so a turn boundary or a dead turn (API drop, crash) auto-resumes the loop from its checkpoint:- In-session durability:
ScheduleWakeup(prompt: "/loobster:loop <goal>", delaySeconds: 1200, reason: "goal-loop heartbeat")— a self-paced re-entry that resumes the loop after a turn boundary or a dead turn (reasonis required; delay min 60s). Re-arm whilestatus: active; stop arming on exit. - Closed-session / overnight durability: also
CronCreate(cron: "*/20 * * * *", prompt: "/loobster:loop <goal>", durable: true)so it survives the session being closed.durable: trueis required — without it the cron is in-memory only and dies when the session ends (defeating overnight durability).CronDeleteit on exit; recurring crons auto-expire after 7 days. - If neither tool is available in your runtime (Codex, Cursor, a minimal harness), the loop still runs cycles in-session — and for durability, tell the user about the bundled external driver:
bin/loobster-drive.sh plans/loop/<slug>.md --tool codexre-invokes the agent CLI while the marker staysactive, stops onpaused/done(gates sacred), and works forclaude,codex,cursor, or any CLI viaLOOBSTER_DRIVE_CMD. That script is the cross-tool answer to "the loop died and nothing restarted it".
- In-session durability:
- Crash resilience, in one line: the self-armed re-entry re-fires
/loobster:loop <goal>after a dead turn → it self-heals (reclaims stalein_progress, continues from the checkpoint — see Self-healing below). The bundled Stop hook (hooks/hooks.json→bin/loop-rearm.py) additionally refuses a premature graceful stop whilestatus: active— repeatedly, up toLOOBSTER_LOOP_REARM_MAX(default 5) consecutive nudges with no cycle progress, resetting whenever the marker'scycle/heartbeat advances (so a working loop is re-armed indefinitely, a wedged one fails open). Together a dead turn, a crash, or a milestone-pause all resume automatically. Disable the hook withLOOBSTER_LOOP_REARM=0.
Permissions (loop autonomy ≠ tool permissions)
The loop suppresses its own questions, but Claude Code's tool-permission prompts are a separate layer the loop can't control. The subagents this loop spawns do not inherit a session's runtime --dangerously-skip-permissions flag — they resolve their permission mode from settings. So if permissions.defaultMode is auto or default, you'll be prompted per file write even though the main session shows bypass (auto mode's classifier independently evaluates each subagent's tool calls). To run the loop prompt-free:
- Set
permissions.defaultMode: "bypassPermissions"in~/.claude/settings.json(a launch flag alone is not enough for subagents). - Add a
permissions.denylist (e.g. recursive deletes, force-push, hard-reset,.envreads) —deny/askrules still fire in bypass, so you keep guardrails. - Then restart the session (settings load at start).
This command is executable, not advisory — do these concrete steps
On invocation, actually perform the following (don't just describe them):
Setup
- Goal intake — clarify once, then never ask again. Before creating any records, ask the user a SINGLE round of clarifying questions (2–4 in one message; use
AskUserQuestionif available, else plain text) covering: (a) scope — what's in, what's explicitly out; (b) definition of done — measurable success criteria; (c) constraints — stack, conventions, files/areas not to touch; (d) standing approvals — the load-bearing one: delivery mode. Offer two:pr-lane(recommended for unattended runs — the loop may commit to feature branches, push them, and open PRs on its own, never the default branch, so the commit gate becomes an async PR review instead of a blocking stop) orinteractive(hold at every commit/push approval). Record the answers: criteria + constraints in the marker body;deliveryMode: pr-lane|interactiveand any other granted standing approvals in the frontmatter. Skip the round with--no-intake, when the goal text already answers (a)–(d), or when no human is present (a cron/driver kickoff) — then defaultdeliveryMode: interactive. This is the only designed moment the loop interviews the user; everything after runs on these recorded answers — which is exactly why it's worth the one round. - Restate the goal in one line and write free-text success criteria (from intake where it ran). Create a goal record:
TaskCreatea task titledGOAL: <one-line goal>withmetadata.kind="goal",metadata.goalId="<slug>", the success criteria, andmetadata.maxCycles(default 10). Also writeplans/loop/<slug>.mdwith frontmatter (status: active,goalId: <slug>, the one-linegoal,cycle: 0, plus the single-runner leaserunner:+runnerHeartbeatAt:— see Self-healing) followed by the success criteria + an empty learnings log. Thestatus: activemarker is load-bearing: the Stop hook (bin/loop-rearm.py) reads it to keep the loop running across turn boundaries — set it todoneonly on a real exit/escalation. - Recommend compression (loops re-read code every cycle): tell the user "set
LOOBSTER_HEADROOM=1to compress repeated code reads (README › Token reduction; PHI caveat applies)." Proceed regardless. Also make sure the loop's runtime droppings are gitignored — if.gitignoredoesn't already cover them, offer to add:plans/loop/*.lock*(covers the lease lock, its flock gate, and tmp files),plans/loop/.rearm-state.json,plans/loop/*.drive.log, andplans/loop/status.html(commit that last one only if the team publishes it via Pages). The markerplans/loop/<slug>.mditself SHOULD be committed — it's the durable, shareable loop state. - Seed the backlog:
TaskCreateone task per known work item, each withmetadata.goalId=<slug>and a RICE score per.agents/skills/backlog-scoring/SKILL.md. For large scoped items, run/make-planto decompose — its sub-tasks join the backlog.- Linear-backed backlog (
--linear <project>): when the flag is given and the Linear MCP is available, Linear owns what work exists and the local Tasks are the loop's scored working mirror. Seed: fetch the project's open issues (skip ones assigned to other humans), create one local Task per issue withmetadata.linearId+ a RICE score (use Linear priority/estimate as scoring inputs; keep the score in Task metadata — don't spam it onto issues), and recordlinearProject: <id-or-name>in the marker frontmatter. Items the loop discovers later are created both as Linear issues in the project and as local Tasks, so the team sees the loop's queue in the tool they already use. If the flag is given but the MCP isn't configured, say so once and fall back to local Tasks.
- Linear-backed backlog (
- Arm self-re-entry and SHOW the schedule (this is what makes the single command self-driving — and visible). Like the
/loopskill confirms what it scheduled, this loop prints its own re-entry settings on kickoff so you can see exactly what's running, no separate script:- If
CronCreate/ScheduleWakeupare callable, arm them —ScheduleWakeup(prompt: "/loobster:loop <goal>", delaySeconds: 1200, reason: "goal-loop heartbeat")for an in-session dead-turn fallback (idle ticks past the 5-min cache window are wasted, so lean 1200–1800s), plusCronCreate(cron: "*/20 * * * *", prompt: "/loobster:loop <goal>", durable: true)for closed-session durability (durable: trueis required, else the cron dies when the session closes). The single-runner lease (see Self-healing) makes this safe even on a shared worktree: a fired wakeup/cron backs off if a live runner holds the lease and only takes over when the lease goes stale — so you can arm cron instead of hand-skipping it to dodge contention. Record the id in the marker frontmatter (reentry: <id>), then print the schedule: the cron expression, the human-readable cadence, the job/wakeup id, that recurring crons auto-expire after 7 days, and how to cancel (CronDelete <id>or setstatus: done). Lead the line withself-driving: ARMED. - If those tools aren't available here, print
self-driving: UNAVAILABLE → in-session loop + Stop hook onlyand continue — and print the external-driver line the user can run from a second terminal for real durability:bin/loobster-drive.sh plans/loop/<slug>.md --tool <claude|codex|cursor>. - Also print a one-line readiness so nothing is silent: Stop-hook re-arm on/off (
LOOBSTER_LOOP_REARM), permission mode (is itbypassPermissions?), headroom on/off. This is the "ask loobster" answer at kickoff — and thestatusmode below reprints it on demand. - Run
bin/loop-status.py buildonce and print: "live status board: plans/loop/status.html — open it in a browser; it refreshes every 30s and is regenerated each cycle." That file is how a human watches the loop without asking it anything.
- If
Self-healing — do this on every (re)entry (crash-safe resume)
A previous run may have died mid-cycle (API drop, crash, stop). Before picking work, reconcile durable state:
- Check the marker status first. If
plans/loop/<slug>.mdstatusis notactive— it'spaused(a human approval gate is pending) ordone(finished/escalated) — the loop is intentionally halted: do not resume or re-arm the driver; report the state and stop. A self-re-entry wakeup that fires on apaused/doneloop must be a no-op (this is what keeps an approval gate from being auto-driven past). Only continue whenstatus: active— with one exception: if the marker ispausedand the human has now answered the pending gate (their reply is in this conversation, or they re-invoked/loobster:loopthemselves), that pause is resolved — apply their decision, setstatus: activeback in the marker, re-arm the re-entry, and continue the loop.pausedmeans "waiting on a human", not "over"; only unattended re-entries (a cron/wakeup with no new human input) must treat it as a no-op. A loop that never flipspausedback toactiveafter the gate is answered is a dead loop — this re-activation step is as load-bearing as the pause itself. - Acquire the single-runner lease (concurrency safety — only ONE runner at a time per loop marker). First generate a fresh unique runner id for THIS invocation:
<runner-id>= the output ofbin/loop-lease.py newid, generated once at the start of every invocation and reused for this instance's acquire/refresh/release. Never derive the id from the goal/branch and never reuse the marker's storedrunner:— aScheduleWakeup/cron re-entry that reuses the live holder's id hits the lease's idempotent-refresh path and mistakes the live lease for its own (two runners on one loop, the exact collision the lease exists to prevent). A fresh id per invocation is what makes a re-entry correctly seeheld by <other>and back off. The authoritative mutex is an atomic lock file (<marker>.lock, one per marker — two different goals in one worktree lease independently), claimed viabin/loop-lease.py acquire plans/loop/<slug>.md <runner-id>(it usesO_CREAT|O_EXCL, and stale takeovers serialize on anfcntl.flockgate, so two racing instances cannot both win — a real lock, not just a convention). If it printsheld by <other>and exits 3, another instance is already looping this marker (aScheduleWakeup/cron firing while an in-session run is live, or a parallel headless run): do not start a concurrent cycle — leave the fallback armed and exit quietly. If it printsacquired(a free or stale lease), proceed and mirrorrunner+runnerHeartbeatAtinto the marker frontmatter for humans/status. This per-runner lease (one runner per marker) is distinct from a task'smetadata.heartbeatAtin step 4, which only distinguishes an interrupted task from a running one. - Reload the goal task (
metadata.kind=goal) +plans/loop/<slug>.md(cycle, learnings). If the goal task is gone but the marker exists, rebuild fromTaskListfor thegoalId. - Reclaim stale in-progress. Any task
in_progresswhosemetadata.heartbeatAtis missing or older than ~2 cycles was interrupted, not running → it becomes the next item. Check what already landed first (git status/diff, committed sub-issues, sub-task states) and continue it — never redo completed work, never double-commit. (This is the M19-style "stranded in_progress after a connection drop" case.) - Then enter the loop, refreshing the lease each cycle while you hold it —
bin/loop-lease.py refresh plans/loop/<slug>.md <runner-id>— and mirrorrunnerHeartbeatAtin the marker. If a refresh exits 3 ("not held by you"), you LOST the lease — you looked stale (a cycle ran longer than the TTL, default 3600s) and another instance took over. Do not keep working on a worktree another runner now owns: checkpoint nothing further, do not touch the marker, and exit quietly — the takeover runner reclaims your in-progress task via its heartbeat, exactly as if you had crashed. (/resumeroutes a goal-loop straight here, and must re-acquire the lease too.)
The loop — repeat until an exit condition
- Trigger (next item): first, honor upstream marker edits (remote control): quietly
git fetchand compare the marker'sstatusonorigin/<branch>with the local one — if a teammate (or the fleet dashboard,templates/fleet-pages.yml, whose Pause/Stop buttons commit marker edits) setpausedordoneupstream, pull that change, adopt it, and obey it exactly as if it were set locally: release the lease and pause/stop. This is what makes the dashboard's buttons real controls rather than decoration. Adopt upstream task-board edits the same way: the fleet dashboard's task board writes toplans/loop/<slug>-tasks/(one file per task) — a new file there →TaskCreatea backlog item from its frontmatter and re-score it (keep human-set fields); astatus:edit → mirror it onto the Task (done→ completed,parked→ blocked with a note,open→ reopen); withlinearProjectset, also file board-added items as Linear issues (Linear stays the source of what work exists). Then consume relevant signals from the shared hub (see.agents/skills/signals/SKILL.md) — readsignals/*.mdwithstatus: new|ackand arelevancetag this goal cares about; a high-confidence signal may outrank the backlog (or spawn a new scored task). IflinearProjectis set, re-sync with Linear before picking: pull issues created/updated since the last cycle into the mirror (new issue → new scored Task; an issue closed or reassigned to a human externally → complete/drop its Task — Linear wins). ThenTaskList, filter to open, unblocked tasks for thisgoalId, pick the highestmetadata.score(ties → lowest effort, then lowest id). On picking a task withmetadata.linearId, move its Linear issue to In Progress. If none remain → go to step 4 to decide done-vs-new-work. - Investigate & Act — in a subagent (
Agent, so the heavy reads stay out of the loop's context; apply theactmodel preference from.claude/loobster.jsonif present — see run.md "Preferred subagents"): the subagent investigates the item, then acts — substantial change → run/run <item>(autonomous mode; sensitive items still hit full gates + Secure); small fix → do it directly. It returns only a compact result: outcome, any new backlog items (with RICE), and a one-line learning.TaskUpdatethe worked item (in_progress→completed, or keepin_progress+ record the blocker). Linear mirror: if the item hasmetadata.linearId, move the issue with it — completed via a merged/direct change → Done; completed as an open PR (pr-lane) → In Review, with the PR linked on the issue; blocked → back to its ready state with a comment saying exactly what's needed. Heartbeat: when you set a taskin_progress, stampmetadata.startedAt+metadata.leaseIdand refreshmetadata.heartbeatAtas it progresses — that timestamp is what lets a later entry distinguish an interrupted task from a running one (step Self-healing).- Do not force
isolation: "worktree"on the act subagent. A worktree writes into an untrusted.claude/worktrees/<id>/path, and combined with non-bypass permission modes it surfaces a write-prompt per file (see "Permissions" above) — which defeats unattended running. Use worktree isolation only when genuinely parallelizing independent sub-issues and the session is inbypassPermissions; otherwise run the act in the loop's own permission context.
- Do not force
- Backlog gen / Assign:
TaskCreatethe new items the subagent surfaced (scored, tagged withgoalId); withlinearProjectset, also create each as a Linear issue in the project (Task getsmetadata.linearId). - Review & learn: judge the cycle's result against the goal's success criteria → met / partial / not-met. Use a separate verifier subagent — never let the agent that did step 2 (Act) judge its own work (see the "Never self-verify" rule in
run.md); the judge gets the diff + criteria and returns its verdict. Apply theverifymodel preference from.claude/loobster.jsonif present — preferring a different model thanactmakes the judge independent in capability, not just in context. Then: append a one-line learning toplans/loop/<slug>.md(re-summarize the log so it stays small); re-score the backlog from what this cycle taught (never overwrite user-set factors). Emit signals: if this cycle surfaced something other loops/teammates should know (a friction, an opportunity, a fact), write it to the shared hub per.agents/skills/signals/SKILL.md(stampauthor, dedup first, no PHI). Mark any signal you acted on this cycleacted/archived. - Loop: if no exit condition, go straight back to step 1 — do not pause for the user between cycles.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 36
- Forks
- 2
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
loop-nilswidal- Source
- github.com/nilswidal/loobster