vibe-heal follows a modular, layered architecture with clear separation of concerns.
┌─────────────────────────────────────────────────────────┐
│ CLI Layer │
│ (typer, rich - user interface, progress, reporting) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Orchestration Layer │
│ (VibeHealOrchestrator - main workflow logic) │
└──┬──────────┬──────────┬──────────┬─────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│Config│ │SonarQube│ │AI Tool│ │ Git │
│ Mgmt │ │ Client │ │Manager │ │ Manager │
└──────┘ └────────┘ └────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│.env │ │SonarQube│ │Claude │ │ Git │
│files │ │ API │ │Code/ │ │ Repo │
│ │ │ │ │Aider │ │ │
└──────┘ └────────┘ └────────┘ └──────────┘
src/vibe_heal/
├── __init__.py
├── __main__.py # Entry point for `python -m vibe_heal`
├── cli.py # CLI interface (typer)
├── orchestrator.py # Main workflow orchestration
├── config/
│ ├── __init__.py
│ ├── models.py # Configuration Pydantic models
│ └── loader.py # Configuration loading logic
├── sonarqube/
│ ├── __init__.py
│ ├── client.py # SonarQube API client
│ └── models.py # SonarQube response models
├── ai_tools/
│ ├── __init__.py
│ ├── base.py # Abstract base class + AIToolType enum
│ ├── claude_code.py # Claude Code implementation
│ ├── aider.py # Aider implementation
│ ├── factory.py # Factory for creating AI tool instances
│ └── models.py # Shared models (FixResult, etc.)
├── git/
│ ├── __init__.py
│ └── manager.py # Git operations
├── processor/
│ ├── __init__.py
│ ├── issue_processor.py # Issue sorting and filtering
│ └── models.py # Processing models
└── utils/
├── __init__.py
├── logging.py # Logging setup
└── validators.py # Common validation utilities
config/)Purpose: Load and validate configuration from environment variables and config files.
Key Classes:
AIToolType: Enum for supported AI tools
class AIToolType(str, Enum):
CLAUDE_CODE = "claude-code"
AIDER = "aider"
VibeHealConfig: Pydantic model with settings
pydantic-settings to load from .env.vibeheal or .envKey fields:
| Field | Default | Description |
|—|—|—|
| sonarqube_url | — | SonarQube server URL |
| sonarqube_token | None | Auth token (preferred over username/password) |
| sonarqube_project_key | — | SonarQube project key |
| ai_tool | None | AI tool to use; auto-detected if not set |
| code_context_lines | 5 | Lines of context shown around issue for AI |
| include_rule_description | True | Include full rule docs in AI prompts |
| pre_commit_command | None | Pre-commit hook command (see below) |
pre_commit_command values:
None (default) — auto-detect: uses pre-commit run --files if pre-commit is installed, otherwise falls back to git hook run --ignore-missing pre-commit"" (empty string) — disabled: skip pre-commit hooks entirely"my-hook-runner --check"Example:
class VibeHealConfig(BaseSettings):
sonarqube_url: str
sonarqube_token: str | None = None
sonarqube_username: str | None = None
sonarqube_password: str | None = None
sonarqube_project_key: str
ai_tool: AIToolType | None = None # auto-detect if None
pre_commit_command: str | None = None # auto-detect if None
model_config = SettingsConfigDict(
env_file=['.env.vibeheal', '.env'],
env_file_encoding='utf-8',
env_prefix='',
)
sonarqube/)Purpose: Interface with SonarQube Web API to fetch issues.
Key Classes:
SonarQubeClient: HTTP client for SonarQube API
get_issues_for_file(file_path: str) -> list[SonarQubeIssue]SonarQubeIssue: Pydantic model representing an issue
class SonarQubeIssue(BaseModel):
key: str
rule: str
severity: str
message: str
line: int | None
component: str
status: str
SonarQube API Endpoints Used:
/api/issues/search - Search for issues
componentKeys, resolved=falseprocessor/)Purpose: Sort and filter issues to determine fix order.
Key Classes:
IssueProcessor: Business logic for issue processing
sort_issues(issues: list[SonarQubeIssue]) -> list[SonarQubeIssue]
filter_fixable(issues: list[SonarQubeIssue]) -> list[SonarQubeIssue]
ai_tools/)Purpose: Abstract interface for AI coding tools with multiple implementations.
Key Classes:
AIToolType (Enum): Enum defining supported AI tools
class AIToolType(str, Enum):
CLAUDE_CODE = "claude-code"
AIDER = "aider"
@property
def cli_command(self) -> str:
"""Get the CLI command name for this tool"""
return self.value
@property
def display_name(self) -> str:
"""Get human-readable display name"""
return {
AIToolType.CLAUDE_CODE: "Claude Code",
AIToolType.AIDER: "Aider",
}[self]
AITool (ABC): Abstract base class
class AITool(ABC):
tool_type: AIToolType
@abstractmethod
def is_available(self) -> bool:
"""Check if tool is installed and accessible"""
@abstractmethod
async def fix_issue(
self,
issue: SonarQubeIssue,
file_path: str
) -> FixResult:
"""Attempt to fix the issue"""
ClaudeCodeTool(AITool): Claude Code implementation
class ClaudeCodeTool(AITool):
tool_type = AIToolType.CLAUDE_CODE
def is_available(self) -> bool:
# Check for claude-code CLI
AiderTool(AITool): Aider implementation
class AiderTool(AITool):
tool_type = AIToolType.AIDER
def is_available(self) -> bool:
# Check for aider CLI
AIToolFactory: Factory for creating tool instances
class AIToolFactory:
@staticmethod
def create(tool_type: AIToolType) -> AITool:
"""Create an AI tool instance based on type"""
tool_map = {
AIToolType.CLAUDE_CODE: ClaudeCodeTool,
AIToolType.AIDER: AiderTool,
}
return tool_map[tool_type]()
@staticmethod
def detect_available() -> AIToolType | None:
"""Auto-detect first available AI tool"""
# Try Claude Code first, then Aider
for tool_type in AIToolType:
tool = AIToolFactory.create(tool_type)
if tool.is_available():
return tool_type
return None
FixResult: Result model
class FixResult(BaseModel):
success: bool
error_message: str | None = None
files_modified: list[str] = []
Prompt Template:
Fix the following SonarQube issue in {file_path}:
Rule: {rule}
Line: {line}
Severity: {severity}
Message: {message}
Please fix this issue while maintaining code functionality and style.
git/)Purpose: Handle git operations for committing fixes.
Key Classes:
GitManager: Wrapper around GitPython
__init__(repo_path, pre_commit_command) — pre_commit_command=None auto-detects, "" disablesis_repository() -> boolis_clean() -> boolcommit_fix(issue, files, ai_tool_type) -> str | Noneget_current_branch() -> str_run_pre_commit_hooks(files) -> list[str] — runs hooks, returns files modified by them_stage_and_commit(files, message) -> str | None — stages, hooks, re-stages, commits with retryPre-commit Hook Integration:
_stage_and_commit() runs pre-commit hooks before calling repo.index.commit() so that formatters like ruff-format can auto-fix staged files without causing the commit to fail:
1. Stage files (repo.index.add)
2. Run _run_pre_commit_hooks(files)
└─ pre-commit CLI (if installed) or git hook run fallback
3. If hooks modified files → re-stage them
4. Check if index differs from HEAD (skip if nothing to commit)
5. repo.index.commit(message) with one retry
└─ On GitError: re-stage any newly dirty files and retry once
Hook runner priority:
pre_commit_command from config (if set)pre-commit run --files <files> (if pre-commit CLI is on PATH)git hook run --ignore-missing pre-commit (native git fallback)Commit Message Format:
fix: [SQ-S1481] Remove unused import
SonarQube Issue: ABC123
Rule: python:S1481
Severity: MAJOR
Location: project:src/foo.py:42
Fixed by: vibe-heal using Claude Code
[vibe-heal](https://github.com/alexeieleusis/vibe-heal)
orchestrator.py)Purpose: Main workflow coordination.
Key Class:
VibeHealOrchestrator: Coordinates all components
class VibeHealOrchestrator:
def __init__(self, config: VibeHealConfig):
self.config = config
self.sonarqube_client = SonarQubeClient(config)
self.git_manager = GitManager()
self.ai_tool = self._initialize_ai_tool()
def _initialize_ai_tool(self) -> AITool:
"""Initialize AI tool based on config or auto-detect"""
if self.config.ai_tool:
tool_type = self.config.ai_tool
else:
tool_type = AIToolFactory.detect_available()
if not tool_type:
raise RuntimeError("No AI tool found (Claude Code or Aider)")
return AIToolFactory.create(tool_type)
async def fix_file(
self,
file_path: str,
dry_run: bool = False,
max_issues: int | None = None
) -> FixSummary:
# Main workflow
Workflow:
cli.py)Purpose: User interface.
Key Functions:
app = typer.Typer() - Main CLI app@app.command() decorated functions for commandsCommands:
vibe-heal fix <file_path> # Fix issues in file
--dry-run # Preview without committing
--max-issues N # Limit number of fixes
--ai-tool {claude-code,aider} # Override tool detection
--verbose # Debug logging
vibe-heal config # Show current configuration
vibe-heal version # Show version
CLI uses AIToolType enum:
@app.command()
def fix(
file_path: str,
dry_run: bool = False,
max_issues: int | None = None,
ai_tool: AIToolType | None = typer.Option(None, help="AI tool to use"),
verbose: bool = False,
):
# ...
1. User runs: vibe-heal fix src/foo.py
2. CLI parses arguments
├─> Load configuration
└─> Create VibeHealOrchestrator
3. Orchestrator.__init__()
├─> Initialize components
└─> _initialize_ai_tool()
├─> config.ai_tool is None
├─> AIToolFactory.detect_available()
│ ├─> Try ClaudeCodeTool().is_available() → True
│ └─> Return AIToolType.CLAUDE_CODE
└─> AIToolFactory.create(AIToolType.CLAUDE_CODE)
└─> Return ClaudeCodeTool instance
4. Orchestrator.fix_file("src/foo.py")
├─> GitManager.is_clean() → ✓
├─> SonarQubeClient.get_issues_for_file("src/foo.py")
│ └─> Returns [Issue1(line=50), Issue2(line=30), Issue3(line=10)]
├─> IssueProcessor.sort_issues()
│ └─> Returns [Issue1(line=50), Issue2(line=30), Issue3(line=10)]
│
├─> For Issue1 (line=50):
│ ├─> AITool.fix_issue(Issue1, "src/foo.py")
│ │ └─> Returns FixResult(success=True, files_modified=["src/foo.py"])
│ └─> GitManager.commit_fix(Issue1, ["src/foo.py"], AIToolType.CLAUDE_CODE)
│
├─> For Issue2 (line=30):
│ ├─> AITool.fix_issue(Issue2, "src/foo.py")
│ │ └─> Returns FixResult(success=True, files_modified=["src/foo.py"])
│ └─> GitManager.commit_fix(Issue2, ["src/foo.py"], AIToolType.CLAUDE_CODE)
│
└─> For Issue3 (line=10):
├─> AITool.fix_issue(Issue3, "src/foo.py")
│ └─> Returns FixResult(success=False, error_message="...")
└─> Track as failed
5. Return FixSummary(fixed=2, failed=1, skipped=0)
6. CLI displays summary report
git reset HEAD~N to undo N fixesvibe-heal undo command--ai-tool flag).env.vibeheal file.env filepytest-mock and responses library.env* to .gitignore (already done in template)httpx with connection pooling