Drive one OpenThrottle plan to a PR with the built-in /loop, a task at a time in an isolated worktree. USE WHEN the user runs /ot-loop <planId>, says "loop over plan <id>", or wants a plan executed autonomously. Canonical source of the per-task discipline.
Scanned 9/13/2026
Install to Claude Code
npx -y skills add OpenThrottle/monorepo --skill ot-loop --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ot Loop?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/openthrottle-ot-loop)More formats (shields.io, HTML) on the badges page.
---
name: ot-loop
description: >-
Drive one OpenThrottle plan to a PR with the built-in /loop, a task at a time
in an isolated worktree. USE WHEN the user runs /ot-loop <planId>, says
"loop over plan <id>", or wants a plan executed autonomously. Canonical source
of the per-task discipline.
argument-hint: <planId>
arguments:
planId: string
disable-model-invocation: true
---
Important: `$planId` is the first (and only) argument passed to this prompt. If that value does not map back to an OpenThrottle Plan or Task ID, throw an error immediately!
Your job is to run the built-in **`/loop`** over OpenThrottle plan **`$planId`** and its tasks — executing one task at a time, keeping status in OT in sync, appending progress to the plan output stream as you go, and opening a PR when the plan is done. This is **Ralph driven interactively via `/loop`** rather than the workflow CLI: the loop is the outer driver, but the per-task discipline is identical to the [`agents-ralph`](https://github.com/openthrottle/monorepo/blob/main/skills/agents-ralph/SKILL.md) skill.
## Setup (once, before the loop)
1. **Work in an isolated worktree on a feature branch**, never on the base checkout. Creating, healing and removing worktrees is the [`ot-worktree`](../ot-worktree/SKILL.md) skill's job — use it for **every** worktree interaction in this loop, never a bare `git worktree` command. Follow that skill for how to invoke create / heal / destroy; a bare `git worktree add` skips the repo's provisioner (ports, `.env`, compose isolation), and a bare `git worktree remove` skips the teardown hook and the safety checks.
Name it for the plan (e.g. `feat/openthrottle-drivers`, `ot/cli-allow-list`); rename the generated branch with `git branch -m` if you want a different prefix. Runs to date have lived in dedicated `loop-plan-*` worktrees so the main checkout's server + OT MCP stay up.
Two things to check before you start work:
- **The worktree branches off whatever the base checkout has checked out**, not `main`. Confirm the base checkout is on the branch you intend to build from first, or fix it after with `git reset --hard origin/main`.
- **Where it lands is not fixed** — the root comes from a ladder (`OPENTHROTTLE_WORKTREE_ROOT` in the environment → `OPENTHROTTLE_WORKTREE_ROOT` in the target repo's `.env` → the default `~/.openthrottle/worktrees`), with `<org>/<repo>` appended beneath it. Use the path create prints; never assume one.
2. **Load the plan and tasks** — `get_plan(planId)` + `get_tasks_by_plan_id(planId)` (or `get_remaining_tasks_for_plan`). Canonical order is `sortOrder ASC, createdAt ASC`. If tasks are out of sequence, fix with `reorder_plan_tasks` (never delete-and-recreate).
3. **Set the plan `IN_PROGRESS`** via `update_plan(planId, { status: 'IN_PROGRESS' })` if it isn't already.
4. **Open a run row so this run is attributable** — `register_plan_run({ planId, model, branch })`.
Without this an interactive loop is invisible: nothing records which agent ran it, on which model, on which branch, or when. A queued Ralph run has always had this row.
- **Declare the `model` you are actually running as.** If you cannot determine it, pass null rather than a plausible guess — a wrong value in a provenance column is worse than a missing one, because nothing downstream can tell it is wrong. Null is a legible answer.
- **Do not declare `executionBackend`.** The MCP detects it from the harness that launched it. Pass one only if the tool tells you detection found nothing.
- **Capture the returned run id** and carry it as loop state for the rest of the run. You need it to settle.
- **Best-effort**: if register fails, say so and keep working. A run with no row is today's status quo, not a regression — never block real work on telemetry.
Then, once the worktree exists, `register_plan_run_worktree_checkout({ planRunId, filesystemPath })` with the path create printed, so `checkout_id` resolves. That is what makes "open in editor" work for this run and what lets the worktree read as busy while you are in it. It soft-fails harmlessly if the path is not a linked worktree.
5. **A fresh worktree needs codegen before app tests will collect** — run `pnpm nx run-many --target=codegen-graphql --all` if the `__generated__` output is missing.
## The loop (one task at a time)
> **This section is the canonical statement of the per-task discipline.** Change it here and
> nowhere else. One place restates it deliberately, because it is injected as a standalone
> prompt with no access to this file: [`agents-ralph`](../agents-ralph/SKILL.md) (read by the
> `workflow-ralph` CLI). Keep it in sync when you edit this. Everything else links here.
Each `/loop` iteration works exactly one task. Resume the lowest-`sortOrder` `IN_PROGRESS` task first; otherwise pick the lowest `sortOrder` `PENDING`/`QUEUED`.
> **Invariant — at most ONE task `IN_PROGRESS` at a time.** Steps 1→5 are one atomic unit: never run step 1 (`IN_PROGRESS`) for a task while another task is still `IN_PROGRESS`. Even when you power through several tasks in a single turn, fully close the current one — through **step 4 (`COMPLETED`)** — _before_ you start the next. Dropping the step-4 flip strands the task `IN_PROGRESS` even though its work shipped and was committed; there is no server-side reconcile, so it just sits there. **This is the single most common failure of this loop** — if you ever have two tasks `IN_PROGRESS`, you skipped a step 4.
1. **Start:** `update_task(taskId, { status: 'IN_PROGRESS' })`. (Precondition: no other task is `IN_PROGRESS` — see the invariant above.)
2. **Do the work** for that task, following the repo's rules (generators first, code style, no deep imports, etc.).
3. **Validate** before completing — at minimum `pnpm nx affected --target=lint,typecheck,test` for the touched projects (run targets **sequentially**, not in parallel — they share the Nx cache). Don't mark a task done on red.
4. **Complete — do this BEFORE starting any other task:** `update_task(taskId, { status: 'COMPLETED' })`. If the task genuinely can't be finished, set `BLOCKED` or `SKIPPED` instead — but never leave it `IN_PROGRESS` while you move on. Committing the work (step 5) is **not** a substitute for this flip.
5. **Commit per task** with `/github-commit` — conventional commit, with `Plan-Id:` and `Task-Id:` footers for traceability. Do **not** record a work-ledger artifact for these per-task work commits; the footers carry the traceability.
- **💰 Commit per task, but do NOT push per task.** Let the commits accumulate locally and push **once**, when the plan is done (the PR step below does it). Every push to a branch with a ready PR triggers a full CI run, so pushing per task burns N runs validating a branch nobody is reviewing yet. One run at the end validates the whole batch — the same total validation over fewer runs. Push mid-plan only if you must hand off, exit, or the worktree is at risk of being reaped. See [ci-cost.md](../../docs/monorepo/ci-cost.md).
6. **Add tasks when the work reveals more work** (`create_tasks` appends after the plan max when `sortOrder` is omitted).
7. **Repeat** — before selecting the next task, confirm the one you just finished is `COMPLETED` (not still `IN_PROGRESS`). Continue until every task is `COMPLETED`.
**Narrate as you go.** Use `append_plan_output(planId, ...)` for decisions and progress, passing `taskId` = the task the log actually describes (omit only for genuinely plan-level notes). One iteration can touch several tasks — tag the right id.
## Your run row
Everything here is specific to running interactively. It is deliberately outside the canonical
block above, because the detached `workflow-ralph` CLI already does all of it — it heartbeats on
a timer and polls the cancel marker itself. Only an agent turn has to do it by hand.
**Poll for a cancel at each task boundary.** Call `get_plan_runs(planId)` and check your run's
`cancelRequestedAt`. If it is set, stop where you are and settle `CANCELLED`.
> **This is the only way a Kill reaches you.** The server deliberately cannot stop an interactive
> run: it has no iteration loop to interrupt, so cancelling one returns `CANCELLATION_REQUESTED`
> and leaves the plan alone rather than resetting it underneath you. That safety floor is exactly
> what makes the poll your responsibility. An unpolled cancel is a cancel that silently does
> nothing — the user pressed the button and nothing happened.
Missing a poll only delays a cancel; it corrupts nothing. Best-effort is fine, silence is not.
> **Invariant — every exit path settles the run.** `settle_plan_run(planRunId, status)` with
> `COMPLETED` when the PR opens, `CANCELLED` on a deliberate stop, `FAILED` when you give up or
> hit something you cannot finish. A row opened and never settled sits `IN_PROGRESS` forever,
> reads as live, and holds its worktree marked busy. This is the same class of bug as leaving a task
> `IN_PROGRESS` (the loop's most common failure), one level up. Settling twice is a safe no-op, so
> settle when in doubt.
### What catches you if you don't
Three backstops exist. **None of them can pick the right terminal status** — only you know whether
this run completed, was cancelled or failed — so every one of them settles to `STALE`, which reads
as "lost contact". Treat them as damage control for a hard crash, never as a substitute for step 6.
| Backstop | Harness | Fires |
| ------------------------------- | ------- | -------------------------------------------- |
| Settle-on-next-register | any | instantly, when a new run opens on this plan |
| Claude Code `Stop` hook | Claude | ~6h after the session is provably gone |
| Unsupervised age sweep (server) | any | ~12h after the run started |
The floor is harness-agnostic: a run left open under cursor-agent, codex, gemini or antigravity is
settled by the age sweep just as surely as one under Claude Code — the difference is latency, not
coverage. What you lose by not settling is the accurate status and the hours in between, during
which the run reads as live and its worktree stays marked busy.
## Finishing
1. **Verify every task is closed, THEN set the plan `COMPLETED`.** First re-fetch `get_tasks_by_plan_id(planId)` (or `get_remaining_tasks_for_plan`) and confirm **zero** tasks are `IN_PROGRESS`, `PENDING`, or `QUEUED`. Flip any stranded task to `COMPLETED` (or `BLOCKED`/`SKIPPED`) before continuing — a committed task left `IN_PROGRESS` is the usual culprit (see the loop invariant). Only once the list is clean, `update_plan(planId, { status: 'COMPLETED' })`. There is no server-side downward reconcile in **either** direction: the plan can read `COMPLETED` while tasks are still `IN_PROGRESS`, so this explicit re-fetch is mandatory — never skip it.
2. Before continuing ensure `nx run-many -t lint test typecheck format-write check:local` all complete, flagging any errors we encounter
3. To minimize friction merging with main we will run `/github-squash` to condense our PR to a single commit
4. Next we will fetch main `git fetch origin main:main` and rebase the branch against `main`
5. **Open a Draft PR** with `/github-pull-request` (conventional-commit title, the repo PR template, testing steps phrased as things to do) — this is the single push for the whole plan. Leave it in **draft**: `build` skips on draft PRs, so a draft is what keeps any later push cheap. Mark it ready (`gh pr ready`) only when the work is genuinely up for review. **Capture the PR URL** — a real PR (branch pushed to the remote, PR object created) is the precondition for teardown below.
6. **Settle your run row** — `settle_plan_run(planRunId, 'COMPLETED')`. The PR is open, so the run is genuinely done. Nothing else will ever close this row.
7. **Stop the loop** once the PR is open. Do **not** merge.
## Teardown the worktree (only after a successful PR)
Once — and **only** once — the PR is confirmed open, tear down the isolated worktree. The branch now lives on the remote (via the PR), so removing the local worktree frees it to be checked out in the primary instance, letting the end-user easily pull the work for manual verification against a primary server.
1. **Confirm the PR is real first.** You must have the PR URL from the step above, and `git status` in the worktree must show the branch up to date with its remote and nothing uncommitted. If PR creation failed or anything is unpushed, **do not tear down** — leave the worktree intact and report the failure so no work is lost. Settle your run row `FAILED` in that case: the work is preserved but this run is over, and an unsettled row keeps the worktree marked busy for as long as it lives.
2. **Stop anything running in the worktree** — kill dev servers/watchers scoped to _this worktree's path only_. Never use a bare process-name pattern; that also kills the main checkout's server + OT MCP. You do **not** need to stop the worktree's containers by hand — OpenThrottle's `.worktree/teardown.sh` does that in step 3, scoped to this worktree's own compose project.
3. **Remove the worktree with [`ot-worktree`](../ot-worktree/SKILL.md) destroy**, never a bare `git worktree remove` — destroy runs `.worktree/teardown.sh` first, which stops this worktree's docker compose project (a bare removal leaves those containers running, detached from any checkout), and it refuses removals that would eat work. Run it from inside the worktree (no argument) or pass the worktree path.
It refuses the primary checkout, refuses a dirty tree, prunes admin dirs afterward, and preserves the branch by default — which is what you want here, since both the open PR and the end-user's verification checkout depend on it. Never pass `--delete-branch`. Add `--force` only if it reports a dirty tree _after_ you have already confirmed everything is committed and pushed; `--dry-run` first if you want to see exactly what it will do.
4. **Report** the PR link and note that the branch is now free to check out in the primary instance for manual verification (`git checkout <branch>`), or in a fresh isolated worktree via [`ot-worktree`](../ot-worktree/SKILL.md) create.
## After merge (not part of the loop)
This loop **does not wait for merge**. It stops once the PR is open, and any later merge-queue enqueue/landing is a separate step.
Only **after the PR is actually merged**, record the squash on the work ledger — `attach_session_subject({ planId, taskId? })` then `record_artifact({ type: 'git_commit', payload: { repo, sha } })`, or run `pnpm exec workflow-link-merge --plan <id> --sha <squash-sha> --repo <owner/repo>`. On a merge-queue-protected branch, `gh pr merge --auto` can return after **queuing** the PR, so do not use the branch head SHA for this step. Wait until `gh pr view --json mergedAt,mergeCommitSha` shows the landed merge commit (or read it back from `main`), then record that SHA. One `git_commit` artifact per merged commit, never per intermediate work commit.
## Rules
- **ALWAYS** follow [`ot-plans`](../ot-plans/SKILL.md) for OT tool selection and plan/task conventions, and [`github-commit`](../github-commit/SKILL.md) / [`github-pull-request`](../github-pull-request/SKILL.md) for commit and PR rules.
- Plans/tasks live in **OT only** — if the openthrottle-mcp MCP is unavailable, fail loudly; never fall back to Markdown plan files.
- Author/assignee fields expect the **GitHub username**, not a display name.
- Never push to `main`, never `--no-verify`; require confirmation before rebase/force-push.
- **One task `IN_PROGRESS` at a time.** Flip it to `COMPLETED` (or `BLOCKED`/`SKIPPED`) before starting the next, and re-fetch tasks to confirm zero `IN_PROGRESS`/`PENDING`/`QUEUED` before marking the plan `COMPLETED`. Committing the work is not the same as flipping the status.
- Task states: `BACKLOG`, `BLOCKED`, `CANCELED`, `COMPLETED`, `IN_PROGRESS`, `PENDING`, `QUEUED`, `SKIPPED`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!