#!/usr/bin/env python3 """ Centralized error messaging framework for autonomous-dev plugin. Provides consistent, helpful error messages following the pattern: - WHERE: Current context (Python env, directory, hook/script) - WHAT: What went wrong - HOW: Step-by-step fix instructions - LEARN MORE: Link to documentation All errors include error codes (ERR-XXX) for searchability. Design Patterns: See library-design-patterns skill for standardized design patterns. """ import os import sys from pathlib import Path from typing import Optional, List # Error code registry class ErrorCode: """Error code constants for autonomous-dev plugin.""" # Installation & Setup (ERR-100s) FORMATTER_NOT_FOUND = "ERR-101" PROJECT_MD_MISSING = "ERR-102" GITHUB_TOKEN_INVALID = "ERR-103" PYTHON_VERSION_MISMATCH = "ERR-104" DEPENDENCY_MISSING = "ERR-105" # Hook Errors (ERR-200s) HOOK_EXECUTION_FAILED = "ERR-201" HOOK_NOT_EXECUTABLE = "ERR-202" HOOK_TIMEOUT = "ERR-203" # Validation Errors (ERR-300s) VALIDATION_FAILED = "ERR-301" TEST_COVERAGE_LOW = "ERR-302" SECURITY_ISSUE_FOUND = "ERR-303" COMMAND_INVALID = "ERR-304" # File/Directory Errors (ERR-400s) FILE_NOT_FOUND = "ERR-401" DIRECTORY_NOT_FOUND = "ERR-402" PERMISSION_DENIED = "ERR-403" FILE_PARSE_ERROR = "ERR-404" # Configuration Errors (ERR-500s) CONFIG_MISSING = "ERR-501" CONFIG_INVALID = "ERR-502" ENVIRONMENT_MISMATCH = "ERR-503" class ErrorContext: """Captures current execution context for error messages.""" def __init__(self): self.python_path = sys.executable self.python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" self.working_dir = Path.cwd() self.script_name = Path(sys.argv[0]).name if sys.argv else "unknown" self.hook_type = os.environ.get('HOOK_TYPE', None) def format(self) -> str: """Format context for error messages.""" lines = [ "Where you are:", f" • Python: {self.python_path} (v{self.python_version})", f" • Working directory: {self.working_dir}", ] if self.hook_type: lines.append(f" • Hook: {self.script_name} ({self.hook_type})") else: lines.append(f" • Script: {self.script_name}") return "\n".join(lines) class ErrorMessage: """Builder for structured, helpful error messages.""" def __init__( self, code: str, title: str, what_wrong: str, how_to_fix: List[str], learn_more: Optional[str] = None, context: Optional[ErrorContext] = None ): self.code = code self.title = title self.what_wrong = what_wrong self.how_to_fix = how_to_fix self.learn_more = learn_more self.context = context or ErrorContext() def format(self, include_context: bool = True) -> str: """Format complete error message.""" lines = [ "", "=" * 70, f"ERROR: {self.title} [{self.code}]", "=" * 70, "" ] if include_context: lines.append(self.context.format()) lines.append("") lines.append("What's wrong:") lines.append(f" • {self.what_wrong}") lines.append("") lines.append("How to fix:") for i, step in enumerate(self.how_to_fix, 1): # Multi-line steps step_lines = step.split('\n') lines.append(f" {i}. {step_lines[0]}") for extra_line in step_lines[1:]: lines.append(f" {extra_line}") lines.append("") if self.learn_more: lines.append(f"Learn more: {self.learn_more}") lines.append("") lines.append("=" * 70) lines.append("") return "\n".join(lines) def print(self, include_context: bool = True, file=sys.stderr): """Print error message to stderr.""" print(self.format(include_context=include_context), file=file) # Common error message templates def formatter_not_found_error(formatter_name: str, python_path: str) -> ErrorMessage: """Standard error for missing formatters (black, isort, etc.)""" return ErrorMessage( code=ErrorCode.FORMATTER_NOT_FOUND, title=f"{formatter_name} not found", what_wrong=f"{formatter_name} formatter not installed in current Python environment", how_to_fix=[ f"Install in current environment:\n{python_path} -m pip install {formatter_name}", "OR use project virtualenv:\nsource venv/bin/activate\npip install {formatter_name}", "OR skip formatting for this commit:\ngit commit --no-verify" ], learn_more="docs/TROUBLESHOOTING.md#issue-1-hooks-not-running" ) def project_md_missing_error(expected_path: Path) -> ErrorMessage: """Standard error for missing PROJECT.md""" return ErrorMessage( code=ErrorCode.PROJECT_MD_MISSING, title="PROJECT.md not found", what_wrong=f"PROJECT.md file not found at: {expected_path}", how_to_fix=[ "Create PROJECT.md from template:\n/setup", "OR copy template manually:\ncp .claude/templates/PROJECT.md PROJECT.md\nvim PROJECT.md", "OR skip PROJECT.md validation (not recommended):\nDISABLE_PROJECT_MD=1 [your command]" ], learn_more="docs/TROUBLESHOOTING.md#issue-3-projectmd-missing" ) def dependency_missing_error( package_name: str, required_for: str, python_path: str ) -> ErrorMessage: """Standard error for missing Python dependencies""" return ErrorMessage( code=ErrorCode.DEPENDENCY_MISSING, title=f"Dependency missing: {package_name}", what_wrong=f"{package_name} is required for {required_for}", how_to_fix=[ f"Install dependency:\n{python_path} -m pip install {package_name}", "OR install all plugin dependencies:\npip install -r .claude/plugins/autonomous-dev/requirements.txt", f"OR disable {required_for}:\n[See documentation for disabling specific features]" ], learn_more="docs/TROUBLESHOOTING.md#dependency-issues" ) def validation_failed_error( what_failed: str, failures: List[str], fix_command: Optional[str] = None ) -> ErrorMessage: """Standard error for validation failures""" what_wrong = f"{what_failed}\n" + "\n".join(f" - {f}" for f in failures) how_to_fix = [] if fix_command: how_to_fix.append(f"Run fix command:\n{fix_command}") how_to_fix.extend([ "Review failures above and fix manually", "OR skip validation (not recommended):\ngit commit --no-verify" ]) return ErrorMessage( code=ErrorCode.VALIDATION_FAILED, title=f"{what_failed}", what_wrong=what_wrong, how_to_fix=how_to_fix, learn_more="docs/TROUBLESHOOTING.md#validation-failures" ) def file_not_found_error(file_path: Path, expected_purpose: str) -> ErrorMessage: """Standard error for missing files""" return ErrorMessage( code=ErrorCode.FILE_NOT_FOUND, title="File not found", what_wrong=f"Expected file not found: {file_path}\nPurpose: {expected_purpose}", how_to_fix=[ f"Create the file:\ntouch {file_path}", "OR check if file moved:\nfind . -name '{}' -type f".format(file_path.name), "OR restore from git:\ngit checkout HEAD -- {}".format(file_path) ], learn_more="docs/TROUBLESHOOTING.md#file-not-found" ) def config_invalid_error( config_file: Path, errors: List[str], example_config: Optional[str] = None ) -> ErrorMessage: """Standard error for invalid configuration""" what_wrong = f"Configuration file has errors: {config_file}\n" + "\n".join(f" - {e}" for e in errors) how_to_fix = [] if example_config: how_to_fix.append(f"Use example configuration:\n{example_config}") how_to_fix.extend([ f"Edit configuration:\nvim {config_file}", f"OR reset to defaults:\nmv {config_file} {config_file}.backup\n[regenerate config]" ]) return ErrorMessage( code=ErrorCode.CONFIG_INVALID, title="Invalid configuration", what_wrong=what_wrong, how_to_fix=how_to_fix, learn_more="docs/TROUBLESHOOTING.md#configuration-errors" ) # Utility functions def print_error(message: ErrorMessage, include_context: bool = True): """Print error message and exit with code 1.""" message.print(include_context=include_context) sys.exit(1) def print_warning(title: str, message: str, file=sys.stderr): """Print warning (non-fatal) message.""" print("", file=file) print("⚠️ WARNING: {}".format(title), file=file) print(f" {message}", file=file) print("", file=file) def print_info(title: str, message: str): """Print informational message.""" print("") print(f"ℹ️ {title}") print(f" {message}") print("") if __name__ == "__main__": # Demo error messages print("=" * 70) print("ERROR MESSAGE FRAMEWORK DEMO") print("=" * 70) print() # Example 1: Formatter not found err1 = formatter_not_found_error("black", sys.executable) err1.print() # Example 2: PROJECT.md missing err2 = project_md_missing_error(Path("PROJECT.md")) err2.print() # Example 3: Validation failed err3 = validation_failed_error( "Test coverage below minimum", ["src/module_a.py: 65% (needs 80%)", "src/module_b.py: 45% (needs 80%)"], fix_command="pytest --cov=src --cov-report=term-missing" ) err3.print() print() print("=" * 70) print("See lib/error_messages.py for full API") print("=" * 70)