Skip to main content

The AI Issue Loop

ai-issue-loop is a label-driven pipeline that takes a GitHub issue marked ai-ready, implements it in a per-issue git worktree, opens a PR, has two agents review it, and hands it to you to merge. It runs unattended on a timer.

repo-tooling ships it as an agent skill and owns every repo-side piece it depends on — the branch-protection standard, the .claude/settings.json worktree config, and the skill itself.

Install it

npx @rtorcato/repo-tooling fix claude-skills

That writes ~/.claude/skills/ai-issue-loop/SKILL.md. Unlike every other fixer this one writes user-global state — a directory shared by every project on the machine — which is why it is opt-in: a bare fix or fix --yes skips it and says so, and doctor reports it as not configured rather than as a finding against your repo.

Three things follow from that:

  • --skills-dir <path> overrides the destination. It is required alongside --yes / --json when ~/.claude/skills does not exist, since a prompt would corrupt the JSON payload.
  • A stow-managed symlink is written through, not replaced. If ~/.claude/skills/ai-issue-loop/SKILL.md is a symlink into a dotfiles checkout, the content lands in dotfiles and stays version-controlled with the rest of your Claude config. The CLI reports the resolved real path so you know what to commit.
  • The install refuses to downgrade. Each installed copy carries a repo-tooling-version stamp in its frontmatter. A repo pinned to an older release reports and skips rather than overwriting a newer skill — otherwise two repos on different versions would fight over it on every fix.

Any agent that reads the skills CLI format can also take it straight from GitHub:

npx skills add https://github.com/rtorcato/repo-tooling --skill ai-issue-loop

The one constraint

Every agent in the pipeline authenticates as your own gh — no PATs, no bot account. GitHub refuses gh pr review --approve on your own PR, so a real GitHub approval is impossible.

Two consequences, and both are load-bearing:

  1. Approval is a label, not a review. ai-ok-code / ai-ok-sec record that an agent passed the diff.
  2. Required status checks stay the real merge gate. Never set required_pull_request_reviews on the protected branch — required review deadlocks every PR the loop opens.

The same constraint means everything an agent posts looks hand-written by the repo owner. So every comment an agent leaves opens with a 🤖 *Automated …* header naming which agent wrote it. A detailed security review under a human's avatar misrepresents who reviewed the code.

Labels and the state machine

All state lives in GitHub labels. A tick is a stateless, idempotent pass over that state, so a missed tick, a crash, or a restart costs nothing.

LabelOnMeaning
ai-readyissueEligible for an agent. The hard gate.
ai-wipissueClaimed; a worktree exists.
ai-blockedissueAgent gave up; needs a human.
holdingissueA gate — closes on human judgement, never picked up.
ai-reviewPRAwaiting agent review.
ai-ok-codePRcode-reviewer passed.
ai-ok-secPRsecurity-expert passed.
ai-changesPRA reviewer requested changes.
ai-notesPRPassed, but a reviewer left something to read before merging.
issue: ai-ready ─pickup─> ai-wip ─> PR opened, labelled ai-review
PR: ai-review ─> reviewers ─┬─> ai-ok-code + ai-ok-sec ─┬─ issue PR ─> assigned to you
│ (± ai-notes) │ ─> YOU merge
│ └─ dependabot ─> auto-merge
└─> ai-changes ─> fix round (max 2) ─> ai-review
└─ round 3 ─> ai-blocked

ai-changes is the send-back — never re-apply ai-ready to an open PR's issue; that is what double-picks it.

ai-notes is advisory and never blocks. It rides alongside a pass label, not instead of one. It exists because a pass label otherwise means both "clean" and "I found something real but would not hold the PR over it", and those two are indistinguishable in the Assigned to you view where merges actually happen. The bar is a finding that changes what a human would do: a semver implication, a deliberate omission, a follow-up that must be filed. ai-notes on every PR is the failure mode — it trains the reader to ignore it.

Repo prerequisites

npx @rtorcato/repo-tooling fix github-settings --yes

GITHUB_STANDARD already encodes exactly what the loop needs: squash as the only merge method (Pass 2 finds the (#N) squash subject on main to confirm work landed — a merge commit makes it look like nothing merged, and the worktree leaks), auto-merge, delete-branch-on-merge, and required_pull_request_reviews: null with a comment explaining that required review deadlocks auto-merge. You also need at least one required status check — that is the gate doing the real work.

Verify:

gh api repos/$OWNER_REPO --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, allow_auto_merge, delete_branch_on_merge}'
gh api repos/$OWNER_REPO/branches/main/protection \
--jq '{contexts: .required_status_checks.contexts, reviews: .required_pull_request_reviews}'

Worktrees also want node_modules symlinked in, so an agent can typecheck without a full install per issue. fix ai writes that into .claude/settings.json.

The tick

Passes run cheapest first, so a quiet repo exits fast.

PassDoes
0 — orientResolve the main checkout, fetch, list open PRs and ai-wip issues. Adopt unlabelled Dependabot PRs. Bail to Pass 5 with idle if there is nothing at all.
1 — mergeAuto-merge only Dependabot PRs that passed both reviews. Assign every other ready PR to you and drop ai-review. Send back anything GitHub reports as not CLEAN.
2 — clean upRemove worktrees whose PR merged (confirming the squash is on main first), then reap stalls.
3 — reviewSpawn the missing reviewers for ai-review PRs; dispatch a fix round for ai-changes.
4 — pick upClaim eligible ai-ready issues, create the worktree, spawn an implementer.
5 — reportOne-line summary, notify only when it changed. Never skipped, including on an idle tick.

Two details worth knowing because they fail silently when got wrong:

  • Worktrees live in a sibling directory (<repo>-worktrees/), never inside the repo. A worktree under .claude/worktrees/ sits on a path most repos exclude from their own tooling — observed on a repo whose Biome config carried "!**/.claude", where the pre-commit hook linted nothing in every agent worktree and failed with a message that read like a tooling glitch.
  • Pass 2 confirms the squash landed on main before removing anything. A squash-merged branch always looks like it has unmerged commits, which is indistinguishable from work that was never merged at all.

Limits

These exist because the loop runs unattended against a monthly usage cap.

  • 4 issues in flight, counted from open ai-wip issues.
  • Reviewers see the diff onlygh pr view, gh pr diff, the issue body. No repo-wide exploration.
  • 2 fix rounds per PR. On the third ai-changes, stop and mark ai-blocked. Reviewer↔implementer ping-pong is the one unbounded token sink.
  • An idle tick spawns zero agents.
  • Stall reaping instead of timeouts. Nothing can time an agent out from outside, so a label that has sat 45 minutes without its expected transition is reaped — but only when no PR exists, since an agent that opened one has already handed off. Every reap comments why; a bare ai-blocked reads as a considered judgement when it was actually a timeout.

Driving it

/loop 15m /ai-issue-loop

Ticks fire only while the REPL is idle, and a recurring /loop auto-expires after 7 days. Stop with /loop stop, or just remove the ai-ready labels — the loop then idles harmlessly.

Run /ai-issue-loop manually three or four times against one trivial issue before letting the timer drive it.

Safety

The ai-ready label is the hard gate: on a public repo only collaborators can apply labels. An author-association check (OWNER / MEMBER / COLLABORATOR) is the backstop, and the issue body is treated as untrusted data, never instructions. See Public-Repo Issue Safety for the full standard.

GitHub only — the loop is built on gh and has no GitLab path.