AI-Powered GitHub PR Template Generation (#1618)
* auto-claude: subtask-1-1 - Create PR template filler agent system prompt Add system prompt at apps/backend/prompts/github/pr_template_filler.md that instructs the agent to receive PR template, diff summary, spec overview, commit history, and branch context, then fill every section intelligently with accurate descriptions and appropriate checkboxes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-2 - Create pr_template_filler agent module Add apps/backend/agents/pr_template_filler.py with: - detect_pr_template(): finds .github/PULL_REQUEST_TEMPLATE.md or .github/PULL_REQUEST_TEMPLATE/ directory templates - run_pr_template_filler(): async function that gathers change context, truncates large diffs to file-level summaries, builds a prompt with template content and context, and invokes Claude via create_client() + run_agent_session() with agent_type='pr_template_filler' - Helper functions for diff truncation, prompt building, and spec loading Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-3 - Register pr_template_filler agent in AGENT_CONFIGS * auto-claude: subtask-2-1 - Integrate AI PR template filler into create_pull_request() Add AI-powered PR body generation to WorktreeManager.create_pull_request(): - detect_pr_template() checks for .github/PULL_REQUEST_TEMPLATE.md - Gathers diff summary and commit log from git for context - Calls run_pr_template_filler() with 30s timeout via asyncio - Falls back to _extract_spec_summary() on any failure - Handles both sync and async calling contexts gracefully * auto-claude: subtask-3-1 - Add pr_template_filler agent config to AgentTools.tsx Register the pr_template_filler entry in the frontend AGENT_CONFIGS with category='utility', tools=['Read', 'Glob', 'Grep'], and feature settings source matching the existing utility agent pattern. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-3-2 - Add i18n translation keys for pr_template_filler agent Add English i18n keys for the PR Template Filler agent under a new 'agents' section in settings.json with label and description entries. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-3-3 - Add French i18n translation keys for pr_template_filler agent Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-4-1 - Fix detect_pr_template to accept str and verify integration - Accept str | Path in detect_pr_template() for robustness - Verified module imports cleanly - Verified AGENT_CONFIGS contains pr_template_filler entry - Verified detect_pr_template() correctly finds/misses templates - All 1969 existing backend tests pass with no regressions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-template): improve accuracy and markdown rendering - Add _strip_markdown_fences() to remove code block wrappers from AI response so PR body renders correctly on GitHub instead of as raw code - Update prompt with detailed checkbox guidelines: - Distinguish between inferable checkboxes (type, area, base branch) and verification-required checkboxes (tested locally, CI passes, platform tested) - Instruct AI to leave unverifiable checkboxes unchecked - Add guidance for platform/code quality checkboxes - Remove markdown code fence wrapper from template in prompt to reduce likelihood of AI wrapping output in fences Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-template): respect user model config and enhance diff context Address PR review findings: - HIGH: Replace hardcoded model="sonnet" with get_utility_model_config() to respect user configuration via UTILITY_MODEL_ID env var - LOW: Add actual code changes to PR context via git diff -p with 30k char truncation for better AI template generation accuracy Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-template): sort imports per ruff I001 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-template): format import per ruff I001 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: run ruff format on pr_template_filler and worktree Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pr-template): unpack all 3 return values from run_agent_session run_agent_session returns (status, response, error_info) tuple. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
PR Template Filler Agent Module
|
||||
================================
|
||||
|
||||
Detects GitHub PR templates in a project and uses Claude to intelligently
|
||||
fill them based on code changes, spec context, commit history, and branch info.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from core.client import create_client
|
||||
from task_logger import LogPhase, get_task_logger
|
||||
|
||||
from .session import run_agent_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Maximum diff size (in characters) before truncating to file-level summaries
|
||||
MAX_DIFF_CHARS = 30_000
|
||||
|
||||
|
||||
def detect_pr_template(project_dir: Path | str) -> str | None:
|
||||
"""
|
||||
Detect a GitHub PR template in the project.
|
||||
|
||||
Searches for:
|
||||
1. .github/PULL_REQUEST_TEMPLATE.md (single template)
|
||||
2. .github/PULL_REQUEST_TEMPLATE/ directory (picks the first .md file)
|
||||
|
||||
Args:
|
||||
project_dir: Root directory of the project
|
||||
|
||||
Returns:
|
||||
The template content as a string, or None if no template is found.
|
||||
"""
|
||||
project_dir = Path(project_dir)
|
||||
# Check for single template file
|
||||
single_template = project_dir / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
||||
if single_template.is_file():
|
||||
try:
|
||||
content = single_template.read_text(encoding="utf-8")
|
||||
if content.strip():
|
||||
logger.info(f"Found PR template: {single_template}")
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read PR template {single_template}: {e}")
|
||||
|
||||
# Check for template directory (pick first .md file alphabetically)
|
||||
template_dir = project_dir / ".github" / "PULL_REQUEST_TEMPLATE"
|
||||
if template_dir.is_dir():
|
||||
try:
|
||||
md_files = sorted(template_dir.glob("*.md"))
|
||||
if md_files:
|
||||
content = md_files[0].read_text(encoding="utf-8")
|
||||
if content.strip():
|
||||
logger.info(f"Found PR template: {md_files[0]}")
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read PR template from {template_dir}: {e}")
|
||||
|
||||
logger.info("No GitHub PR template found in project")
|
||||
return None
|
||||
|
||||
|
||||
def _truncate_diff(diff_summary: str) -> str:
|
||||
"""
|
||||
Truncate a large diff to file-level summaries to stay within token limits.
|
||||
|
||||
If the diff is within MAX_DIFF_CHARS, return it unchanged.
|
||||
Otherwise, extract only file-level change summaries (e.g. file names
|
||||
with insertions/deletions counts) and discard line-level detail.
|
||||
|
||||
Args:
|
||||
diff_summary: The full diff summary text
|
||||
|
||||
Returns:
|
||||
The original or truncated diff summary.
|
||||
"""
|
||||
if len(diff_summary) <= MAX_DIFF_CHARS:
|
||||
return diff_summary
|
||||
|
||||
lines = diff_summary.splitlines()
|
||||
summary_lines: list[str] = []
|
||||
summary_lines.append("(Diff truncated to file-level summaries due to size)")
|
||||
summary_lines.append("")
|
||||
|
||||
for line in lines:
|
||||
# Keep file-level summary lines (stat lines, file headers, etc.)
|
||||
stripped = line.strip()
|
||||
if (
|
||||
stripped.startswith("diff --git")
|
||||
or stripped.startswith("---")
|
||||
or stripped.startswith("+++")
|
||||
or "file changed" in stripped.lower()
|
||||
or "files changed" in stripped.lower()
|
||||
or "insertion" in stripped.lower()
|
||||
or "deletion" in stripped.lower()
|
||||
or stripped.startswith("rename")
|
||||
or stripped.startswith("new file")
|
||||
or stripped.startswith("deleted file")
|
||||
or stripped.startswith("Binary files")
|
||||
):
|
||||
summary_lines.append(line)
|
||||
|
||||
# If we couldn't extract meaningful summaries, take the first chunk
|
||||
if len(summary_lines) <= 2:
|
||||
truncated = diff_summary[:MAX_DIFF_CHARS]
|
||||
return truncated + "\n\n(... diff truncated due to size)"
|
||||
|
||||
return "\n".join(summary_lines)
|
||||
|
||||
|
||||
def _strip_markdown_fences(content: str) -> str:
|
||||
"""
|
||||
Strip markdown code fences from the response if present.
|
||||
|
||||
The AI sometimes wraps the output in ```markdown ... ``` even when instructed
|
||||
not to. This ensures the PR body renders correctly on GitHub.
|
||||
|
||||
Args:
|
||||
content: The response content to clean
|
||||
|
||||
Returns:
|
||||
The content with markdown fences stripped.
|
||||
"""
|
||||
result = content
|
||||
|
||||
# Strip opening fence (```markdown or just ```)
|
||||
if result.startswith("```markdown"):
|
||||
result = result[len("```markdown") :].lstrip("\n")
|
||||
elif result.startswith("```md"):
|
||||
result = result[len("```md") :].lstrip("\n")
|
||||
elif result.startswith("```"):
|
||||
result = result[3:].lstrip("\n")
|
||||
|
||||
# Strip closing fence
|
||||
if result.endswith("```"):
|
||||
result = result[:-3].rstrip("\n")
|
||||
|
||||
return result.strip()
|
||||
|
||||
|
||||
def _build_prompt(
|
||||
template_content: str,
|
||||
diff_summary: str,
|
||||
spec_overview: str,
|
||||
commit_log: str,
|
||||
branch_name: str,
|
||||
target_branch: str,
|
||||
) -> str:
|
||||
"""
|
||||
Build the prompt for the PR template filler agent.
|
||||
|
||||
Combines the system prompt context variables into a single message
|
||||
that includes the template and all change context.
|
||||
|
||||
Args:
|
||||
template_content: The PR template markdown
|
||||
diff_summary: Git diff summary (possibly truncated)
|
||||
spec_overview: Spec.md content or summary
|
||||
commit_log: Git log of commits in the PR
|
||||
branch_name: Source branch name
|
||||
target_branch: Target branch name
|
||||
|
||||
Returns:
|
||||
The assembled prompt string.
|
||||
"""
|
||||
return f"""Fill out the following GitHub PR template using the provided context.
|
||||
Return ONLY the filled template markdown — no preamble, no explanation, no code fences.
|
||||
|
||||
## Checkbox Guidelines
|
||||
|
||||
IMPORTANT: Be accurate and honest about what has and hasn't been verified.
|
||||
|
||||
**Check these based on context (you can infer from the diff/spec):**
|
||||
- Base Branch targeting — check based on target_branch value
|
||||
- Type of Change (bug fix, feature, docs, refactor, test) — infer from diff and spec
|
||||
- Area (Frontend, Backend, Fullstack) — infer from changed file paths
|
||||
- Feature Toggle "N/A" — if the feature appears complete and not behind a flag
|
||||
- Breaking Changes "No" — if changes appear backward compatible
|
||||
|
||||
**Leave UNCHECKED (these require human verification you cannot perform):**
|
||||
- "I've tested my changes locally" — you have not tested anything
|
||||
- "All CI checks pass" — CI has not run yet
|
||||
- "Windows/macOS/Linux tested" — requires manual testing on each platform
|
||||
- "All existing tests pass" — CI has not run yet
|
||||
- "New features include test coverage" — unless test files are clearly visible in the diff
|
||||
- "Bug fixes include regression tests" — unless test files are clearly visible in the diff
|
||||
|
||||
**For platform/code quality checkboxes:**
|
||||
- "Used centralized platform/ module" — leave unchecked unless you can verify from the diff
|
||||
- "No hardcoded paths" — leave unchecked unless you can verify from the diff
|
||||
- "PR is small and focused (< 400 lines)" — check only if diff stats show < 400 lines changed
|
||||
|
||||
**For the "I've synced with develop branch" checkbox:**
|
||||
- Leave unchecked — you cannot verify the sync status
|
||||
|
||||
## PR Template
|
||||
|
||||
{template_content}
|
||||
|
||||
## Change Context
|
||||
|
||||
### Branch Information
|
||||
- **Source branch:** {branch_name}
|
||||
- **Target branch:** {target_branch}
|
||||
|
||||
### Git Diff Summary
|
||||
```
|
||||
{diff_summary}
|
||||
```
|
||||
|
||||
### Spec Overview
|
||||
{spec_overview}
|
||||
|
||||
### Commit History
|
||||
```
|
||||
{commit_log}
|
||||
```
|
||||
|
||||
Fill every section of the PR template. Follow the checkbox guidelines above carefully.
|
||||
Output ONLY the completed template — no code fences, no preamble."""
|
||||
|
||||
|
||||
def _load_spec_overview(spec_dir: Path) -> str:
|
||||
"""
|
||||
Load the spec.md content for context. Falls back to a brief note if unavailable.
|
||||
|
||||
Args:
|
||||
spec_dir: Directory containing the spec files
|
||||
|
||||
Returns:
|
||||
The spec content or a fallback message.
|
||||
"""
|
||||
spec_file = spec_dir / "spec.md"
|
||||
if spec_file.is_file():
|
||||
try:
|
||||
content = spec_file.read_text(encoding="utf-8")
|
||||
# Truncate very long specs to keep prompt manageable
|
||||
if len(content) > 8000:
|
||||
return content[:8000] + "\n\n(... spec truncated for brevity)"
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read spec.md: {e}")
|
||||
return "(No spec overview available)"
|
||||
|
||||
|
||||
async def run_pr_template_filler(
|
||||
project_dir: Path,
|
||||
spec_dir: Path,
|
||||
model: str,
|
||||
thinking_budget: int | None = None,
|
||||
branch_name: str = "",
|
||||
target_branch: str = "develop",
|
||||
diff_summary: str = "",
|
||||
commit_log: str = "",
|
||||
verbose: bool = False,
|
||||
) -> str | None:
|
||||
"""
|
||||
Run the PR template filler agent to generate a filled PR body.
|
||||
|
||||
Detects the project's PR template, gathers change context, and invokes
|
||||
Claude to intelligently fill out the template sections.
|
||||
|
||||
Args:
|
||||
project_dir: Root directory of the project
|
||||
spec_dir: Directory containing the spec files
|
||||
model: Claude model to use
|
||||
thinking_budget: Max thinking tokens (None to disable extended thinking)
|
||||
branch_name: Source branch name for the PR
|
||||
target_branch: Target branch name for the PR
|
||||
diff_summary: Git diff summary of changes
|
||||
commit_log: Git log of commits included in the PR
|
||||
verbose: Whether to show detailed output
|
||||
|
||||
Returns:
|
||||
The filled template markdown string, or None if template detection fails
|
||||
or the agent encounters an error.
|
||||
"""
|
||||
# Detect PR template
|
||||
template_content = detect_pr_template(project_dir)
|
||||
if template_content is None:
|
||||
logger.info("No PR template detected — skipping template filler")
|
||||
return None
|
||||
|
||||
# Load spec overview
|
||||
spec_overview = _load_spec_overview(spec_dir)
|
||||
|
||||
# Truncate diff if too large
|
||||
truncated_diff = _truncate_diff(diff_summary)
|
||||
|
||||
# Build the prompt
|
||||
prompt = _build_prompt(
|
||||
template_content=template_content,
|
||||
diff_summary=truncated_diff,
|
||||
spec_overview=spec_overview,
|
||||
commit_log=commit_log,
|
||||
branch_name=branch_name,
|
||||
target_branch=target_branch,
|
||||
)
|
||||
|
||||
# Initialize task logger
|
||||
task_logger = get_task_logger(spec_dir)
|
||||
if task_logger:
|
||||
task_logger.start_phase(LogPhase.CODING, "PR template filling")
|
||||
|
||||
# Create client following the pattern from planner.py
|
||||
client = create_client(
|
||||
project_dir,
|
||||
spec_dir,
|
||||
model,
|
||||
agent_type="pr_template_filler",
|
||||
max_thinking_tokens=thinking_budget,
|
||||
)
|
||||
|
||||
try:
|
||||
async with client:
|
||||
status, response, _ = await run_agent_session(
|
||||
client, prompt, spec_dir, verbose, phase=LogPhase.CODING
|
||||
)
|
||||
|
||||
if task_logger:
|
||||
task_logger.end_phase(
|
||||
LogPhase.CODING,
|
||||
success=(status != "error"),
|
||||
message="PR template filling completed",
|
||||
)
|
||||
|
||||
if status == "error":
|
||||
logger.error("PR template filler agent returned an error")
|
||||
return None
|
||||
|
||||
# The agent should return only the filled template markdown
|
||||
if response and response.strip():
|
||||
result = _strip_markdown_fences(response.strip())
|
||||
logger.info("PR template filled successfully")
|
||||
return result
|
||||
|
||||
logger.warning("PR template filler returned empty response")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PR template filler error: {e}")
|
||||
if task_logger:
|
||||
task_logger.log_error(f"PR template filler error: {e}", LogPhase.CODING)
|
||||
return None
|
||||
@@ -263,6 +263,12 @@ AGENT_CONFIGS = {
|
||||
"auto_claude_tools": [],
|
||||
"thinking_default": "low",
|
||||
},
|
||||
"pr_template_filler": {
|
||||
"tools": BASE_READ_TOOLS, # Read-only — reads diff, template, spec
|
||||
"mcp_servers": [], # No MCP needed, context passed via prompt
|
||||
"auto_claude_tools": [],
|
||||
"thinking_default": "low", # Fast utility task for structured fill-in
|
||||
},
|
||||
"pr_reviewer": {
|
||||
"tools": BASE_READ_TOOLS + WEB_TOOLS, # Read-only
|
||||
"mcp_servers": ["context7"],
|
||||
|
||||
@@ -15,6 +15,7 @@ This allows:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -28,8 +29,11 @@ from typing import TypedDict, TypeVar
|
||||
|
||||
from core.gh_executable import get_gh_executable, invalidate_gh_cache
|
||||
from core.git_executable import get_git_executable, get_isolated_git_env, run_git
|
||||
from core.model_config import get_utility_model_config
|
||||
from debug import debug_warning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -1192,8 +1196,22 @@ class WorktreeManager:
|
||||
target = target_branch or self.base_branch
|
||||
pr_title = title or f"auto-claude: {spec_name}"
|
||||
|
||||
# Get PR body from spec.md if available
|
||||
pr_body = self._extract_spec_summary(spec_name)
|
||||
# Try AI-powered PR body from project's PR template, fall back to spec summary
|
||||
pr_body: str | None = None
|
||||
try:
|
||||
diff_summary, commit_log = self._gather_pr_context(spec_name, target)
|
||||
pr_body = self._try_ai_pr_body(
|
||||
spec_name=spec_name,
|
||||
target_branch=target,
|
||||
branch_name=info.branch,
|
||||
diff_summary=diff_summary,
|
||||
commit_log=commit_log,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"AI PR body generation encountered an error: {e}")
|
||||
|
||||
if not pr_body:
|
||||
pr_body = self._extract_spec_summary(spec_name)
|
||||
|
||||
# Find gh executable before attempting PR creation
|
||||
gh_executable = get_gh_executable()
|
||||
@@ -1315,6 +1333,167 @@ class WorktreeManager:
|
||||
error="gh CLI not found. Install from https://cli.github.com/",
|
||||
)
|
||||
|
||||
def _gather_pr_context(self, spec_name: str, target_branch: str) -> tuple[str, str]:
|
||||
"""
|
||||
Gather diff summary and commit log for PR template filling.
|
||||
|
||||
Args:
|
||||
spec_name: The spec folder name
|
||||
target_branch: The target branch for the PR
|
||||
|
||||
Returns:
|
||||
Tuple of (diff_summary, commit_log)
|
||||
"""
|
||||
worktree_path = self.get_worktree_path(spec_name)
|
||||
info = self.get_worktree_info(spec_name)
|
||||
branch = info.branch if info else self.get_branch_name(spec_name)
|
||||
|
||||
# Get diff summary (stat for overview)
|
||||
diff_result = self._run_git(
|
||||
["diff", "--stat", f"{target_branch}...{branch}"],
|
||||
cwd=worktree_path,
|
||||
timeout=30,
|
||||
)
|
||||
diff_summary = diff_result.stdout.strip() if diff_result.returncode == 0 else ""
|
||||
|
||||
# Get shortstat for quick summary
|
||||
shortstat_result = self._run_git(
|
||||
["diff", "--shortstat", f"{target_branch}...{branch}"],
|
||||
cwd=worktree_path,
|
||||
timeout=30,
|
||||
)
|
||||
if shortstat_result.returncode == 0 and shortstat_result.stdout.strip():
|
||||
diff_summary += "\n\n" + shortstat_result.stdout.strip()
|
||||
|
||||
# Get actual code changes (patch format) for better AI context
|
||||
# Truncate to 30k chars to avoid token limits while still providing meaningful context
|
||||
patch_result = self._run_git(
|
||||
["diff", "-p", "--stat-width=999", f"{target_branch}...{branch}"],
|
||||
cwd=worktree_path,
|
||||
timeout=30,
|
||||
)
|
||||
if patch_result.returncode == 0 and patch_result.stdout.strip():
|
||||
patch_content = patch_result.stdout.strip()
|
||||
MAX_DIFF_CHARS = 30_000
|
||||
|
||||
if len(patch_content) > MAX_DIFF_CHARS:
|
||||
# Truncate patch and add notice
|
||||
truncated_patch = patch_content[:MAX_DIFF_CHARS]
|
||||
diff_summary += (
|
||||
"\n\n" + truncated_patch + "\n\n(... diff truncated due to size)"
|
||||
)
|
||||
else:
|
||||
diff_summary += "\n\n" + patch_content
|
||||
|
||||
# Get commit log
|
||||
log_result = self._run_git(
|
||||
[
|
||||
"log",
|
||||
"--oneline",
|
||||
"--no-merges",
|
||||
f"{target_branch}..{branch}",
|
||||
],
|
||||
cwd=worktree_path,
|
||||
timeout=30,
|
||||
)
|
||||
commit_log = log_result.stdout.strip() if log_result.returncode == 0 else ""
|
||||
|
||||
return diff_summary, commit_log
|
||||
|
||||
def _try_ai_pr_body(
|
||||
self,
|
||||
spec_name: str,
|
||||
target_branch: str,
|
||||
branch_name: str,
|
||||
diff_summary: str,
|
||||
commit_log: str,
|
||||
) -> str | None:
|
||||
"""
|
||||
Attempt to generate a PR body using the AI template filler agent.
|
||||
|
||||
Runs the async agent synchronously with a 30-second timeout.
|
||||
Returns None on any failure so the caller can fall back gracefully.
|
||||
|
||||
Args:
|
||||
spec_name: The spec folder name
|
||||
target_branch: The target branch for the PR
|
||||
branch_name: The source branch name
|
||||
diff_summary: Git diff summary of changes
|
||||
commit_log: Git log of commits
|
||||
|
||||
Returns:
|
||||
The AI-generated PR body string, or None if unavailable.
|
||||
"""
|
||||
try:
|
||||
from agents.pr_template_filler import (
|
||||
detect_pr_template,
|
||||
run_pr_template_filler,
|
||||
)
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"PR template filler module not available, skipping AI PR body"
|
||||
)
|
||||
return None
|
||||
|
||||
# Check if a PR template exists before doing any heavy lifting
|
||||
template = detect_pr_template(self.project_dir)
|
||||
if template is None:
|
||||
return None
|
||||
|
||||
# Resolve spec directory
|
||||
spec_dir = self.project_dir / ".auto-claude" / "specs" / spec_name
|
||||
if not spec_dir.is_dir():
|
||||
# Try worktree-local spec path
|
||||
worktree_path = self.get_worktree_path(spec_name)
|
||||
spec_dir = worktree_path / ".auto-claude" / "specs" / spec_name
|
||||
if not spec_dir.is_dir():
|
||||
logger.warning("Spec directory not found for AI PR body generation")
|
||||
return None
|
||||
|
||||
# Get model configuration from environment (respects user settings)
|
||||
model, thinking_budget = get_utility_model_config()
|
||||
|
||||
async def _run_with_timeout() -> str | None:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
run_pr_template_filler(
|
||||
project_dir=self.project_dir,
|
||||
spec_dir=spec_dir,
|
||||
model=model,
|
||||
thinking_budget=thinking_budget,
|
||||
branch_name=branch_name,
|
||||
target_branch=target_branch,
|
||||
diff_summary=diff_summary,
|
||||
commit_log=commit_log,
|
||||
verbose=False,
|
||||
),
|
||||
timeout=30.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("PR template filler timed out after 30s")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Check if there's already a running event loop
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
if loop and loop.is_running():
|
||||
# We're already inside an async context — run in a new thread
|
||||
import concurrent.futures
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
future = pool.submit(asyncio.run, _run_with_timeout())
|
||||
return future.result(timeout=35)
|
||||
else:
|
||||
return asyncio.run(_run_with_timeout())
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"AI PR body generation failed: {e}")
|
||||
return None
|
||||
|
||||
def _extract_spec_summary(self, spec_name: str) -> str:
|
||||
"""Extract a summary from spec.md for PR body."""
|
||||
worktree_path = self.get_worktree_path(spec_name)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# PR Template Filler Agent
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an expert developer filling out a GitHub Pull Request template. You receive the repository's PR template along with comprehensive context about the changes — git diff summary, spec overview, commit history, and branch information. Your job is to produce a complete, accurate PR body that matches the template structure exactly, with every section filled intelligently and every relevant checkbox checked.
|
||||
|
||||
## Input Context
|
||||
|
||||
You will receive:
|
||||
|
||||
1. **PR Template** — The repository's `.github/PULL_REQUEST_TEMPLATE.md` content
|
||||
2. **Git Diff Summary** — A summary of all code changes (files changed, insertions, deletions)
|
||||
3. **Spec Overview** — The specification document describing the feature/fix being implemented
|
||||
4. **Commit History** — The list of commits included in this PR
|
||||
5. **Branch Context** — Source branch name, target branch name
|
||||
|
||||
## Methodology
|
||||
|
||||
### Step 1: Understand the Changes
|
||||
|
||||
Before filling anything:
|
||||
|
||||
1. **Read the spec overview** to understand the purpose and scope of the work
|
||||
2. **Analyze the diff summary** to identify what files changed and what kind of changes were made
|
||||
3. **Review the commit history** to understand the progression of work
|
||||
4. **Note the branch names** to infer the PR target and type of change
|
||||
|
||||
### Step 2: Fill Every Section
|
||||
|
||||
For each section in the template:
|
||||
|
||||
1. **Identify the section type** — Is it a description field, a checkbox list, a free-text area, or a conditional section?
|
||||
2. **Select the appropriate content** based on the change context
|
||||
3. **Be specific and accurate** — Reference actual files, components, and behaviors from the diff
|
||||
4. **Never leave a section empty** — If a section is not applicable, explicitly state "N/A" or "Not applicable"
|
||||
|
||||
### Step 3: Check Appropriate Checkboxes
|
||||
|
||||
For checkbox lists (`- [ ]` items):
|
||||
|
||||
1. **Check boxes that apply** by changing `- [ ]` to `- [x]`
|
||||
2. **Leave unchecked** boxes that don't apply
|
||||
3. **Base decisions on evidence** from the diff and spec, not assumptions
|
||||
4. **When uncertain**, leave unchecked rather than incorrectly checking
|
||||
|
||||
### Step 4: Validate Output
|
||||
|
||||
Before returning:
|
||||
|
||||
1. **Verify markdown structure** matches the template exactly (same headings, same order)
|
||||
2. **Ensure no template placeholders remain** (no `<!-- comments -->` left unfilled where content is expected)
|
||||
3. **Check that descriptions are concise** but informative (2-3 sentences for summaries)
|
||||
4. **Confirm all checkboxes reflect reality** based on the provided context
|
||||
|
||||
## Section-Specific Guidelines
|
||||
|
||||
### Description Sections
|
||||
|
||||
- Write 2-3 clear sentences explaining what the PR does and why
|
||||
- Reference the spec or task if available
|
||||
- Focus on the "what" and "why", not implementation details
|
||||
|
||||
### Type of Change
|
||||
|
||||
- Determine from the spec and diff whether this is a bug fix, feature, refactor, docs, or test change
|
||||
- Check exactly one type unless the PR genuinely spans multiple types
|
||||
- Use the spec's `workflow_type` field as a strong signal
|
||||
|
||||
### Area / Service
|
||||
|
||||
- Analyze which directories were modified in the diff
|
||||
- `frontend` = changes in `apps/frontend/`
|
||||
- `backend` = changes in `apps/backend/`
|
||||
- `fullstack` = changes in both
|
||||
|
||||
### Related Issues
|
||||
|
||||
- Extract issue numbers from branch names (e.g., `feature/123-description` → `#123`)
|
||||
- Extract from spec metadata if available
|
||||
- Use `Closes #N` format for issues that will be closed by this PR
|
||||
|
||||
### Checklists
|
||||
|
||||
- **Testing checklists**: Check items that the commit history and diff evidence support
|
||||
- **Platform checklists**: Check platforms that CI covers; note if manual testing is needed
|
||||
- **Code quality checklists**: Check if the diff shows adherence to the principles mentioned
|
||||
|
||||
### AI Disclosure
|
||||
|
||||
- Always check the AI disclosure box — this PR is generated by Auto Claude
|
||||
- Set tool to "Auto Claude (Claude Agent SDK)"
|
||||
- Set testing level based on whether QA was run (check spec context for QA status)
|
||||
- Always check "I understand what this PR does" — the AI agent analyzed the changes
|
||||
|
||||
### Screenshots
|
||||
|
||||
- If the diff includes UI changes (frontend components, styles), note that screenshots should be added
|
||||
- If no UI changes, write "N/A - No UI changes" or remove the section if the template allows
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Analyze the diff for API changes, removed exports, changed interfaces, or modified database schemas
|
||||
- If no breaking changes are evident, mark as "No"
|
||||
- If breaking changes exist, describe what breaks and suggest migration steps
|
||||
|
||||
### Feature Toggle
|
||||
|
||||
- Check the spec for mentions of feature flags, localStorage flags, or environment variables
|
||||
- If the feature is complete and ready, check "N/A - Feature is complete and ready for all users"
|
||||
|
||||
## Output Format
|
||||
|
||||
Return **only** the filled PR template as valid markdown. Do not include any preamble, explanation, or wrapper — just the completed template content ready to be used as a GitHub PR body.
|
||||
|
||||
## Quality Standards
|
||||
|
||||
1. **Accuracy over completeness** — It's better to leave a checkbox unchecked than to incorrectly check it
|
||||
2. **Evidence-based** — Every filled section should be traceable to the provided context
|
||||
3. **Professional tone** — Write as a senior developer would in a real PR
|
||||
4. **Concise but informative** — Don't pad sections with filler text
|
||||
5. **Valid markdown** — The output must render correctly on GitHub
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### DO NOT:
|
||||
|
||||
- **Invent information** not present in the provided context
|
||||
- **Leave template placeholders** like `<!-- What does this PR do? -->` without replacing them with actual content
|
||||
- **Check every checkbox** — only check those supported by evidence
|
||||
- **Write vague descriptions** like "This PR makes some changes" — be specific
|
||||
- **Add sections** not present in the original template
|
||||
- **Remove sections** from the original template — fill or mark as N/A
|
||||
- **Hallucinate file names** or components not mentioned in the diff
|
||||
- **Guess issue numbers** — only reference issues you can confirm from the branch name or spec
|
||||
|
||||
---
|
||||
|
||||
Remember: Your output becomes the PR body on GitHub. It should be professional, accurate, and immediately useful for reviewers. Every section should help a reviewer understand what changed, why it changed, and what to look for during review.
|
||||
@@ -256,6 +256,14 @@ const AGENT_CONFIGS: Record<string, AgentConfig> = {
|
||||
mcp_servers: ['context7'],
|
||||
settingsSource: { type: 'feature', feature: 'roadmap' },
|
||||
},
|
||||
pr_template_filler: {
|
||||
label: 'PR Template Filler',
|
||||
description: 'Generates AI-powered PR descriptions from templates',
|
||||
category: 'utility',
|
||||
tools: ['Read', 'Glob', 'Grep'],
|
||||
mcp_servers: [],
|
||||
settingsSource: { type: 'feature', feature: 'utility' },
|
||||
},
|
||||
};
|
||||
|
||||
// MCP Server descriptions - accurate per backend models.py
|
||||
|
||||
@@ -858,5 +858,11 @@
|
||||
"increase": "Increase {{label}} by {{step}}",
|
||||
"currentValue": "Current value: {{value}}"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"pr_template_filler": {
|
||||
"label": "PR Template Filler",
|
||||
"description": "AI-fills GitHub PR templates from code changes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,5 +858,11 @@
|
||||
"increase": "Augmenter {{label}} de {{step}}",
|
||||
"currentValue": "Valeur actuelle : {{value}}"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"pr_template_filler": {
|
||||
"label": "Remplisseur de modèle PR",
|
||||
"description": "Remplit intelligemment les modèles de PR GitHub à partir des changements de code"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user