loop

SkillProductivity

Run 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.

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 /loop won'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 /loop scheduler. At setup (and re-armed at the end of each turn while status: 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 (reason is required; delay min 60s). Re-arm while status: 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: true is required — without it the cron is in-memory only and dies when the session ends (defeating overnight durability). CronDelete it 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 codex re-invokes the agent CLI while the marker stays active, stops on paused/done (gates sacred), and works for claude, codex, cursor, or any CLI via LOOBSTER_DRIVE_CMD. That script is the cross-tool answer to "the loop died and nothing restarted it".
  • Crash resilience, in one line: the self-armed re-entry re-fires /loobster:loop <goal> after a dead turn → it self-heals (reclaims stale in_progress, continues from the checkpoint — see Self-healing below). The bundled Stop hook (hooks/hooks.jsonbin/loop-rearm.py) additionally refuses a premature graceful stop while status: activerepeatedly, up to LOOBSTER_LOOP_REARM_MAX (default 5) consecutive nudges with no cycle progress, resetting whenever the marker's cycle/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 with LOOBSTER_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.deny list (e.g. recursive deletes, force-push, hard-reset, .env reads) — deny/ask rules 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

  1. 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 AskUserQuestion if 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) or interactive (hold at every commit/push approval). Record the answers: criteria + constraints in the marker body; deliveryMode: pr-lane|interactive and 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 default deliveryMode: 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.
  2. Restate the goal in one line and write free-text success criteria (from intake where it ran). Create a goal record: TaskCreate a task titled GOAL: <one-line goal> with metadata.kind="goal", metadata.goalId="<slug>", the success criteria, and metadata.maxCycles (default 10). Also write plans/loop/<slug>.md with frontmatter (status: active, goalId: <slug>, the one-line goal, cycle: 0, plus the single-runner lease runner: + runnerHeartbeatAt: — see Self-healing) followed by the success criteria + an empty learnings log. The status: active marker is load-bearing: the Stop hook (bin/loop-rearm.py) reads it to keep the loop running across turn boundaries — set it to done only on a real exit/escalation.
  3. Recommend compression (loops re-read code every cycle): tell the user "set LOOBSTER_HEADROOM=1 to compress repeated code reads (README › Token reduction; PHI caveat applies)." Proceed regardless. Also make sure the loop's runtime droppings are gitignored — if .gitignore doesn'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, and plans/loop/status.html (commit that last one only if the team publishes it via Pages). The marker plans/loop/<slug>.md itself SHOULD be committed — it's the durable, shareable loop state.
  4. Seed the backlog: TaskCreate one task per known work item, each with metadata.goalId=<slug> and a RICE score per .agents/skills/backlog-scoring/SKILL.md. For large scoped items, run /make-plan to 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 with metadata.linearId + a RICE score (use Linear priority/estimate as scoring inputs; keep the score in Task metadata — don't spam it onto issues), and record linearProject: <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.
  5. Arm self-re-entry and SHOW the schedule (this is what makes the single command self-driving — and visible). Like the /loop skill 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/ScheduleWakeup are 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), plus CronCreate(cron: "*/20 * * * *", prompt: "/loobster:loop <goal>", durable: true) for closed-session durability (durable: true is 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 set status: done). Lead the line with self-driving: ARMED.
    • If those tools aren't available here, print self-driving: UNAVAILABLE → in-session loop + Stop hook only and 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 it bypassPermissions?), headroom on/off. This is the "ask loobster" answer at kickoff — and the status mode below reprints it on demand.
    • Run bin/loop-status.py build once 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.

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:

  1. Check the marker status first. If plans/loop/<slug>.md status is not active — it's paused (a human approval gate is pending) or done (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 a paused/done loop must be a no-op (this is what keeps an approval gate from being auto-driven past). Only continue when status: activewith one exception: if the marker is paused and the human has now answered the pending gate (their reply is in this conversation, or they re-invoked /loobster:loop themselves), that pause is resolved — apply their decision, set status: active back in the marker, re-arm the re-entry, and continue the loop. paused means "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 flips paused back to active after the gate is answered is a dead loop — this re-activation step is as load-bearing as the pause itself.
  2. 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 of bin/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 stored runner: — a ScheduleWakeup/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 see held 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 via bin/loop-lease.py acquire plans/loop/<slug>.md <runner-id> (it uses O_CREAT|O_EXCL, and stale takeovers serialize on an fcntl.flock gate, so two racing instances cannot both win — a real lock, not just a convention). If it prints held by <other> and exits 3, another instance is already looping this marker (a ScheduleWakeup/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 prints acquired (a free or stale lease), proceed and mirror runner + runnerHeartbeatAt into the marker frontmatter for humans/status. This per-runner lease (one runner per marker) is distinct from a task's metadata.heartbeatAt in step 4, which only distinguishes an interrupted task from a running one.
  3. Reload the goal task (metadata.kind=goal) + plans/loop/<slug>.md (cycle, learnings). If the goal task is gone but the marker exists, rebuild from TaskList for the goalId.
  4. Reclaim stale in-progress. Any task in_progress whose metadata.heartbeatAt is 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.)
  5. 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 mirror runnerHeartbeatAt in 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. (/resume routes a goal-loop straight here, and must re-acquire the lease too.)

The loop — repeat until an exit condition

  1. Trigger (next item): first, honor upstream marker edits (remote control): quietly git fetch and compare the marker's status on origin/<branch> with the local one — if a teammate (or the fleet dashboard, templates/fleet-pages.yml, whose Pause/Stop buttons commit marker edits) set paused or done upstream, 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 to plans/loop/<slug>-tasks/ (one file per task) — a new file there → TaskCreate a backlog item from its frontmatter and re-score it (keep human-set fields); a status: edit → mirror it onto the Task (done → completed, parked → blocked with a note, open → reopen); with linearProject set, 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) — read signals/*.md with status: new|ack and a relevance tag this goal cares about; a high-confidence signal may outrank the backlog (or spawn a new scored task). If linearProject is 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). Then TaskList, filter to open, unblocked tasks for this goalId, pick the highest metadata.score (ties → lowest effort, then lowest id). On picking a task with metadata.linearId, move its Linear issue to In Progress. If none remain → go to step 4 to decide done-vs-new-work.
  2. Investigate & Act — in a subagent (Agent, so the heavy reads stay out of the loop's context; apply the act model preference from .claude/loobster.json if 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. TaskUpdate the worked item (in_progresscompleted, or keep in_progress + record the blocker). Linear mirror: if the item has metadata.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 task in_progress, stamp metadata.startedAt + metadata.leaseId and refresh metadata.heartbeatAt as 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 in bypassPermissions; otherwise run the act in the loop's own permission context.
  3. Backlog gen / Assign: TaskCreate the new items the subagent surfaced (scored, tagged with goalId); with linearProject set, also create each as a Linear issue in the project (Task gets metadata.linearId).
  4. 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 the verify model preference from .claude/loobster.json if present — preferring a different model than act makes the judge independent in capability, not just in context. Then: append a one-line learning to plans/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 (stamp author, dedup first, no PHI). Mark any signal you acted on this cycle acted/archived.
  5. 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