diff --git a/.claude/skills/gh-triage/SKILL.md b/.claude/skills/gh-triage/SKILL.md
new file mode 100644
index 00000000..a198fc0d
--- /dev/null
+++ b/.claude/skills/gh-triage/SKILL.md
@@ -0,0 +1,60 @@
+---
+name: gh-triage
+description: Fetch all GitHub issues via gh CLI and provide consolidated AI-powered analysis with clustering, prioritization, and actionable insights. Use for issue triage, backlog cleanup, or when user mentions "issues", "triage", or "backlog".
+license: MIT
+metadata:
+ author: bmad-code-org
+ version: "2.1.0"
+ anthropic-internal: Core team issue triage tool for BMad Method repositories
+ min-github-cli-version: "2.0"
+compatibility: Requires gh CLI, Python 3.8+, and git repository
+---
+
+# GitHub Issue Triage
+
+**IMPORTANT:** Never include time or effort estimates in output or recommendations.
+
+## What This Does
+
+1. **Fetch all issues** from repository via gh CLI (configurable: open/closed/all)
+2. **Extract data** into structured format (JSON + markdown tables)
+3. **Generate AI analysis** with:
+ - Issue clustering by theme
+ - Priority recommendations
+ - Actionable insights
+ - Cross-repo detection
+ - Cleanup candidates
+
+## Steps
+
+```bash
+# 1. Navigate to scripts directory
+cd .claude/skills/gh-triage/scripts
+
+# 2. Run the triage tool (outputs to _bmad-output/triage-reports/)
+python3 gh_triage.py --state open
+
+# 3. Review the generated report
+cat _bmad-output/triage-reports/triage-*.md
+```
+
+## Command Reference
+
+| Parameter | Description | Default |
+| ---------------- | ------------------------------------------ | -------------------------------------------------- |
+| `--repo` | Repository (auto-detected from git remote) | current repo |
+| `--state` | Filter: `all`, `open`, `closed` | `open` |
+| `--focus` | Filter by keywords in title/body | none |
+| `--output`, `-o` | Save output to file | `_bmad-output/triage-reports/triage-YYYY-MM-DD.md` |
+| `--json` | Output as JSON instead of markdown | false (outputs to stdout) |
+| `--limit` | Max issues to fetch | 1000 |
+
+## Output
+
+All reports automatically save to `_bmad-output/triage-reports/` with:
+- Summary statistics
+- Issue clusters by theme
+- Priority matrix
+- Actionable recommendations
+- Cross-repo issues with close commands
+- Cleanup candidates (duplicates, stale, outdated)
diff --git a/.claude/skills/gh-triage/scripts/gh_triage.py b/.claude/skills/gh-triage/scripts/gh_triage.py
new file mode 100755
index 00000000..e73da756
--- /dev/null
+++ b/.claude/skills/gh-triage/scripts/gh_triage.py
@@ -0,0 +1,713 @@
+#!/usr/bin/env python3
+"""
+GitHub Issue Triage Tool
+
+Fetches, categorizes, and groups GitHub issues for efficient triage.
+Optimized for large datasets with parallel processing support.
+
+IMPORTANT: Never provide time, date, or effort estimates in output.
+AI execution speed varies greatly from human timelines.
+Focus on what needs to be done, not how long it takes.
+"""
+
+import argparse
+import json
+import os
+import re
+import subprocess
+from collections import defaultdict
+from dataclasses import dataclass, field
+from datetime import datetime, timedelta, timezone
+from typing import List, Dict, Any, Optional, Set, Tuple
+from enum import Enum
+from difflib import SequenceMatcher
+import re
+
+
+class Category(Enum):
+ """Issue categories"""
+ BUG = "bug"
+ FEATURE = "feature"
+ ENHANCEMENT = "enhancement"
+ DOCUMENTATION = "documentation"
+ PERFORMANCE = "performance"
+ SECURITY = "security"
+ QUESTION = "question"
+ REFACTOR = "refactor"
+ TECH_DEBT = "tech-debt"
+ OTHER = "other"
+
+
+class Priority(Enum):
+ """Priority levels"""
+ CRITICAL = "critical"
+ HIGH = "high"
+ MEDIUM = "medium"
+ LOW = "low"
+ INFORMATIONAL = "informational"
+
+
+class TriagingStatus(Enum):
+ """Triage status"""
+ NEEDS_TRIAGE = "needs-triage"
+ READY_FOR_DEV = "ready-for-dev"
+ BLOCKED = "blocked"
+ STALE = "stale"
+ DUPLICATE = "duplicate"
+ INVALID = "invalid"
+ IN_PROGRESS = "in-progress"
+ COMPLETED = "completed"
+
+
+@dataclass
+class Issue:
+ """Represents a GitHub issue with triage metadata"""
+ number: int
+ title: str
+ state: str
+ author: str
+ created_at: datetime
+ updated_at: Optional[datetime]
+ labels: List[str]
+ body: Optional[str]
+ comments: int
+ category: Category = field(default=Category.OTHER)
+ priority: Priority = field(default=Priority.MEDIUM)
+ triage_status: TriagingStatus = field(default=TriagingStatus.NEEDS_TRIAGE)
+
+ @property
+ def age_days(self) -> int:
+ """Age of issue in days"""
+ return (datetime.now(timezone.utc) - self.created_at).days
+
+ @property
+ def days_since_update(self) -> Optional[int]:
+ """Days since last update"""
+ if not self.updated_at:
+ return None
+ return (datetime.now(timezone.utc) - self.updated_at).days
+
+ @property
+ def url(self) -> str:
+ """GitHub URL for the issue"""
+ # Get from parent triage object
+ return f"https://github.com/{self._repo_url}/issues/{self.number}"
+
+
+class IssueCategorizer:
+ """Categorizes issues based on content and metadata"""
+
+ # Keywords for categorization
+ CATEGORY_KEYWORDS = {
+ Category.BUG: ['bug', 'fix', 'crash', 'error', 'broken', 'fails', 'exception', 'segfault', 'leak'],
+ Category.FEATURE: ['feature', 'add ', 'implement', 'support for', 'new ', 'request', 'wish'],
+ Category.ENHANCEMENT: ['enhance', 'improve', 'optimize', 'better', 'enhancement'],
+ Category.DOCUMENTATION: ['doc', 'readme', 'tutorial', 'guide', 'documentation', 'example', 'comment'],
+ Category.PERFORMANCE: ['slow', 'performance', 'latency', 'speed', 'fast', 'optimize', 'memory'],
+ Category.SECURITY: ['security', 'vulnerability', 'exploit', 'xss', 'injection', 'csrf', 'auth'],
+ Category.QUESTION: ['question', 'how to', 'help', 'confusion', 'unclear', 'clarify'],
+ Category.REFACTOR: ['refactor', 'clean up', 'reorganize', 'restructure', 'simplify'],
+ Category.TECH_DEBT: ['tech debt', 'technical debt', 'legacy', 'deprecated', 'cleanup'],
+ }
+
+ # Priority indicators from labels
+ PRIORITY_LABELS = {
+ Priority.CRITICAL: ['critical', 'blocker', 'urgent'],
+ Priority.HIGH: ['high', 'important', 'priority'],
+ Priority.MEDIUM: ['medium'],
+ Priority.LOW: ['low', 'minor', 'trivial'],
+ }
+
+ def categorize(self, issue: Issue) -> Category:
+ """Determine category based on title, body, and labels"""
+ text = f"{issue.title} {issue.body or ''}".lower()
+
+ # Check labels first
+ for label in issue.labels:
+ label_lower = label.lower()
+ if any(cat_str in label_lower for cat_str in ['bug', 'defect']):
+ return Category.BUG
+ if any(cat_str in label_lower for cat_str in ['feature', 'enhancement']):
+ return Category.FEATURE
+ if 'doc' in label_lower:
+ return Category.DOCUMENTATION
+ if 'perf' in label_lower:
+ return Category.PERFORMANCE
+ if 'security' in label_lower:
+ return Category.SECURITY
+
+ # Check keywords
+ for category, keywords in self.CATEGORY_KEYWORDS.items():
+ if any(keyword in text for keyword in keywords):
+ return category
+
+ return Category.OTHER
+
+ def determine_priority(self, issue: Issue) -> Priority:
+ """Determine priority based on labels and metadata"""
+ # Check labels
+ for label in issue.labels:
+ label_lower = label.lower()
+ for priority, keywords in self.PRIORITY_LABELS.items():
+ if any(keyword in label_lower for keyword in keywords):
+ return priority
+
+ # Infer from metadata
+ if issue.age_days > 90 and issue.state == 'open':
+ # Old open issues are lower priority
+ return Priority.LOW
+
+ if issue.comments > 10:
+ # Highly discussed issues are important
+ return Priority.HIGH
+
+ return Priority.MEDIUM
+
+ def determine_triage_status(self, issue: Issue) -> TriagingStatus:
+ """Determine triage status"""
+ # Check labels
+ for label in issue.labels:
+ label_lower = label.lower()
+ if 'duplicate' in label_lower:
+ return TriagingStatus.DUPLICATE
+ if any(x in label_lower for x in ['invalid', 'wontfix', 'wont-fix']):
+ return TriagingStatus.INVALID
+ if 'blocked' in label_lower or 'blocking' in label_lower:
+ return TriagingStatus.BLOCKED
+ if any(x in label_lower for x in ['in-progress', 'in progress', 'working']):
+ return TriagingStatus.IN_PROGRESS
+
+ # Check staleness
+ if issue.state.upper() == 'OPEN':
+ if issue.days_since_update and issue.days_since_update > 30:
+ return TriagingStatus.STALE
+ if not any(label.lower() in ['accepted', 'approved', 'ready'] for label in issue.labels):
+ return TriagingStatus.NEEDS_TRIAGE
+ return TriagingStatus.READY_FOR_DEV
+
+ return TriagingStatus.COMPLETED
+
+
+class IssueTriage:
+ """Main triage coordinator"""
+
+ # Module repository mapping
+ MODULE_REPOS = {
+ 'builder': {
+ 'repo': 'bmad-code-org/bmad-builder',
+ 'names': ['bmb', 'builder', 'bmad-builder', 'agent builder', 'agent-builder'],
+ 'url': 'https://github.com/bmad-code-org/bmad-builder'
+ },
+ 'tea': {
+ 'repo': 'bmad-code-org/bmad-method-test-architecture-enterprise',
+ 'names': ['tea', 'test architect', 'test-architect', 'test architecture'],
+ 'url': 'https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise'
+ },
+ 'bmgd': {
+ 'repo': 'bmad-code-org/bmad-module-game-dev-studio',
+ 'names': ['bmgd', 'game dev', 'game-dev', 'gamedev', 'game dev studio', 'game-dev-studio'],
+ 'url': 'https://github.com/bmad-code-org/bmad-module-game-dev-studio'
+ },
+ 'cis': {
+ 'repo': 'bmad-code-org/bmad-module-creative-intelligence-suite',
+ 'names': ['cis', 'creative intelligence', 'creative-intelligence', 'creative intelligence suite'],
+ 'url': 'https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite'
+ },
+ }
+
+ def __init__(self, repo: Optional[str] = None, state: str = 'open'):
+ self.repo = repo or self._detect_repo()
+ self.state = state
+ self.categorizer = IssueCategorizer()
+ self.issues: List[Issue] = []
+ self._repo_url = self.repo # Store for issue URL generation
+
+ def _detect_repo(self) -> str:
+ """Detect repository from git remote"""
+ try:
+ result = subprocess.run(
+ ['git', 'remote', 'get-url', 'origin'],
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ url = result.stdout.strip()
+ # Convert git@github.com:user/repo.git to user/repo
+ if url.startswith('git@github.com:'):
+ return url[15:-4]
+ if url.startswith('https://github.com/'):
+ return url[19:-4]
+ except subprocess.CalledProcessError:
+ pass
+ return 'unknown/repo'
+
+ def fetch_issues(self) -> List[Issue]:
+ """Fetch issues using gh CLI"""
+ print(f"Fetching issues from {self.repo}...")
+
+ cmd = [
+ 'gh', 'issue', 'list',
+ '--repo', self.repo,
+ '--state', self.state,
+ '--limit', '1000', # Fetch up to 1000 issues (default is 30)
+ '--json', 'number,title,state,author,createdAt,updatedAt,labels,body,comments'
+ ]
+
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ data = json.loads(result.stdout)
+
+ self.issues = []
+ for item in data:
+ labels = [label['name'] for label in item.get('labels', [])]
+ issue = Issue(
+ number=item['number'],
+ title=item['title'],
+ state=item['state'],
+ author=item['author']['login'],
+ created_at=datetime.fromisoformat(item['createdAt'].replace('Z', '+00:00')),
+ updated_at=datetime.fromisoformat(item['updatedAt'].replace('Z', '+00:00')) if item.get('updatedAt') else None,
+ labels=labels,
+ body=item.get('body'),
+ comments=len(item.get('comments', []))
+ )
+ issue._repo_url = self._repo_url
+ self.issues.append(issue)
+
+ print(f"Fetched {len(self.issues)} issues")
+ return self.issues
+
+ def analyze_issues(self, focus_filter: Optional[str] = None) -> List[Issue]:
+ """Analyze and categorize all issues"""
+ print("Analyzing issues...")
+
+ for issue in self.issues:
+ issue.category = self.categorizer.categorize(issue)
+ issue.priority = self.categorizer.determine_priority(issue)
+ issue.triage_status = self.categorizer.determine_triage_status(issue)
+
+ # Apply focus filter if provided
+ if focus_filter:
+ keywords = focus_filter.lower().split()
+ self.issues = [
+ issue for issue in self.issues
+ if any(keyword in f"{issue.title} {issue.body or ''}".lower()
+ for keyword in keywords)
+ ]
+ print(f"Filtered to {len(self.issues)} issues matching focus criteria")
+
+ return self.issues
+
+ def find_duplicates(self, threshold: float = 0.7) -> List[Tuple[Issue, Issue, float]]:
+ """Find potential duplicate issues based on title similarity"""
+ print("Detecting potential duplicates...")
+ duplicates = []
+ open_issues = [i for i in self.issues if i.state.upper() == 'OPEN']
+
+ for i, issue1 in enumerate(open_issues):
+ for issue2 in open_issues[i+1:]:
+ # Skip if already marked as duplicate
+ if 'duplicate' in [l.lower() for l in issue1.labels + issue2.labels]:
+ continue
+
+ # Calculate title similarity
+ similarity = SequenceMatcher(None, issue1.title.lower(), issue2.title.lower()).ratio()
+
+ if similarity >= threshold:
+ duplicates.append((issue1, issue2, similarity))
+
+ # Sort by similarity (highest first)
+ duplicates.sort(key=lambda x: x[2], reverse=True)
+ return duplicates
+
+ def find_outdated_issues(self, before_date: datetime = None) -> Dict[str, List[Issue]]:
+ """Find issues that are likely outdated"""
+ print("Identifying outdated issues...")
+
+ if before_date is None:
+ # Default to December 1, 2025
+ before_date = datetime(2025, 12, 1, tzinfo=timezone.utc)
+
+ outdated = {
+ 'old_issues': [],
+ 'v4_issues': [],
+ 'ancient_stale': []
+ }
+
+ open_issues = [i for i in self.issues if i.state.upper() == 'OPEN']
+
+ for issue in open_issues:
+ # Issues created before cutoff date
+ if issue.created_at < before_date:
+ outdated['old_issues'].append(issue)
+
+ # Issues mentioning v4
+ text = f"{issue.title} {issue.body or ''}".lower()
+ if 'v4' in text or 'version 4' in text or 'v 4' in text:
+ outdated['v4_issues'].append(issue)
+
+ # Issues very old and stale (>90 days since update)
+ if issue.age_days > 90 and (issue.days_since_update or 0) > 90:
+ outdated['ancient_stale'].append(issue)
+
+ return outdated
+
+ def generate_bulk_commands(self, issues: List[Issue], label: str) -> List[str]:
+ """Generate gh CLI commands for bulk operations"""
+ commands = []
+ for issue in issues:
+ cmd = f"gh issue edit {issue.number} --repo {self.repo} --add-label '{label}'"
+ commands.append(cmd)
+ return commands
+
+ def generate_close_commands(self, issues: List[Issue], reason: str) -> List[str]:
+ """Generate gh CLI commands to close issues with comment"""
+ commands = []
+ for issue in issues:
+ comment = reason.replace("'", "'\\''") # Escape single quotes
+ cmd = (f"gh issue close {issue.number} --repo {self.repo} "
+ f"--comment '{comment}'")
+ commands.append(cmd)
+ return commands
+
+ def find_cross_repo_issues(self) -> Dict[str, List[Tuple[Issue, str]]]:
+ """Find issues that belong in other module repositories"""
+ print("Detecting cross-repo issues...")
+ cross_repo = defaultdict(list)
+
+ open_issues = [i for i in self.issues if i.state.upper() == 'OPEN']
+
+ for issue in open_issues:
+ text = f"{issue.title} {issue.body or ''}".lower()
+
+ for module_key, module_info in self.MODULE_REPOS.items():
+ # Check if issue mentions this module
+ for name in module_info['names']:
+ # Use word boundaries to avoid false positives
+ pattern = r'\b' + re.escape(name) + r'\b'
+ if re.search(pattern, text):
+ cross_repo[module_key].append((issue, name))
+ break # Only add once per issue
+
+ return cross_repo
+
+ def generate_actionable_recommendations(self) -> str:
+ """Generate actionable recommendations with specific commands"""
+ lines = []
+ open_issues = [i for i in self.issues if i.state.upper() == 'OPEN']
+
+ lines.append("## 🎯 Actionable Recommendations\n")
+
+ # Cross-repo issues (show first!)
+ cross_repo_issues = self.find_cross_repo_issues()
+ if cross_repo_issues:
+ total_cross = sum(len(issues) for issues in cross_repo_issues.values())
+ lines.append(f"### Issues in Wrong Repository ({total_cross} issues)\n")
+ lines.append("**High Priority.** These issues should be closed here and opened in the correct module repository:\n")
+
+ for module_key, issues in cross_repo_issues.items():
+ if issues:
+ module_info = self.MODULE_REPOS[module_key]
+ lines.append(f"#### {module_info['repo'].replace('bmad-code-org/', '').title()} ({len(issues)} issues)")
+ lines.append(f"**Correct repo:** [{module_info['repo']}]({module_info['url']}/issues/new)\n")
+ lines.append(f"**Close these and report in the correct repo:**")
+ lines.append(f"```bash")
+
+ for issue, matched_name in issues[:10]: # Show first 10
+ comment = (f"This issue relates to {matched_name} which is maintained in a separate repository. "
+ f"Please report this issue at {module_info['url']}/issues/new")
+ lines.append(f"gh issue close {issue.number} --repo {self.repo} --comment '{comment}'")
+
+ if len(issues) > 10:
+ lines.append(f"# ... and {len(issues) - 10} more")
+ lines.append(f"```\n")
+
+ # Find duplicates
+ duplicates = self.find_duplicates()
+ if duplicates:
+ lines.append(f"### Potential Duplicates ({len(duplicates)} pairs)\n")
+ lines.append("**Manual review required.** Close the older issue as a duplicate of the newer one:\n")
+ for issue1, issue2, similarity in duplicates[:20]: # Top 20
+ older = issue1 if issue1.created_at < issue2.created_at else issue2
+ newer = issue2 if issue1.created_at < issue2.created_at else issue1
+ lines.append(f"#### {older.title}")
+ lines.append(f"- **Older:** #{older.number} ({older.age_days} days old)")
+ lines.append(f"- **Newer:** #{newer.number} ({newer.age_days} days old)")
+ lines.append(f"- **Similarity:** {similarity:.1%}")
+ lines.append(f"- **Command:** `gh issue close {older.number} --repo {self.repo} --comment 'Duplicate of #{newer.number}' --duplicate-of {newer.number}`")
+ lines.append("")
+
+ # Find outdated issues
+ outdated = self.find_outdated_issues()
+ total_outdated = len(outdated['old_issues']) + len(outdated['v4_issues']) + len(outdated['ancient_stale'])
+
+ if total_outdated > 0:
+ lines.append(f"### Outdated Issues ({total_outdated} total)\n")
+
+ # Pre-Dec 2025 issues
+ if outdated['old_issues']:
+ cutoff_date = datetime(2025, 12, 1, tzinfo=timezone.utc).strftime('%B %Y')
+ lines.append(f"#### Issues from before {cutoff_date} ({len(outdated['old_issues'])})")
+ lines.append(f"These issues are quite old and may no longer be relevant. Consider reviewing and closing outdated ones.\n")
+ lines.append("**To add label for review:**")
+ lines.append(f"```bash")
+ for issue in outdated['old_issues'][:10]: # Show first 10
+ lines.append(f"gh issue edit {issue.number} --repo {self.repo} --add-label 'outdated,needs-review'")
+ if len(outdated['old_issues']) > 10:
+ lines.append(f"# ... and {len(outdated['old_issues']) - 10} more")
+ lines.append(f"```\n")
+
+ # v4-related issues
+ if outdated['v4_issues']:
+ lines.append(f"#### v4-Related Issues ({len(outdated['v4_issues'])})")
+ lines.append(f"BMad Method v4 is deprecated. These issues likely no longer apply to v6.\n")
+ lines.append("**Bulk close with comment:**")
+ lines.append(f"```bash")
+ for issue in outdated['v4_issues'][:10]:
+ lines.append(f"gh issue close {issue.number} --repo {self.repo} --comment 'Closing as this relates to BMad Method v4 which is deprecated. Please open a new issue if this still applies to v6.'")
+ if len(outdated['v4_issues']) > 10:
+ lines.append(f"# ... and {len(outdated['v4_issues']) - 10} more")
+ lines.append(f"```\n")
+
+ # Ancient stale issues
+ if outdated['ancient_stale']:
+ lines.append(f"#### Ancient Stale Issues ({len(outdated['ancient_stale'])})")
+ lines.append(f"Issues that are both very old (>90 days) and haven't been updated in >90 days.\n")
+ lines.append("**Close as stale:**")
+ lines.append(f"```bash")
+ for issue in outdated['ancient_stale'][:10]:
+ lines.append(f"gh issue close {issue.number} --repo {self.repo} --comment 'Closing due to inactivity. Please reopen if this is still relevant.'")
+ if len(outdated['ancient_stale']) > 10:
+ lines.append(f"# ... and {len(outdated['ancient_stale']) - 10} more")
+ lines.append(f"```\n")
+
+ # Bulk tagging suggestions
+ lines.append("### Bulk Tagging Suggestions\n")
+ lines.append("Add appropriate labels to untagged issues:\n")
+
+ # Find issues without category labels
+ untagged = [i for i in open_issues if not any(
+ l.lower() in ['bug', 'feature', 'enhancement', 'documentation', 'performance', 'question', 'refactor', 'tech-debt']
+ for l in i.labels
+ )]
+
+ if untagged:
+ lines.append(f"**Issues without category labels ({len(untagged)}):**\n")
+ by_category = defaultdict(list)
+ for issue in untagged:
+ by_category[issue.category.value].append(issue)
+
+ for category, issues in sorted(by_category.items(), key=lambda x: len(x[1]), reverse=True)[:5]:
+ lines.append(f"##### Label as `{category}` ({len(issues)} issues)")
+ lines.append(f"```bash")
+ for issue in issues[:5]:
+ lines.append(f"gh issue edit {issue.number} --repo {self.repo} --add-label '{category}'")
+ if len(issues) > 5:
+ lines.append(f"# ... and {len(issues) - 5} more")
+ lines.append(f"```\n")
+
+ # Priority labeling
+ no_priority = [i for i in open_issues if not any(
+ l.lower() in ['critical', 'high', 'medium', 'low', 'priority']
+ for l in i.labels
+ )]
+
+ if no_priority:
+ # Group by priority
+ by_priority = defaultdict(list)
+ for issue in no_priority:
+ by_priority[issue.priority.value].append(issue)
+
+ lines.append("**Add priority labels:**\n")
+ for priority_level in ['critical', 'high', 'medium', 'low']:
+ if priority_level in by_priority:
+ count = len(by_priority[priority_level])
+ lines.append(f"##### Label as `{priority_level}` priority ({count} issues)")
+ lines.append(f"```bash")
+ for issue in by_priority[priority_level][:5]:
+ lines.append(f"gh issue edit {issue.number} --repo {self.repo} --add-label '{priority_level}'")
+ if count > 5:
+ lines.append(f"# ... and {count - 5} more")
+ lines.append(f"```\n")
+
+ return "\n".join(lines)
+
+ def generate_report(self) -> str:
+ """Generate markdown triage report"""
+ lines = []
+
+ # Summary
+ lines.append("# GitHub Issue Triage Report\n")
+ lines.append(f"**Repository:** {self.repo}\n")
+ lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}\n")
+
+ # Count stats
+ total = len(self.issues)
+ open_issues = [i for i in self.issues if i.state.upper() == 'OPEN']
+ closed_issues = [i for i in self.issues if i.state.upper() == 'CLOSED']
+
+ lines.append("## Summary\n")
+ lines.append(f"- **Total Issues:** {total}")
+ lines.append(f"- **Open:** {len(open_issues)} | **Closed:** {len(closed_issues)}")
+
+ # Category breakdown
+ category_counts = defaultdict(int)
+ for issue in self.issues:
+ category_counts[issue.category] += 1
+ top_categories = sorted(category_counts.items(), key=lambda x: x[1], reverse=True)[:5]
+ lines.append(f"- **Top Categories:** {', '.join(f'{cat.value} ({count})' for cat, count in top_categories)}")
+ lines.append("")
+
+ # Priority action items
+ lines.append("## Priority Action Items\n")
+
+ # Critical/High priority open issues
+ critical_issues = [i for i in open_issues if i.priority in [Priority.CRITICAL, Priority.HIGH]]
+ if critical_issues:
+ lines.append(f"### {'🚨 ' if critical_issues else ''}Critical & High Priority ({len(critical_issues)})\n")
+ for issue in sorted(critical_issues, key=lambda x: x.age_days, reverse=True):
+ lines.append(f"- [#{issue.number}]({issue.url}) {issue.title}")
+ lines.append(f" - {issue.category.value.upper()} | {issue.priority.value.upper()} | Age: {issue.age_days} days")
+ if issue.labels:
+ lines.append(f" - Labels: {', '.join(issue.labels)}")
+ lines.append("")
+
+ # Stale issues needing review
+ stale_issues = [i for i in open_issues if i.triage_status == TriagingStatus.STALE]
+ if stale_issues:
+ lines.append(f"### Stale Issues - Needs Review ({len(stale_issues)})\n")
+ for issue in sorted(stale_issues, key=lambda x: x.days_since_update or 0, reverse=True)[:15]:
+ days_stale = issue.days_since_update or 0
+ lines.append(f"- [#{issue.number}]({issue.url}) {issue.title}")
+ lines.append(f" - Last updated {days_stale} days ago | {issue.category.value}")
+ lines.append("")
+
+ # Categories
+ lines.append("## Categories\n")
+
+ for category in Category:
+ category_issues = [i for i in self.issues if i.category == category]
+ if not category_issues:
+ continue
+
+ open_in_cat = [i for i in category_issues if i.state.upper() == 'OPEN']
+ closed_in_cat = [i for i in category_issues if i.state.upper() == 'CLOSED']
+
+ lines.append(f"### {category.value.title()} ({len(open_in_cat)} open, {len(closed_in_cat)} closed)\n")
+
+ # Sort open by priority
+ priority_order = {Priority.CRITICAL: 0, Priority.HIGH: 1, Priority.MEDIUM: 2, Priority.LOW: 3, Priority.INFORMATIONAL: 4}
+ open_in_cat_sorted = sorted(open_in_cat, key=lambda x: priority_order.get(x.priority, 5))
+
+ for issue in open_in_cat_sorted[:20]: # Limit to 20 per category
+ status_icon = {
+ TriagingStatus.NEEDS_TRIAGE: '🔍',
+ TriagingStatus.READY_FOR_DEV: '✅',
+ TriagingStatus.BLOCKED: '🚫',
+ TriagingStatus.STALE: '💤',
+ TriagingStatus.IN_PROGRESS: '🔧',
+ }.get(issue.triage_status, '')
+
+ lines.append(f"{status_icon} [#{issue.number}]({issue.url}) {issue.title}")
+ lines.append(f" Details
")
+ lines.append(f" ")
+ lines.append(f" - **Priority:** {issue.priority.value}")
+ lines.append(f" - **Status:** {issue.triage_status.value}")
+ lines.append(f" - **Age:** {issue.age_days} days")
+ lines.append(f" - **Author:** {issue.author}")
+ if issue.labels:
+ lines.append(f" - **Labels:** {', '.join(issue.labels)}")
+ lines.append(f" ")
+ lines.append("")
+
+ if len(open_in_cat) > 20:
+ lines.append(f"*... and {len(open_in_cat) - 20} more*\n")
+
+ # Actionable recommendations
+ lines.append(self.generate_actionable_recommendations())
+
+ # Cleanup candidates
+ lines.append("## Cleanup Candidates\n")
+
+ duplicates = [i for i in self.issues if i.triage_status == TriagingStatus.DUPLICATE]
+ if duplicates:
+ lines.append(f"### Duplicates ({len(duplicates)})\n")
+ for issue in duplicates:
+ lines.append(f"- [#{issue.number}]({issue.url}) {issue.title}")
+
+ invalid = [i for i in self.issues if i.triage_status == TriagingStatus.INVALID]
+ if invalid:
+ lines.append(f"\n### Invalid/Wontfix ({len(invalid)})\n")
+ for issue in invalid:
+ lines.append(f"- [#{issue.number}]({issue.url}) {issue.title}")
+
+ lines.append("\n---\n")
+ lines.append("*Report generated by BMad Issue Triage Tool*")
+
+ return "\n".join(lines)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Triage and categorize GitHub issues',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ %(prog)s # Triage open issues in current repo
+ %(prog)s --state all # All issues (including closed)
+ %(prog)s --focus "installer" # Filter for installer-related issues
+ %(prog)s --repo user/repo --state closed
+ """
+ )
+
+ parser.add_argument('--repo', help='Repository (default: detect from git)')
+ parser.add_argument('--state', choices=['all', 'open', 'closed'], default='open',
+ help='Filter by state (default: open)')
+ parser.add_argument('--focus', help='Focus context to filter issues')
+ parser.add_argument('--output', '-o', help='Output file (default: _bmad-output/triage-reports/triage-.md)')
+ parser.add_argument('--json', action='store_true', help='Output as JSON')
+
+ args = parser.parse_args()
+
+ # Set default output to _bmad-output/triage-reports if not specified
+ if not args.output and not args.json:
+ os.makedirs('_bmad-output/triage-reports', exist_ok=True)
+ output_date = datetime.now().strftime('%Y-%m-%d')
+ args.output = f'_bmad-output/triage-reports/triage-{output_date}.md'
+
+ triage = IssueTriage(repo=args.repo, state=args.state)
+ triage.fetch_issues()
+ triage.analyze_issues(focus_filter=args.focus)
+
+ if args.json:
+ # Output as JSON for further processing
+ data = [
+ {
+ 'number': i.number,
+ 'title': i.title,
+ 'state': i.state,
+ 'category': i.category.value,
+ 'priority': i.priority.value,
+ 'triage_status': i.triage_status.value,
+ 'age_days': i.age_days,
+ 'url': i.url,
+ 'labels': i.labels,
+ 'author': i.author
+ }
+ for i in triage.issues
+ ]
+ output = json.dumps(data, indent=2)
+ else:
+ output = triage.generate_report()
+
+ if args.output:
+ with open(args.output, 'w') as f:
+ f.write(output)
+ # Get relative path for nicer output
+ rel_path = args.output.replace('./', '')
+ print(f"✅ Report saved to: {rel_path}")
+ else:
+ print(output)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml
new file mode 100644
index 00000000..6c5507d9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yaml
@@ -0,0 +1,124 @@
+name: Bug Report
+description: File a bug report to help us improve BMad Method
+title: "[BUG] "
+labels: bug
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for filing a bug report! Please fill out the information below to help us reproduce and fix the issue.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Clear and concise description of what the bug is
+ placeholder: e.g., When I run /dev-story, it crashes on step 3
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: Step-by-step instructions to reproduce the behavior
+ placeholder: |
+ 1. Run 'npx bmad-method install'
+ 2. Select option X
+ 3. Run workflow Y
+ 4. See error
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: What you expected to happen
+ placeholder: The workflow should complete successfully
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual behavior
+ description: What actually happened
+ placeholder: The workflow crashed with error "..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: Add screenshots if applicable (paste images directly)
+ placeholder: Paste any relevant screenshots here
+
+ - type: dropdown
+ id: module
+ attributes:
+ label: Which module is this for?
+ description: Select the BMad module this issue relates to
+ options:
+ - BMad Method (BMM) - Core Framework
+ - BMad Builder (BMB) - Agent Builder Tool
+ - Test Architect (TEA) - Test Strategy Module
+ - Game Dev Studio (BMGD) - Game Development Module
+ - Creative Intelligence Suite (CIS) - Innovation Module
+ - Not sure / Other
+ validations:
+ required: true
+
+ - type: input
+ id: version
+ attributes:
+ label: BMad Version
+ description: "Check with: npx bmad-method --version or check package.json"
+ placeholder: e.g., 6.0.0-Beta.4
+ validations:
+ required: true
+
+ - type: dropdown
+ id: ide
+ attributes:
+ label: Which AI IDE are you using?
+ options:
+ - Claude Code
+ - Cursor
+ - Windsurf
+ - Copilot CLI / GitHub Copilot
+ - Kilo Code
+ - Other
+ validations:
+ required: true
+
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Operating System
+ options:
+ - macOS
+ - Windows
+ - Linux
+ - Other
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Relevant log output
+ description: Copy and paste any relevant log output
+ render: shell
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Confirm
+ options:
+ - label: I've searched for existing issues
+ required: true
+ - label: I'm using the latest version
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml
new file mode 100644
index 00000000..00729a36
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.yaml
@@ -0,0 +1,55 @@
+name: Documentation
+description: Report issues or suggest improvements to documentation
+title: "[DOCS] "
+labels: documentation
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Help us improve the BMad Method documentation!
+
+ - type: dropdown
+ id: doc-type
+ attributes:
+ label: What type of documentation issue is this?
+ options:
+ - Error or inaccuracy
+ - Missing information
+ - Unclear or confusing
+ - Outdated content
+ - Request for new documentation
+ - Typo or grammar
+ validations:
+ required: true
+
+ - type: textarea
+ id: location
+ attributes:
+ label: Documentation location
+ description: Where is the documentation that needs improvement?
+ placeholder: e.g., http://docs.bmad-method.org/tutorials/getting-started/ or "In the README"
+ validations:
+ required: true
+
+ - type: textarea
+ id: issue
+ attributes:
+ label: What's the issue?
+ description: Describe the documentation issue in detail
+ placeholder: e.g., Step 3 says to run command X but it should be command Y
+ validations:
+ required: true
+
+ - type: textarea
+ id: suggestion
+ attributes:
+ label: Suggested improvement
+ description: How would you like to see this improved?
+ placeholder: e.g., Change the command to X and add an example
+
+ - type: input
+ id: version
+ attributes:
+ label: BMad Version (if applicable)
+ placeholder: e.g., 6.0.0-Beta.4
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature-request.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/feature_request.md
rename to .github/ISSUE_TEMPLATE/feature-request.md
diff --git a/.gitignore b/.gitignore
index 885cb245..6af83303 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,7 +50,7 @@ _bmad-output
.qwen
.rovodev
.kilocodemodes
-.claude
+.claude/commands
.codex
.github/chatmodes
.github/agents