Published on

Automating Development with GitHub Agent Loops

Authors

What if your GitHub issues could resolve themselves? This guide shows how to build an agent loop that continuously processes issues labeled agent-ready, implements them using TDD, runs QA, and closes them without you touching anything.

I've been running this across a few projects—video generation, a helpdesk app, some experiment platforms—and honestly, it works better than I expected for well-scoped tasks.


How It Works

┌─────────────────────────────────────────────────────────────┐
Agent Loop Cycle├─────────────────────────────────────────────────────────────┤
1. Fetch oldest issue with 'agent-ready' label             │
2. Move to 'in-progress'3. Implementation Phase (TDD with Agent Teams)4. QA Phase (test, fix, verify)5. Documentation Update (CLAUDE.md)6. Close issue + push changes                              │
7. Repeat└─────────────────────────────────────────────────────────────┘

The loop runs indefinitely (or for N iterations), picking up issues in order and processing them through a structured workflow. Each step gets posted as a comment on the issue, so you can see exactly what the agent did.


Key Features

Label-Based Workflow

LabelMeaning
agent-readyIssue is ready to be picked up
in-progressAgent is currently working on it
completedSuccessfully finished

Issues move through these states automatically. If the script crashes, it resets any in-progress issues back to agent-ready on startup. Crash recovery is built in.

Structured Execution Steps

Each issue goes through seven documented steps:

  1. Starting Work – Agent announces pickup
  2. Implementation – TDD with Agent Teams (up to 10 parallel agents)
  3. Summary – Documents what was built
  4. QA Review & Fix – Runs tests, fixes any failures
  5. QA Report – Final status with diffs
  6. CLAUDE.md Update – Captures learnings
  7. Complete – Closes issue, pushes code

Agent Teams Integration

The script enables Claude Code's experimental Agent Teams feature:

export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

This spawns up to 10 parallel agents:

  • Writing tests in parallel
  • Implementing separate modules simultaneously
  • Running checks while others code
  • Fixing different categories of issues concurrently

Setup Requirements

Prerequisites

# GitHub CLI authenticated
gh auth status

# Claude Code installed
claude --version

# Node.js project with these scripts
npm run build
npm run test
npm run typecheck
npm run lint

Directory Structure

your-project/
├── scripts/
│   └── loop-github.sh
├── logs/
│   └── agent-loop.log
├── CLAUDE.md
└── ...

GitHub Labels

Create these labels in your repository:

gh label create "agent-ready" --color "0E8A16" --description "Ready for agent"
gh label create "in-progress" --color "D93F0B" --description "Agent working"
gh label create "completed" --color "5319E7" --description "Agent finished"

Usage

Basic Run

# Run 10 iterations
./scripts/loop-github.sh 10

# Run forever
./scripts/loop-github.sh -1

Background Execution

# Run in background with logging
nohup ./scripts/loop-github.sh -1 > logs/agent-loop.log 2>&1 &

# Or use tmux for easy monitoring
tmux new-session -d -s agent-loop './scripts/loop-github.sh -1'
tmux attach -t agent-loop

Monitoring

# Watch the log
tail -f logs/agent-loop.log

# Check tmux session
tmux capture-pane -t agent-loop -p | tail -20

# List issues in progress
gh issue list --label "in-progress"

Creating Agent-Ready Issues

How you write issues matters more than you'd think. Structure them like this:

## Objective
Implement user authentication with JWT tokens.

## Requirements
- [ ] Login endpoint at POST /api/auth/login
- [ ] Token generation with 24h expiry
- [ ] Middleware for protected routes
- [ ] Unit tests for all functions

## Acceptance Criteria
- Tests pass
- TypeScript types complete
- No hardcoded secrets

## Context
See existing auth patterns in lib/auth.ts

What helps:

  • Be specific about endpoints, file locations, expected behavior
  • Include acceptance criteria the agent can actually verify
  • Reference existing code patterns
  • Break large features into smaller issues (the agent handles focused tasks better than sprawling ones)

Customization Points

Project-Specific Commands

Edit the prompts to match your stack:

# Instead of generic npm commands
npm run build
npm run test

# You might use
pnpm build
pytest tests/
cargo test

Phase Labels

Add phase labels for tracking progress:

gh label create "phase-1" --color "BFD4F2"
gh label create "phase-2" --color "D4C5F9"

The script detects these and includes them in context:

PHASE=$(echo "$ISSUE_LABELS" | grep -oE 'phase-[0-9]+' | head -1)

Iteration Limits

Control how many issues to process:

./scripts/loop-github.sh 5    # Process exactly 5 issues
./scripts/loop-github.sh -1   # Run until manually stopped

Error Handling

Crash Recovery

On startup, the script resets any stuck issues:

STUCK_ISSUES=$(gh issue list --label "in-progress" --state open ...)
for stuck_issue in $STUCK_ISSUES; do
  gh issue edit "$stuck_issue" --remove-label "in-progress" --add-label "agent-ready"
done

Graceful Shutdown

Catches SIGINT/SIGTERM to reset the current issue:

trap cleanup SIGINT SIGTERM SIGHUP

cleanup() {
  if [ -n "$CURRENT_ISSUE" ]; then
    gh issue edit "$CURRENT_ISSUE" --remove-label "in-progress" --add-label "agent-ready"
  fi
  exit 0
}

Press Ctrl+C and the current issue returns to agent-ready instead of getting stuck.

Implementation Failures

If Claude fails during implementation, the issue moves back:

gh issue edit "$ISSUE_NUMBER" --add-label "agent-ready" --remove-label "in-progress"
sleep 10
continue

This allows retry on the next iteration.


What I've Learned Running This

Start small. Begin with isolated, well-defined issues. Expand scope once you trust it.

Watch the first few runs. You'll want to tune prompts based on what actually happens.

Still review the work. The loop is autonomous, but periodic human review catches edge cases the tests miss.

Phase labels help. Grouping related issues with phase-1, phase-2 keeps things logical.

Context matters. Reference existing patterns in issue descriptions. The agent reads CLAUDE.md for guidance, so keep that current.

Documentation compounds. The loop updates CLAUDE.md automatically, so institutional knowledge builds over time.


Where It Falls Short

Be honest about the limitations:

  • Ambiguous requirements – The agent follows instructions well but doesn't do great with "figure out what we should do here"
  • External dependencies – API integrations, database migrations usually need human setup first
  • Security-sensitive code – Review auth, authz, and crypto implementations yourself
  • Big architecture decisions – Major refactors benefit from human planning. Let the agent execute, not architect.

The pattern works best for well-defined tasks in an established codebase. Let humans handle architecture and security; let the agent handle the implementation grind.


Extending the Pattern

Slack/Discord Notifications

Add a webhook call when issues complete:

curl -X POST "$WEBHOOK_URL" -d "{\"text\": \"✅ Issue #$ISSUE_NUMBER completed\"}"

Multiple Repositories

Run separate loops per repo in different tmux windows:

tmux new-session -d -s project-a 'cd ~/project-a && ./scripts/loop-github.sh -1'
tmux new-session -d -s project-b 'cd ~/project-b && ./scripts/loop-github.sh -1'

Priority Labels

Modify the issue fetch to prioritize certain labels:

# High priority first
ISSUE_JSON=$(gh issue list --label "agent-ready" --label "priority-high" --state open --limit 1 --json number,title,body)

# Fall back to regular if none
if [ -z "$ISSUE_JSON" ]; then
  ISSUE_JSON=$(gh issue list --label "agent-ready" --state open --limit 1 --json number,title,body)
fi

Complete Script

Here's the full loop-github.sh script. Customize the prompts and commands for your project:

#!/bin/bash
# loop-github.sh - Automated task execution loop
# Usage: ./scripts/loop-github.sh <iterations>
# Run in background: nohup ./scripts/loop-github.sh 100 > logs/agent.log 2>&1 &
#
# Features:
# - Agent Teams (up to 10 parallel agents)
# - TDD approach
# - QA with actual fixes
# - CLAUDE.md maintenance
# - Crash recovery: resets in-progress issues on startup

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

# Enable Claude Code Agent Teams
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

# Track current issue for cleanup on crash
CURRENT_ISSUE=""

# Cleanup function for graceful shutdown
cleanup() {
  echo ""
  echo "[$(date)] Script interrupted. Cleaning up..."
  if [ -n "$CURRENT_ISSUE" ]; then
    echo "Resetting issue #$CURRENT_ISSUE to agent-ready..."
    gh issue edit "$CURRENT_ISSUE" --remove-label "in-progress" --add-label "agent-ready" 2>/dev/null || true
  fi
  exit 0
}

# Trap signals for graceful shutdown
trap cleanup SIGINT SIGTERM SIGHUP

if [ -z "$1" ]; then
  echo "Usage: $0 <iterations>"
  echo "Example: $0 10       # Run 10 tasks"
  echo "Example: $0 -1       # Run forever"
  exit 1
fi

ITERATIONS=$1
COUNTER=0

echo "=========================================="
echo "Agent Loop Starting"
echo "Iterations: $ITERATIONS (-1 = infinite)"
echo "Agent Teams: ENABLED (up to 10 agents)"
echo "Repository: $(pwd)"
echo "=========================================="

# ============================================================
# STARTUP: Reset any stuck in-progress issues from previous crash
# ============================================================
echo ""
echo "Checking for stuck in-progress issues from previous run..."
STUCK_ISSUES=$(gh issue list --label "in-progress" --state open --limit 100 --json number --jq '.[].number' 2>/dev/null || echo "")
if [ -n "$STUCK_ISSUES" ]; then
  echo "Found stuck issues: $STUCK_ISSUES"
  for stuck_issue in $STUCK_ISSUES; do
    echo "Resetting #$stuck_issue to agent-ready..."
    gh issue edit "$stuck_issue" --remove-label "in-progress" --add-label "agent-ready" 2>/dev/null || true
  done
  echo "Cleanup complete."
else
  echo "No stuck issues found."
fi
echo ""

# Helper function to add comment to issue
add_comment() {
  local issue_num="$1"
  local phase="$2"
  local message="$3"
  local timestamp=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
  
  gh issue comment "$issue_num" --body "### $phase

$message

---
*🕐 $timestamp*"
}

while true; do
  # Check iteration limit
  if [ "$ITERATIONS" != "-1" ] && [ "$COUNTER" -ge "$ITERATIONS" ]; then
    echo "Completed $COUNTER iterations. Exiting."
    break
  fi
  
  COUNTER=$((COUNTER + 1))
  echo ""
  echo "=========================================="
  echo "Iteration $COUNTER / $ITERATIONS"
  echo "Time: $(date)"
  echo "=========================================="

  # Fetch first open issue with 'agent-ready' label, ordered by issue number (oldest first)
  ISSUE_JSON=$(gh issue list --label "agent-ready" --state open --limit 100 --json number,title,body,labels --jq 'sort_by(.number) | .[0]')
  ISSUE_NUMBER=$(echo "$ISSUE_JSON" | jq -r '.number // empty')

  if [ -z "$ISSUE_NUMBER" ] || [ "$ISSUE_NUMBER" == "null" ]; then
    echo "No agent-ready issues found. Waiting 60 seconds..."
    sleep 60
    continue
  fi

  ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.title')
  ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body')
  ISSUE_LABELS=$(echo "$ISSUE_JSON" | jq -r '.labels[].name' | tr '\n' ', ')

  echo "📋 Working on Issue #$ISSUE_NUMBER: $ISSUE_TITLE"
  echo "Labels: $ISSUE_LABELS"

  # Track current issue for cleanup on crash
  CURRENT_ISSUE="$ISSUE_NUMBER"

  # Update labels: remove agent-ready, add in-progress
  gh issue edit "$ISSUE_NUMBER" --add-label "in-progress" --remove-label "agent-ready"

  # Get the phase from labels for context
  PHASE=$(echo "$ISSUE_LABELS" | grep -oE 'phase-[0-9]+' | head -1 || echo "unknown")

  # ============================================================
  # STEP 1: Starting Work
  # ============================================================
  add_comment "$ISSUE_NUMBER" "🚀 Step 1: Starting Work" "
**Agent picked up this issue**

- **Task:** $ISSUE_TITLE
- **Approach:** TDD (Tests First)
- **Phase:** $PHASE
- **Mode:** Agent Teams (up to 10 parallel agents)

**Workflow:**
1. 🔨 Implementation (TDD with Agent Teams)
2. 📝 Summary
3. 🔍 QA Review & Fix
4. 📊 QA Report
5. 📚 CLAUDE.md Update
6. ✅ Complete
"

  # ============================================================
  # STEP 2: Implementation Phase
  # ============================================================
  add_comment "$ISSUE_NUMBER" "🔨 Step 2: Implementation" "
Starting TDD implementation with Agent Teams...

- Spawning up to 10 parallel agents for efficient work
- Writing tests first
- Implementing to make tests pass
"

  # Construct the prompt for Claude - Implementation Phase
  # CUSTOMIZE THIS FOR YOUR PROJECT
  IMPL_PROMPT="
GitHub Issue #$ISSUE_NUMBER: $ISSUE_TITLE
Phase: $PHASE

## Issue Description:
$ISSUE_BODY

## Instructions - IMPLEMENTATION PHASE:
You are implementing features for this project.

### TDD Workflow:
1. **Read context**: Check any CLAUDE.md files for patterns
2. **Write tests FIRST**: Create failing tests that define expected behavior
3. **Implement**: Write code to make the tests pass
4. **Verify**: Run tests, type checks, and linting
5. **Commit**: Make descriptive commits referencing #$ISSUE_NUMBER

### Commands:
- Build: \`npm run build\`
- Test: \`npm run test\`
- Typecheck: \`npm run typecheck\`
- Lint: \`npm run lint\`

### Quality Requirements:
- All tests must pass
- No TypeScript errors
- Follow existing code patterns

## AGENT TEAMS INSTRUCTION:
Use Agent Teams with up to 10 parallel agents to efficiently complete this task.

Provide a detailed summary of:
1. What tests were written
2. What was implemented
3. How many agents were used
4. Commands run and their results
5. Any issues encountered
"

  echo "🔨 Running Implementation Phase with Agent Teams..."
  
  IMPL_RESULT=$(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 claude --dangerously-skip-permissions -p "$IMPL_PROMPT" 2>&1) || {
    echo "❌ Implementation failed"
    add_comment "$ISSUE_NUMBER" "❌ Implementation Error" "
The agent encountered an error during implementation:

\`\`\`
$(echo "$IMPL_RESULT" | tail -c 2000)
\`\`\`

Moving back to agent-ready for retry.
"
    gh issue edit "$ISSUE_NUMBER" --add-label "agent-ready" --remove-label "in-progress"
    sleep 10
    continue
  }

  IMPL_SUMMARY=$(echo "$IMPL_RESULT" | tail -c 4000)

  # ============================================================
  # STEP 3: Implementation Summary
  # ============================================================
  add_comment "$ISSUE_NUMBER" "📝 Step 3: Implementation Summary" "
**Implementation completed.**

\`\`\`
$(echo "$IMPL_SUMMARY" | head -c 3500)
\`\`\`

Proceeding to QA Review & Fix...
"

  # ============================================================
  # STEP 4: QA Review & Fix
  # ============================================================
  add_comment "$ISSUE_NUMBER" "🔍 Step 4: QA Review & Fix" "
Starting Quality Assurance with Agent Teams...

- Running full test suite
- Checking types and linting
- **Fixing any issues found**
"

  QA_PROMPT="
GitHub Issue #$ISSUE_NUMBER: $ISSUE_TITLE

## QA REVIEW & FIX PHASE

You must perform Quality Assurance AND fix any issues found.

### Previous Implementation:
$IMPL_SUMMARY

### Your Tasks:

1. **Review All Changes**
   \`\`\`bash
   git diff HEAD~1
   \`\`\`

2. **Run All Checks**
   \`\`\`bash
   npm run test
   npm run typecheck
   npm run lint
   \`\`\`

3. **FIX ANY ISSUES** (Critical!)
   - If tests fail → FIX THEM
   - If type errors → FIX THEM
   - If lint errors → FIX THEM
   - Commit each fix: 'fix: description (#$ISSUE_NUMBER)'

4. **Verify Everything Works**
   - Re-run all checks after fixes

5. **Final Status**
   - All tests pass? ✅/❌
   - All types pass? ✅/❌
   - All lint pass? ✅/❌

YOU MUST ACTUALLY FIX ISSUES, NOT JUST REPORT THEM.
"

  echo "🔍 Running QA Review & Fix with Agent Teams..."
  
  QA_RESULT=$(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 claude --dangerously-skip-permissions -p "$QA_PROMPT" 2>&1) || {
    echo "⚠️ QA phase had issues"
    QA_RESULT="QA phase encountered an error. Manual review may be needed."
  }

  QA_SUMMARY=$(echo "$QA_RESULT" | tail -c 4000)

  # ============================================================
  # STEP 5: QA Report
  # ============================================================
  add_comment "$ISSUE_NUMBER" "📊 Step 5: QA Report" "
## Quality Assurance Complete

$QA_SUMMARY

---

### All Changes
\`\`\`
$(git diff --name-only HEAD~2 2>/dev/null | head -30 || echo "Unable to get diff")
\`\`\`

### Recent Commits
\`\`\`
$(git log --oneline -5 2>/dev/null || echo "No commits")
\`\`\`
"

  # ============================================================
  # STEP 6: CLAUDE.md Update
  # ============================================================
  add_comment "$ISSUE_NUMBER" "📚 Step 6: CLAUDE.md Update" "
Updating CLAUDE.md files with learnings from this task...
"

  CHANGED_DIRS=$(git diff --name-only HEAD~2 2>/dev/null | xargs -I{} dirname {} | sort -u | head -20 || echo ".")

  CLAUDE_MD_PROMPT="
## CLAUDE.md Maintenance - Final Step

Issue #$ISSUE_NUMBER: $ISSUE_TITLE is complete.

### Changed Directories:
$CHANGED_DIRS

### Your Task:
Update CLAUDE.md files to document what was learned.

**Root CLAUDE.md** (create if missing):
- Repo purpose, global commands
- Routing: 'For X, see @path/CLAUDE.md'
- Under 500 lines

**Subdirectory CLAUDE.md** (for meaningful boundaries):
- What module does
- Local commands
- Patterns / Anti-patterns
- Gotchas from this task

Commit: 'docs: update CLAUDE.md for #$ISSUE_NUMBER'
"

  echo "📚 Running CLAUDE.md Update with Agent Teams..."
  
  CLAUDE_MD_RESULT=$(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 claude --dangerously-skip-permissions -p "$CLAUDE_MD_PROMPT" 2>&1) || {
    echo "⚠️ CLAUDE.md update had issues"
    CLAUDE_MD_RESULT="CLAUDE.md update encountered an error."
  }

  CLAUDE_MD_SUMMARY=$(echo "$CLAUDE_MD_RESULT" | tail -c 2500)

  add_comment "$ISSUE_NUMBER" "📚 Step 6 Complete: CLAUDE.md Updated" "
**Documentation updated:**

\`\`\`
$(echo "$CLAUDE_MD_SUMMARY" | head -c 2000)
\`\`\`
"

  # ============================================================
  # STEP 7: Task Complete
  # ============================================================
  COMMITS=$(git log --oneline -7 2>/dev/null | head -7 || echo "No commits")
  
  add_comment "$ISSUE_NUMBER" "✅ Step 7: Task Complete" "
## Issue #$ISSUE_NUMBER Successfully Completed

### Summary
| Step | Status |
|------|--------|
| Implementation | ✅ Done |
| Tests Written | ✅ Done |
| QA Review & Fix | ✅ Done |
| CLAUDE.md | ✅ Updated |

### Commits
\`\`\`
$COMMITS
\`\`\`

---
*🤖 Completed by agent loop*
*📅 $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
*🔄 Iteration $COUNTER*
"

  # Close issue
  gh issue close "$ISSUE_NUMBER"
  gh issue edit "$ISSUE_NUMBER" --add-label "completed" --remove-label "in-progress"

  # Clear current issue tracker
  CURRENT_ISSUE=""

  echo "✅ Issue #$ISSUE_NUMBER completed!"
  
  # Push changes to remote
  echo "📤 Pushing changes to remote..."
  git push origin main --no-verify 2>/dev/null || echo "⚠️ Push failed (will retry later)"
  
  sleep 5
done

echo "=========================================="
echo "Agent Loop Finished - $COUNTER iterations"
echo "=========================================="

Make it executable and run:

chmod +x scripts/loop-github.sh
./scripts/loop-github.sh -1

This pattern has processed hundreds of issues across my projects. It's not magic—you still need good issues and a codebase with clear patterns—but for routine implementation work, it frees up a lot of time.

Start with a few test issues, tune the prompts, and scale up as you trust it more. The crash recovery means you can leave it running without babysitting.