""" title: Gitea Coder - Development Workflow Role author: Jeff Smith + Claude + minimax version: 1.0.0 license: MIT description: gitea_coder role for automated development workflows - reads tickets, creates branches, commits with detailed messages, and creates PRs requirements: pydantic, httpx changelog: 1.0.0: - Initial implementation of gitea_coder role - Branch creation with scope gating - Commit message generation with ticket references - PR creation from feature branches - Ticket requirement reading - Scope enforcement (branch naming conventions) """ from typing import Optional, Callable, Any, Dict, List from pydantic import BaseModel, Field import re import time class Tools: """Gitea Coder - Development workflow automation role""" class Valves(BaseModel): """System-wide configuration for Gitea Coder integration""" GITEA_URL: str = Field( default="https://gitea.example.com", description="Gitea server URL (ingress or internal service)", ) DEFAULT_REPO: str = Field( default="", description="Default repository in owner/repo format", ) DEFAULT_BRANCH: str = Field( default="main", description="Default branch name for operations", ) ALLOW_USER_OVERRIDES: bool = Field( default=True, description="Allow users to override defaults via UserValves", ) VERIFY_SSL: bool = Field( default=True, description="Verify SSL certificates", ) # Scope enforcement configuration ALLOWED_SCOPES: List[str] = Field( default=["feature", "fix", "refactor", "docs", "test", "chore"], description="Allowed branch scope prefixes", ) PROTECTED_BRANCHES: List[str] = Field( default=["main", "master", "develop", "dev"], description="Branches that cannot be directly modified", ) class UserValves(BaseModel): """Per-user configuration""" GITEA_TOKEN: str = Field( default="", description="Your Gitea API token", ) USER_DEFAULT_REPO: str = Field( default="", description="Override default repository", ) USER_DEFAULT_BRANCH: str = Field( default="", description="Override default branch", ) def __init__(self): """Initialize with configuration""" self.valves = self.Valves() self.user_valves = self.UserValves() # Cache for chat session data (branch, repo, etc.) self._chat_cache: Dict[str, dict] = {} self._cache_ttl = 3600 # 1 hour TTL for cache entries # Reference to dev.py operations (will be set by framework) self._dev = None def _get_token(self, __user__: dict = None) -> str: """Extract Gitea token from user context""" if __user__ and "valves" in __user__: user_valves = __user__.get("valves") if user_valves: return user_valves.GITEA_TOKEN return "" def _get_repo(self, repo: Optional[str], __user__: dict = None) -> str: """Get effective repository""" if repo: return repo if __user__ and "valves" in __user__: user_valves = __user__.get("valves") if user_valves: if self.valves.ALLOW_USER_OVERRIDES and user_valves.USER_DEFAULT_REPO: return user_valves.USER_DEFAULT_REPO return self.valves.DEFAULT_REPO def _get_branch(self, branch: Optional[str], __user__: dict = None) -> str: """Get effective branch""" if branch: return branch if __user__ and "valves" in __user__: user_valves = __user__.get("valves") if user_valves: if self.valves.ALLOW_USER_OVERRIDES and user_valves.USER_DEFAULT_BRANCH: return user_valves.USER_DEFAULT_BRANCH return self.valves.DEFAULT_BRANCH def _get_chat_cache_key(self, chat_id: str, repo: str) -> str: """Generate cache key for chat session""" return f"{chat_id}:{repo}" def _get_cached_data(self, chat_id: str, repo: str) -> Optional[dict]: """Get cached data for chat session""" cache_key = self._get_chat_cache_key(chat_id, repo) if cache_key in self._chat_cache: data = self._chat_cache[cache_key] if time.time() - data.get("timestamp", 0) < self._cache_ttl: return data.get("value") else: # Expired, remove from cache del self._chat_cache[cache_key] return None def _set_cached_data(self, chat_id: str, repo: str, key: str, value: Any): """Set cached data for chat session""" cache_key = self._get_chat_cache_key(chat_id, repo) if cache_key not in self._chat_cache: self._chat_cache[cache_key] = {"timestamp": time.time(), "value": {}} self._chat_cache[cache_key]["value"][key] = value self._chat_cache[cache_key]["timestamp"] = time.time() def _validate_branch_name(self, branch_name: str) -> tuple[bool, str]: """ Validate branch name against scope conventions. Returns: tuple: (is_valid, error_message) """ # Check for protected branches if branch_name in self.valves.PROTECTED_BRANCHES: return False, f"Branch '{branch_name}' is protected. Create a feature branch instead." # Check if it starts with a valid scope scope_pattern = f"^({'|'.join(self.valves.ALLOWED_SCOPES)})/" if not re.match(scope_pattern, branch_name): scopes = ", ".join([f"{s}/" for s in self.valves.ALLOWED_SCOPES]) return False, f"Branch name must start with a valid scope. Allowed: {scopes}" # Validate remaining branch name if not re.match(r"^[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$", branch_name): return False, "Branch name contains invalid characters. Use alphanumeric, hyphens, underscores, and forward slashes only." return True, "" def _generate_commit_message( self, scope: str, short_description: str, issue_id: Optional[str] = None, body: str = "", footer: str = "", ) -> str: """ Generate a detailed commit message following conventional commits format. Args: scope: The scope of the change (feature, fix, etc.) short_description: Brief description of the change issue_id: Issue/ticket number to reference body: Detailed explanation of the change footer: Breaking changes or issue references Returns: str: Formatted commit message """ # Build the header message = f"{scope}({scope}): {short_description}" # Add issue reference if issue_id: message += f"\n\nRefs: #{issue_id}" # Add body if provided if body: message += f"\n\n{body}" # Add footer if provided if footer: message += f"\n\n{footer}" return message def _extract_issue_info(self, issue_text: str) -> dict: """ Extract key information from issue text. Args: issue_text: Raw issue text from Gitea Returns: dict: Extracted information (title, body, labels, etc.) """ info = { "title": "", "body": "", "type": "feature", "has_testing": False, "has_documentation": False, } lines = issue_text.split("\n") current_section = "" for line in lines: line_lower = line.lower().strip() # Detect title if line.startswith("# ") and not info["title"]: info["title"] = line[2:].strip() # Detect sections elif line.startswith("## "): current_section = line[3:].lower() # Extract labels/tags from body elif current_section == "description" and line.startswith("- [ ]"): task = line[4:].strip() if "test" in task.lower(): info["has_testing"] = True if "doc" in task.lower(): info["has_documentation"] = True # Detect issue type from labels elif line_lower.startswith("**labels:**"): labels_str = line.split(":", 1)[1].strip().lower() if "bug" in labels_str or "fix" in labels_str: info["type"] = "fix" elif "docs" in labels_str or "documentation" in labels_str: info["type"] = "docs" elif "test" in labels_str: info["type"] = "test" # Extract body content body_match = re.search(r"## Description\s*\n(.*?)(?=\n## |\n# |\Z)", issue_text, re.DOTALL) if body_match: info["body"] = body_match.group(1).strip() return info async def create_feature_branch( self, branch_name: str, issue_number: Optional[int] = None, from_branch: Optional[str] = None, repo: Optional[str] = None, chat_id: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Create a new feature branch with scope validation. This function enforces branch naming conventions and prevents direct modifications to protected branches like main. Args: branch_name: Name for the new branch (e.g., 'feature/42-add-login') issue_number: Optional issue number to include in branch name from_branch: Source branch (defaults to repository default) repo: Repository in 'owner/repo' format chat_id: Chat session ID for caching default branch __user__: User context __event_emitter__: Event emitter callback Returns: str: Confirmation with branch details or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified. Set DEFAULT_REPO in Valves or USER_DEFAULT_REPO in UserValves." effective_from = from_branch or self._get_branch(None, __user__) # Auto-append issue number if provided if issue_number and str(issue_number) not in branch_name: branch_name = f"{branch_name}-{issue_number}" # Validate branch name is_valid, error_msg = self._validate_branch_name(branch_name) if not is_valid: return f"Error: {error_msg}" # Cache the default branch for this chat session if chat_id: self._set_cached_data(chat_id, effective_repo, "default_branch", effective_from) self._set_cached_data(chat_id, effective_repo, "current_branch", branch_name) if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": f"Creating branch {branch_name}...", "done": False}, } ) try: # Use gitea/dev.py create_branch operation if self._dev: result = await self._dev.create_branch( branch_name=branch_name, from_branch=effective_from, repo=effective_repo, __user__=__user__, __event_emitter__=__event_emitter__, ) return result else: # Fallback: direct API call if dev not available async with httpx.AsyncClient( timeout=30.0, verify=self.valves.VERIFY_SSL ) as client: response = await client.post( f"{self.valves.GITEA_URL.rstrip('/')}/api/v1/repos/{effective_repo}/branches", headers={"Authorization": f"token {token}"}, json={ "new_branch_name": branch_name, "old_branch_name": effective_from, }, ) response.raise_for_status() if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": "Done", "done": True, "hidden": True}, } ) return f"✅ Created branch `{branch_name}` from `{effective_from}` in `{effective_repo}`" except Exception as e: return f"Error: Failed to create branch. {type(e).__name__}: {e}" async def read_ticket( self, issue_number: int, repo: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Read and parse a ticket/issue to extract requirements. This function reads an issue and extracts key information needed for implementation, including title, description, acceptance criteria, and testing requirements. Args: issue_number: Issue number to read repo: Repository in 'owner/repo' format __user__: User context __event_emitter__: Event emitter callback Returns: str: Structured ticket requirements or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified." if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": f"Reading ticket #{issue_number}...", "done": False}, } ) try: # Use gitea/dev.py get_issue operation if self._dev: raw_issue = await self._dev.get_issue( issue_number=issue_number, repo=effective_repo, __user__=__user__, ) else: # Fallback: direct API call async with httpx.AsyncClient( timeout=30.0, verify=self.valves.VERIFY_SSL ) as client: response = await client.get( f"{self.valves.GITEA_URL.rstrip('/')}/api/v1/repos/{effective_repo}/issues/{issue_number}", headers={"Authorization": f"token {token}"}, ) response.raise_for_status() raw_issue = response.json() # Format raw response title = raw_issue.get("title", "No title") body = raw_issue.get("body", "") state = raw_issue.get("state", "unknown") user = raw_issue.get("user", {}).get("login", "unknown") created_at = raw_issue.get("created_at", "")[:10] labels = [label.get("name", "") for label in raw_issue.get("labels", [])] raw_issue = f"# Issue #{issue_number}: {title}\n\n**State:** {state.upper()}\n**Author:** @{user}\n**Labels:** {', '.join(labels) if labels else 'None'}\n**Created:** {created_at}\n\n## Description\n{body}" # Parse the issue info = self._extract_issue_info(raw_issue) # Build structured output output = f"# Ticket Requirements: #{issue_number}\n\n" output += f"**Title:** {info['title'] or 'Untitled'}\n" output += f"**Type:** {info['type'].upper()}\n" output += f"**Testing Required:** {'✅' if info['has_testing'] else '❌'}\n" output += f"**Documentation Required:** {'✅' if info['has_documentation'] else '❌'}\n\n" if info['body']: output += "## Requirements\n\n" # Extract bullet points bullets = [line[2:].strip() for line in info['body'].split('\n') if line.strip().startswith('- ')] if bullets: for i, bullet in enumerate(bullets, 1): output += f"{i}. {bullet}\n" else: output += f"{info['body'][:500]}\n" output += "\n" # Extract testing criteria if "Testing" in raw_issue or "test" in raw_issue.lower(): test_match = re.search(r"(?:Testing Criteria|Tests?).*?\n(.*?)(?=\n## |\n# |\Z)", raw_issue, re.DOTALL | re.IGNORECASE) if test_match: output += "## Testing Criteria\n\n" output += test_match.group(1).strip() + "\n\n" # Extract technical notes if "Technical" in raw_issue: tech_match = re.search(r"(?:Technical Notes).*?\n(.*?)(?=\n## |\n# |\Z)", raw_issue, re.DOTALL | re.IGNORECASE) if tech_match: output += "## Technical Notes\n\n" output += tech_match.group(1).strip() + "\n\n" if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": "Done", "done": True, "hidden": True}, } ) return output.strip() except Exception as e: return f"Error: Failed to read ticket. {type(e).__name__}: {e}" async def commit_changes( self, path: str, content: str, short_description: str, issue_number: Optional[int] = None, body: str = "", branch: Optional[str] = None, repo: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Commit file changes with auto-generated detailed commit messages. This function creates or updates a file and generates a detailed commit message that references the issue number. Args: path: File path to create or update content: New file content short_description: Brief description of changes issue_number: Issue number for commit message reference body: Additional commit message details branch: Branch name (defaults to current feature branch) repo: Repository in 'owner/repo' format __user__: User context __event_emitter__: Event emitter callback Returns: str: Commit confirmation or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified." effective_branch = self._get_branch(branch, __user__) # Validate we're not on a protected branch is_valid, error_msg = self._validate_branch_name(effective_branch) if not is_valid: # Check if it's a protected branch if effective_branch in self.valves.PROTECTED_BRANCHES: return f"Error: Cannot commit directly to protected branch '{effective_branch}'. Create a feature branch first." # If it's just not following conventions, warn but proceed pass # Generate commit message scope = effective_branch.split("/")[0] if "/" in effective_branch else "chore" commit_message = self._generate_commit_message( scope=scope, short_description=short_description, issue_id=str(issue_number) if issue_number else None, body=body, ) if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": f"Committing changes to {path}...", "done": False}, } ) try: # Use gitea/dev.py operations if self._dev: # Check if file exists to determine create vs replace file_info = await self._dev.get_file( path=path, repo=effective_repo, branch=effective_branch, __user__=__user__, ) if "Error: File not found" in file_info: # Create new file result = await self._dev.create_file( path=path, content=content, message=commit_message, repo=effective_repo, branch=effective_branch, __user__=__user__, __event_emitter__=__event_emitter__, ) else: # Replace existing file result = await self._dev.replace_file( path=path, content=content, message=commit_message, repo=effective_repo, branch=effective_branch, __user__=__user__, __event_emitter__=__event_emitter__, ) # Add issue reference to the output if issue_number: result += f"\n\n**Referenced Issue:** #{issue_number}" return result else: return "Error: gitea/dev.py operations not available. Cannot commit changes." except Exception as e: return f"Error: Failed to commit changes. {type(e).__name__}: {e}" async def create_pull_request( self, title: str, head_branch: str, body: str = "", base_branch: Optional[str] = None, repo: Optional[str] = None, issue_number: Optional[int] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Create a pull request from a feature branch. This function creates a PR and optionally references an issue in the PR description. Args: title: PR title head_branch: Source branch with changes body: PR description base_branch: Target branch (defaults to main) repo: Repository in 'owner/repo' format issue_number: Issue number to reference in description __user__: User context __event_emitter__: Event emitter callback Returns: str: PR creation confirmation or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified." effective_base = base_branch or "main" # Validate head branch is not protected is_valid, error_msg = self._validate_branch_name(head_branch) if not is_valid: if head_branch in self.valves.PROTECTED_BRANCHES: return f"Error: Cannot create PR from protected branch '{head_branch}'." # For non-conforming names, warn but proceed pass # Build PR body with issue reference pr_body = body if issue_number: pr_body = f"**References Issue:** #{issue_number}\n\n{body}" if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": f"Creating PR from {head_branch}...", "done": False}, } ) try: # Use gitea/dev.py create_pull_request if self._dev: result = await self._dev.create_pull_request( title=title, head_branch=head_branch, base_branch=effective_base, body=pr_body, repo=effective_repo, __user__=__user__, __event_emitter__=__event_emitter__, ) return result else: return "Error: gitea/dev.py operations not available. Cannot create PR." except Exception as e: return f"Error: Failed to create PR. {type(e).__name__}: {e}" async def list_my_branches( self, repo: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ List branches in the repository with scope categorization. This function lists all branches and groups them by scope for easier navigation. Args: repo: Repository in 'owner/repo' format __user__: User context __event_emitter__: Event emitter callback Returns: str: Formatted branch listing or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified." if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": "Listing branches...", "done": False}, } ) try: # Use gitea/dev.py list_branches if self._dev: result = await self._dev.list_branches( repo=effective_repo, __user__=__user__, __event_emitter__=__event_emitter__, ) # Add scope categorization if we got a result if "Error:" not in result: # Categorize branches by scope scope_branches: Dict[str, List[str]] = {scope: [] for scope in self.valves.ALLOWED_SCOPES} scope_branches["other"] = [] # Parse the result to extract branch names branch_pattern = r"- `([^`]+)`" branches = re.findall(branch_pattern, result) for branch in branches: added = False for scope in self.valves.ALLOWED_SCOPES: if branch.startswith(f"{scope}/"): scope_branches[scope].append(branch) added = True break if not added and branch not in self.valves.PROTECTED_BRANCHES: scope_branches["other"].append(branch) # Add categorization to output categorized = "\n## Branch Scopes\n\n" for scope, branches_list in scope_branches.items(): if branches_list: categorized += f"**{scope.upper()}**\n" for branch in branches_list: categorized += f"- `{branch}`\n" categorized += "\n" result += categorized return result else: return "Error: gitea/dev.py operations not available." except Exception as e: return f"Error: Failed to list branches. {type(e).__name__}: {e}" async def get_branch_status( self, branch: Optional[str] = None, repo: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Get the current branch status and recent activity. This function shows the current branch and recent commits to help the coder understand their current context. Args: branch: Branch name (defaults to current) repo: Repository in 'owner/repo' format __user__: User context __event_emitter__: Event emitter callback Returns: str: Branch status or error message """ token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." effective_repo = self._get_repo(repo, __user__) if not effective_repo: return "Error: No repository specified." effective_branch = self._get_branch(branch, __user__) if __event_emitter__: await __event_emitter__( { "type": "status", "data": {"description": f"Getting status for {effective_branch}...", "done": False}, } ) try: # Get recent commits on this branch if self._dev: commits = await self._dev.list_commits( repo=effective_repo, branch=effective_branch, limit=5, __user__=__user__, ) # Get branch info branches_list = await self._dev.list_branches( repo=effective_repo, __user__=__user__, ) # Check if branch exists branch_exists = f"`{effective_branch}`" in branches_list output = f"# Branch Status: `{effective_branch}`\n\n" output += f"**Status:** {'✅ Exists' if branch_exists else '❌ Not Found'}\n" output += f"**Repository:** {effective_repo}\n\n" if branch_exists: output += "## Recent Commits\n\n" output += commits return output else: return "Error: gitea/dev.py operations not available." except Exception as e: return f"Error: Failed to get branch status. {type(e).__name__}: {e}" async def suggest_branch_name( self, issue_text: str, prefix: Optional[str] = None, __user__: dict = None, ) -> str: """ Suggest a branch name based on issue requirements. This function analyzes the issue text and suggests appropriate branch names following naming conventions. Args: issue_text: Raw issue text prefix: Optional scope prefix (defaults to 'feature') __user__: User context Returns: str: Suggested branch name(s) """ # Extract issue number issue_match = re.search(r"# (\d+):", issue_text) issue_id = issue_match.group(1) if issue_match else "" # Extract title/description title_match = re.search(r"# Issue #?\d+:? (.+)", issue_text) title = title_match.group(1).strip() if title_match else "" # Determine scope from labels or content scope = prefix or "feature" if "bug" in issue_text.lower() or "fix" in issue_text.lower(): scope = "fix" elif "doc" in issue_text.lower(): scope = "docs" elif "test" in issue_text.lower(): scope = "test" elif "refactor" in issue_text.lower(): scope = "refactor" # Generate slug from title if title: # Remove special characters and lowercase slug = re.sub(r"[^a-z0-9]+", "-", title.lower()) slug = slug.strip("-")[:30] else: slug = "unknown-change" # Build branch names suggestions = [] if issue_id: suggestions.append(f"{scope}/{issue_id}-{slug}") suggestions.append(f"{scope}/issue-{issue_id}-{slug}") else: suggestions.append(f"{scope}/{slug}") output = f"# Suggested Branch Names\n\n" output += f"**Based on:** {title or 'Issue requirements'}\n" output += f"**Suggested Scope:** {scope}\n\n" for i, name in enumerate(suggestions, 1): output += f"{i}. `{name}`\n" output += "\n**Note:** Add - if not already included." return output async def workflow_summary( self, repo: Optional[str] = None, __user__: dict = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Provide a summary of the gitea_coder workflow and available commands. This function lists all available commands and provides examples of how to use them for the coding workflow. Args: repo: Repository in 'owner/repo' format __user__: User context __event_emitter__: Event emitter callback Returns: str: Workflow summary """ effective_repo = self._get_repo(repo, __user__) output = f"# Gitea Coder Workflow\n\n" output += f"**Repository:** {effective_repo or 'Not configured'}\n" output += f"**Default Branch:** {self.valves.DEFAULT_BRANCH}\n\n" output += "## Available Commands\n\n" output += "### 1. Start Working on Ticket\n" output += "`read_ticket(issue_number=N)` - Read ticket requirements\n" output += "`suggest_branch_name(issue_text)` - Get branch name suggestions\n" output += "`create_feature_branch(branch_name='feature/N-description')` - Create branch\n\n" output += "### 2. Make Changes\n" output += "`commit_changes(path, content, short_description, issue_number=N)` - Commit with auto-generated message\n\n" output += "### 3. Review & Submit\n" output += "`get_branch_status()` - Check current branch status\n" output += "`create_pull_request(title, head_branch, issue_number=N)` - Create PR\n\n" output += "### 3. Navigation\n" output += "`list_my_branches()` - List all branches by scope\n\n" output += "## Branch Naming Conventions\n\n" output += "**Allowed Scopes:**\n" for scope in self.valves.ALLOWED_SCOPES: output += f"- `{scope}/` - {self._get_scope_description(scope)}\n" output += "\n**Protected Branches:**\n" output += f"- {', '.join([f'`{b}`' for b in self.valves.PROTECTED_BRANCHES])}\n" output += "*(Cannot directly modify protected branches)*\n\n" output += "## Example Workflow\n\n" output += "```\n# Read ticket requirements\nawait read_ticket(issue_number=42)\n\n# Create feature branch\nawait create_feature_branch('feature/42-add-login-functionality')\n\n# Make changes and commit\nawait commit_changes(\n path='src/auth.py',\n content=new_code,\n short_description='add user authentication',\n issue_number=42\n)\n\n# Create PR when done\nawait create_pull_request(\n title='Add user authentication',\n head_branch='feature/42-add-login-functionality'\n)\n```\n" return output def _get_scope_description(self, scope: str) -> str: """Get description for a scope type""" descriptions = { "feature": "New functionality or enhancements", "fix": "Bug fixes", "refactor": "Code restructuring (no behavior change)", "docs": "Documentation only", "test": "Test additions/improvements", "chore": "Maintenance tasks", } return descriptions.get(scope, "Other changes")