- Published on
Automating Development with GitHub Agent Loops
- Authors

- Name
- Avasdream
- @avasdream_
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
| Label | Meaning |
|---|---|
agent-ready | Issue is ready to be picked up |
in-progress | Agent is currently working on it |
completed | Successfully 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:
- Starting Work – Agent announces pickup
- Implementation – TDD with Agent Teams (up to 10 parallel agents)
- Summary – Documents what was built
- QA Review & Fix – Runs tests, fixes any failures
- QA Report – Final status with diffs
- CLAUDE.md Update – Captures learnings
- 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.