A generic issue-to-PR generator that automatically reads GitHub issues, generates solutions tailored to the project's language and architecture, tests them, and opens pull requests. Works with any repository by detecting project skills and patterns.
This tool transforms GitHub issues into complete pull requests through an intelligent, project-aware workflow:
preflight ──(fail)──────────────────────────────────────────────────────────► fail_state
│
▼
load_project_context ──(repo mismatch)──────────────────────────────────────► fail_state
│
▼
read_issue → plan → execute_step ──(more steps)──► execute_step (loop)
│
(all done)
▼
reflect → test ──(pass)──────────────────────────► create_pr
│
└──(fail)──► tester ──(APPROVED)─────► create_pr
│
(NEEDS_WORK, retry remaining)
│
→ plan (re-plan with feedback)
│
(max retries exhausted)
▼
fail_state
preflight clones the target repo to local_repo_path if it is missing or not a git repository. If the clone fails the graph routes immediately to fail_state before any LLM work begins.
- preflight — Clones the target repository to
local_repo_pathif the directory is missing or not a git repo. Any failure aborts immediately before any LLM work begins. - load_project_context — Validates the repo, loads skills, and generates a compressed project summary (cached in Redis → PostgreSQL). Fails immediately if
--repodoes not match the remote origin. - read_issue — Fetches issue title + body from GitHub.
- plan — Single LLM call producing a structured implementation plan; parses the "Ordered steps" section into a
list[str]of discrete tasks. Resetsstep_idx,step_results, andmodified_filesfor a clean execution cycle. Result cached (L1 local LRU + L2 Redis, 60 s TTL) keyed byuser_id:repo:issue:retry_count. - execute_step — Runs one ReAct loop per plan step. Loops back to itself via
route_after_executeuntil all steps are done, then advances toreflect. Accumulatesmodified_files(deduplicated) andstep_resultsacross iterations; the last step's test code is forwarded to the test node. - reflect — Logs a summary of what was accomplished across all steps. Informational only — no state mutation.
- test — Writes the generated test file to the repo, runs it in-place using the project's detected test runner (
pytest,jest,go test, etc.), then removes the file. Returnssuccessorretry. - tester — On failure: analyzes execution output and emits a verdict:
APPROVED— solution satisfies the issue requirements (tests were wrong but code is correct); routes directly to PR creation.NEEDS_WORK— structured feedback (root cause, code issues, test issues, suggestions) fed back into the nextplancall.
- create_pr — Creates a new branch, commits the solution, pushes, and opens a pull request directly.
python main.py run --repo owner/repo --issue 42 --path /path/to/repo [--base main]
# Or start the HTTP server (default):
python main.pyThe agent automatically discovers and installs the right skills for your project using the find-skills CLI. After building a compressed summary of the repository (README, dependencies, entry point, file tree), it queries the skills.sh registry to find matching agent skills and installs them — no manual configuration needed.
This enables:
- Language-specific code generation (Python, JavaScript, Go, Rust, Java, etc.)
- Framework-aware patterns (Django ORM, FastAPI, Express, etc.)
- Correct test syntax (pytest, jest, unittest, mocha, etc.)
- Proper package management (pip, npm, cargo, etc.)
- Python 3.12+
- GitHub credentials (PAT or App)
- Redis (for context caching)
- PostgreSQL with pgvector (optional, for semantic search)
ast-grepCLI (for code analysis)- Node.js /
npx(for skills discovery)
git clone https://github.com/yourusername/story-pr-agent.git
cd story-pr-agentuv syncnpm install -g @ast-grep/cliredis-server
# Or via Docker:
docker run -d -p 6379:6379 redis:latestcp .env.example .envEdit .env:
# Required
ANTHROPIC_API_KEY=sk-ant-...
# GitHub auth — choose one option:
GITHUB_TOKEN=ghp_...
# OR
GITHUB_APP_ID=123456
GITHUB_PRIVATE_KEY_PATH=/path/to/private-key.pem
GITHUB_INSTALLATION_ID=78901234
# Storage
REDIS_URL=redis://localhost:6379
POSTGRES_VEC_URL=postgresql://user:password@localhost/vectordb # Optional
# Agent tuning
MAX_RETRIES=3
LLM_MODEL=claude-sonnet-4-6
BASE_BRANCH=mainTo enable smart project detection and generation, add a skills.sh file to your repository root:
#!/bin/bash
# skills.sh - Project skills and specifications
# Core language
language: "python"
python_version: "3.11"
# Framework/Libraries
framework: "django"
framework_version: "4.2"
# Testing
test_runner: "pytest"
test_directory: "tests/"
# Package management
package_manager: "pip"
requirements_file: "requirements.txt"
# Code quality
code_formatter: "black"
type_checker: "mypy"
# Special capabilities
async_support: "true"
orm_used: "django-orm"The agent understands:
- Languages: python, javascript, typescript, go, rust, java, c++, php, ruby
- Frameworks: django, fastapi, flask, express, nextjs, nuxt, echo, actix, spring, laravel, rails
- Test runners: pytest, jest, unittest, mocha, go test, cargo test, junit
- Package managers: pip, npm, yarn, pnpm, cargo, go modules, maven, bundler
python main.py run \
--repo owner/repo \
--issue 42 \
--path /path/to/local/clone \
--base main| Flag | Required | Description |
|---|---|---|
--repo |
yes | owner/repo format |
--issue |
yes | Issue number |
--path |
yes | Path to local clone of repo |
--base |
no | Base branch (default: main) |
--user |
no | User ID for cache key (default: cli) |
python main.py serve --port 8000Start a run:
curl -X POST http://localhost:8000/run \
-H "Content-Type: application/json" \
-d '{"repo": "owner/repo", "issue": 42, "path": "/path/to/local/clone"}'
# → {"run_id": "<uuid>", "status": "running"}Poll for the result:
curl http://localhost:8000/run/<uuid>
# → {"run_id": "...", "status": "done", "agent_status": "pr_created", "pr_url": "https://..."}| Endpoint | Method | Description |
|---|---|---|
/run |
POST |
Start a new agent run; returns run_id immediately |
/run/<run_id> |
GET |
Poll status: running, done, or error |
/health |
GET |
Liveness check |
Because the workflow involves multiple LLM calls and git operations (30–120 s), the endpoint returns 202 Accepted with a run_id and runs the graph in a background thread.
uvicorn webhook:app --host 0.0.0.0 --port 8000Configure GitHub webhook:
- Payload URL:
https://your-server/webhook - Content type:
application/json - Events: Issues
- Secret: Set
GITHUB_WEBHOOK_SECRETin.env
Trigger by labeling an issue with generate-pr
Before any LLM call or context work, the agent runs a preflight node that clones the target repository to local_repo_path if the directory is missing or not a git repository. If the clone fails, the graph routes immediately to fail_state and no tokens are spent.
If the repo is already present the node is a no-op and execution continues immediately.
Before processing, the agent validates:
- ✓ Local path exists and is a git repository
- ✓ Remote origin is configured and accessible
- ✓
--repomatches the remote origin URL
If --repo does not match the remote origin, the workflow sets status: repo_mismatch and routes directly to fail_state — no skills loading, no context generation, no LLM calls. The CLI exits with code 1 and logs the conflicting repo and path.
After generating the compressed project context, the agent runs npx skills find with the full context text as the search query. Matching skills are then installed automatically via npx skills add.
Requirements:
- Node.js must be installed (
npxavailable on$PATH) - Internet access to reach the skills.sh registry
The agent analyzes the project structure:
- File organization and directory layout
- Existing functions and classes (via ast-grep)
- Code patterns and conventions
- Import structure and dependencies
- Testing patterns and locations
This context is:
- Cached in Redis for fast retrieval on repeated runs
- Compressed to control LLM token usage
- Injected into all prompts to guide code generation
The core of the agent is a three-phase loop replacing the old single-shot code node:
A single LLM call produces a structured implementation plan for the issue. The planner outputs a markdown document with an Ordered steps section; the agent parses this into a list[str] of discrete tasks (e.g. "Add model field", "Update serializer", "Write migration").
The plan is cached per retry attempt so a downstream failure doesn't force re-planning. On tester retry the plan node re-runs with the tester's structured feedback, producing a revised step list for the next execution cycle.
execute_step runs one ReAct loop for each plan step, cycling back to itself until all steps are done:
| Step | Tools used |
|---|---|
| Explore structure | list_directory, read_file |
| Locate targets | find_functions, search_code_patterns |
| Apply changes | str_replace_in_file (targeted edit), write_file (new file) |
| Finish last step | Emits ### TESTS <unit test code> |
Each iteration accumulates modified_files (deduplicated across steps) and a step_results summary. The loop cap is 10 ReAct iterations per step and 5000 output tokens per LLM call.
After all steps complete, the reflect node logs the full plan and per-step summaries. It makes no state changes — it exists purely for observability before handing off to the test node.
Tests run directly in the local repository clone — no Docker sandbox required:
- The generated test file is written to the repo root under a runner-specific filename (
test_solution.py,test_solution.test.js, etc.) - The test runner command is executed with
cwd=local_repo_path - The test file is removed in a
finallyblock regardless of outcome
Supported runners and their commands:
| Runner | Command |
|---|---|
pytest |
pytest test_solution.py -v |
jest |
npx jest test_solution.test.js --no-coverage |
go test |
go test ./... |
cargo test |
cargo test |
unittest |
python -m unittest test_solution |
After each full execute cycle:
- The generated tests run in the local repo using the project's detected test runner.
- On failure, the tester analyzes execution output and emits a verdict:
APPROVED— solution satisfies the issue requirements; routes directly to PR creation.NEEDS_WORK— root cause, code issues, test issues, and concrete suggestions are fed back into the nextplancall.
- On retry,
planre-runs with the tester's feedback and resetsstep_idx,step_results, andmodified_filesfor a fresh execution cycle. - The loop repeats until tests pass, an
APPROVEDverdict is received, orMAX_RETRIESis exhausted.
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
— | Required Anthropic API key |
GITHUB_TOKEN |
— | GitHub Personal Access Token (auth method A) |
GITHUB_APP_ID |
3770889 |
GitHub App ID (auth method B) |
GITHUB_PRIVATE_KEY_PATH |
— | Path to GitHub App private key |
GITHUB_INSTALLATION_ID |
— | GitHub App installation ID |
GITHUB_WEBHOOK_SECRET |
— | HMAC secret for webhook requests |
GITHUB_CLIENT |
— | GitHub OAuth App client ID (web UI login) |
GITHUB_SECRET |
— | GitHub OAuth App client secret (web UI login) |
GOOGLE_OAUTH_CLIENT_ID |
— | Google OAuth client ID (optional, web UI) |
GOOGLE_OAUTH_CLIENT_SECRET |
— | Google OAuth client secret (optional, web UI) |
MAX_RETRIES |
3 |
Max full plan→execute→test cycles before giving up |
LLM_MODEL |
claude-sonnet-4-6 |
Claude model to use for all LLM calls |
BASE_BRANCH |
main |
Default PR target branch |
REDIS_URL |
redis://localhost:6379 |
Redis — L1 context cache (60 s TTL) |
POSTGRES_VEC_URL |
postgresql://…/vectordb |
PostgreSQL — L2 context cache (persistent fallback) |
LANGCHAIN_TRACING_V2 |
false |
Enable LangSmith tracing |
LANGCHAIN_API_KEY |
— | LangSmith API key |
LANGCHAIN_PROJECT |
story-pr-agent |
LangSmith project name |
.
├── main.py # CLI entry point (run / serve subcommands)
├── app.py # Flask REST API server
├── webhook.py # FastAPI webhook server (GitHub events)
├── graph.py # LangGraph state machine
├── state.py # State schema & routing functions
├── config.py # Settings, LLMClient, LLM factory helpers
├── .env.example # Environment variable template
├── actions/
│ ├── git_nodes.py # Git operations: read issue, create PR
│ ├── llm_nodes.py # plan_solution, execute_step, reflect, test_code, tester_review
│ ├── context_nodes.py # Preflight, repo validation, skills loading, context gen
│ ├── planner_prompt.md # Structured planning prompt (outputs Ordered steps section)
│ ├── coder_prompt.md # Per-step ReAct coder prompt (explore → edit → output tests)
│ └── tester_prompt.md # Failure analysis prompt (APPROVED / NEEDS_WORK)
├── cache/
│ ├── __init__.py # Public API: get_local_cache(), make_cache_key()
│ ├── _llm.py # LLM response cache (L1 local + L2 Redis)
│ ├── _redis.py # RedisClient with retry on connection errors
│ └── _pg.py # PostgreSQL persistent context cache (L2 fallback)
├── tools/
│ ├── _retry.py # Shared RetryableError + tenacity config constants
│ ├── executor.py # TestRunner: writes test file, runs in-repo, cleans up
│ ├── github.py # GitHubClient + GitHubOAuthClient (retryable)
│ ├── git_ops.py # Git CLI operations (branch, commit, push)
│ ├── npx_client.py # NPXClient: skills find/add via npx subprocess
│ ├── ast_grep.py # ast-grep CLI wrapper
│ ├── langchain_tools.py # LLM-callable tools: search + file read/write/edit
│ ├── context_generator.py # Project context generation
│ └── context_compression.py # Context compression strategies
├── auth/
│ └── db.py # MongoDBClient (retryable) + module-level helpers
├── cli_runner/
│ ├── runner.py # Parallel check runner + colored result formatter
│ └── checks/ # Individual service health check modules
└── README.md # This file
| Language | Framework | Test | Status |
|---|---|---|---|
| Python | Django, FastAPI, Flask | pytest, unittest | ✅ Full support |
| JavaScript | Express, Next.js | Jest, Mocha | ✅ Full support |
| Go | Gin, Echo | Go test | ✅ Full support |
| TypeScript | NestJS, Remix | Jest | ✅ Full support |
| Rust | Actix, Rocket | Cargo test | 🟡 Partial |
| Java | Spring | JUnit | 🟡 Partial |
Full support: Code generation + testing working reliably Partial support: Code generation works, testing may need manual config
# Repository with skills.sh:
language: python
framework: django
test_runner: pytest
# Issue: "Add user authentication endpoint"
python main.py run --repo myorg/myproject --issue 123 --path /path/to/myproject
# Agent:
# 1. Plans: ["Add model field", "Update serializer", "Add view", "Write pytest tests"]
# 2. Executes each step via ReAct loop (reads files, edits in-place)
# 3. Runs pytest directly in the repo
# 4. Opens PR with all changes# Repository with skills.sh:
language: javascript
framework: express
test_runner: jest
# Issue: "Add rate limiting middleware"
python main.py run --repo myorg/api --issue 456 --path /path/to/api
# Agent:
# 1. Plans: ["Install express-rate-limit", "Write middleware", "Wire into app", "Write jest tests"]
# 2. Executes each step, accumulating modified files across steps
# 3. Runs jest directly in the repo
# 4. Opens PR ready to mergeBefore running the agent, verify all external service connections:
python -m cli_runner # check all services
python -m cli_runner --only github,ast_grep # check specific services
python -m cli_runner --list # list available checks
python -m cli_runner --verbose # show full tracebacks on failureNote: the preflight node inside the graph runs the GitHub and ast-grep checks automatically on every run. cli_runner is for manual validation and CI gating before invoking the agent.
- Ensure Redis is running:
redis-cli ping - Check
REDIS_URLin.env
--repomismatch: The--repo owner/repovalue must match the remote origin of the local clone at--path. Check withgit -C /path/to/repo remote get-url origin.
- Ensure the detected test runner is installed in the target repo's environment (e.g.
pytest,jest). - The test file is always cleaned up — check the repo for a leftover
test_solution.*file if a crash occurred.
- Ensure
npxis available:npx --version - Check internet access — the skills.sh registry must be reachable
- A warning is logged and the agent continues without skills if none are found
- Check
ANTHROPIC_API_KEYis valid - Verify Claude model in
.envis available - Check token limits (Claude Sonnet has 200K context)
- LLM calls time out after 120 s read / 10 s connect and retry up to 4 times on 429/5xx
Contributions welcome! When adding support for new languages/frameworks:
- Add the test runner command to
_TEST_COMMANDSintools/executor.py - Add language support to
context_generator.py - Update
skills.shexample in this README - Add to "Capabilities by Language" table
- Test with example repository
MIT
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📖 Docs: See README.md