Files
luzia/lib/known_issues_detector.py
admin ec33ac1936 Refactor cockpit to use DockerTmuxController pattern
Based on claude-code-tools TmuxCLIController, this refactor:

- Added DockerTmuxController class for robust tmux session management
- Implements send_keys() with configurable delay_enter
- Implements capture_pane() for output retrieval
- Implements wait_for_prompt() for pattern-based completion detection
- Implements wait_for_idle() for content-hash-based idle detection
- Implements wait_for_shell_prompt() for shell prompt detection

Also includes workflow improvements:
- Pre-task git snapshot before agent execution
- Post-task commit protocol in agent guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:42:16 -03:00

412 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Known Issues Detector - Pattern-based bug detection and auto-fix
Features:
1. Detect common error patterns in output
2. Match against known issues database
3. Suggest or auto-apply fixes
4. Learn from fixes applied
5. Report patterns to knowledge graph
"""
import json
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any
from datetime import datetime
from dataclasses import dataclass, asdict
@dataclass
class IssuePattern:
"""Pattern for detecting a known issue"""
name: str
description: str
error_patterns: List[str] # Regex patterns to match
fix: str # Description of fix
auto_fixable: bool = False
fix_command: Optional[str] = None # Shell command to auto-fix
project: Optional[str] = None # Project-specific issue
severity: str = "warning" # warning, error, critical
@dataclass
class DetectedIssue:
"""An issue detected in output"""
pattern_name: str
description: str
severity: str
message: str
suggested_fix: str
auto_fixable: bool
detected_at: str
class KnownIssuesDetector:
"""Detects and suggests fixes for known issues"""
def __init__(self, issues_db_path: Optional[Path] = None):
"""Initialize detector with known issues database
Args:
issues_db_path: Path to JSON file with issue patterns
"""
self.db_path = issues_db_path or Path("/opt/server-agents/orchestrator/config/known_issues.json")
self.patterns: List[IssuePattern] = []
self.detected_history: List[DetectedIssue] = []
self.fixes_applied: List[Dict[str, Any]] = []
self.load_patterns()
self._initialize_default_patterns()
def load_patterns(self) -> None:
"""Load issue patterns from database"""
if self.db_path.exists():
try:
data = json.loads(self.db_path.read_text())
for pattern_data in data.get("patterns", []):
self.patterns.append(IssuePattern(**pattern_data))
except Exception as e:
print(f"[Warning] Failed to load issue patterns: {e}")
def _initialize_default_patterns(self) -> None:
"""Initialize built-in common issue patterns"""
default_patterns = [
# Docker/Container issues
IssuePattern(
name="container_not_found",
description="Docker container not found or exited",
error_patterns=[
r"container .* not found",
r"docker: error response",
r"connection refused.*docker",
r"no such container"
],
fix="Restart container: luzia stop <project> && luzia <project> <task>",
auto_fixable=True,
severity="error"
),
# Permission issues
IssuePattern(
name="permission_denied",
description="Permission denied accessing file or command",
error_patterns=[
r"permission denied",
r"access denied",
r"not allowed to access"
],
fix="Check file permissions or use appropriate sudo/user context",
auto_fixable=False,
severity="error"
),
# Dependency/Package issues
IssuePattern(
name="module_not_found",
description="Python or Node module not found",
error_patterns=[
r"ModuleNotFoundError",
r"ImportError",
r"cannot find module",
r"npm ERR! code",
r"not installed"
],
fix="Install dependencies: npm install (Node) or pip install (Python)",
auto_fixable=True,
fix_command="npm install || pip install -r requirements.txt",
severity="error"
),
# Build/Compilation issues
IssuePattern(
name="build_failed",
description="Build or compilation failed",
error_patterns=[
r"build failed",
r"compilation error",
r"cannot find symbol",
r"SyntaxError",
r"type error"
],
fix="Check build output, fix source code, retry build",
auto_fixable=False,
severity="error"
),
# Configuration issues
IssuePattern(
name="config_corrupted",
description="Configuration file is corrupted or invalid",
error_patterns=[
r"invalid json",
r"malformed yaml",
r"configuration corrupted",
r"parse error.*config"
],
fix="Restore config from backup or regenerate",
auto_fixable=True,
fix_command="~/restore-claude-config.sh",
severity="critical"
),
# Network/Connection issues
IssuePattern(
name="connection_failed",
description="Network connection failed",
error_patterns=[
r"connection refused",
r"network unreachable",
r"timeout",
r"ECONNREFUSED",
r"cannot reach"
],
fix="Check network/service status: ping, netstat, systemctl",
auto_fixable=False,
severity="warning"
),
# Memory/Resource issues
IssuePattern(
name="out_of_memory",
description="Out of memory or resource limit exceeded",
error_patterns=[
r"out of memory",
r"OOM",
r"memory limit exceeded",
r"no space left"
],
fix="Cleanup old files/containers: luzia cleanup",
auto_fixable=True,
severity="critical"
),
# Type/Validation issues
IssuePattern(
name="type_mismatch",
description="Type checking or validation failure",
error_patterns=[
r"type.*error",
r"type check",
r"expected .* got",
r"validation error"
],
fix="Review types and validate inputs, run type checker",
auto_fixable=False,
severity="warning"
),
# File not found
IssuePattern(
name="file_not_found",
description="Required file not found",
error_patterns=[
r"no such file",
r"ENOENT",
r"cannot open",
r"file not found"
],
fix="Check file path and ensure it exists",
auto_fixable=False,
severity="error"
),
]
# Add only if not already in patterns
existing_names = {p.name for p in self.patterns}
for pattern in default_patterns:
if pattern.name not in existing_names:
self.patterns.append(pattern)
def detect_issues(self, output: str, error: str = "",
project: Optional[str] = None) -> List[DetectedIssue]:
"""Detect known issues in output
Args:
output: Task output text
error: Error message text
project: Optional project name for project-specific patterns
Returns:
List of detected issues
"""
detected = []
combined = f"{output}\n{error}".lower()
for pattern in self.patterns:
# Skip project-specific patterns if not applicable
if pattern.project and pattern.project != project:
continue
# Check if any error pattern matches
for error_pattern in pattern.error_patterns:
if re.search(error_pattern, combined, re.IGNORECASE):
issue = DetectedIssue(
pattern_name=pattern.name,
description=pattern.description,
severity=pattern.severity,
message=f"Detected: {pattern.description}",
suggested_fix=pattern.fix,
auto_fixable=pattern.auto_fixable,
detected_at=datetime.now().isoformat()
)
detected.append(issue)
self.detected_history.append(issue)
break # Don't match same pattern twice
return detected
def suggest_fix(self, issue: DetectedIssue) -> str:
"""Get detailed fix suggestion for an issue
Args:
issue: Detected issue
Returns:
Detailed fix suggestion
"""
pattern = next((p for p in self.patterns if p.name == issue.pattern_name), None)
if not pattern:
return issue.suggested_fix
if pattern.fix_command:
return f"Run: {pattern.fix_command}\n\nDetails: {pattern.fix}"
else:
return pattern.fix
def can_auto_fix(self, issue: DetectedIssue) -> bool:
"""Check if an issue can be auto-fixed
Args:
issue: Detected issue
Returns:
True if auto-fixable
"""
pattern = next((p for p in self.patterns if p.name == issue.pattern_name), None)
return pattern and pattern.auto_fixable if pattern else False
def get_fix_command(self, issue: DetectedIssue) -> Optional[str]:
"""Get the command to fix an issue
Args:
issue: Detected issue
Returns:
Shell command to execute, or None
"""
pattern = next((p for p in self.patterns if p.name == issue.pattern_name), None)
return pattern.fix_command if pattern else None
def record_fix_applied(self, issue: DetectedIssue, success: bool,
fix_details: Optional[str] = None) -> None:
"""Record that a fix was attempted
Args:
issue: Issue that was fixed
success: Whether fix was successful
fix_details: Optional details about fix
"""
self.fixes_applied.append({
"pattern_name": issue.pattern_name,
"applied_at": datetime.now().isoformat(),
"success": success,
"details": fix_details
})
def get_recent_issues(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get recently detected issues
Args:
limit: Max issues to return
Returns:
List of recent issues
"""
return [asdict(issue) for issue in self.detected_history[-limit:]]
def get_issue_statistics(self) -> Dict[str, Any]:
"""Get statistics about detected issues
Returns:
Statistics dict
"""
by_name = {}
by_severity = {"warning": 0, "error": 0, "critical": 0}
for issue in self.detected_history:
by_name[issue.pattern_name] = by_name.get(issue.pattern_name, 0) + 1
by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1
fix_success = sum(1 for f in self.fixes_applied if f["success"])
fix_attempts = len(self.fixes_applied)
return {
"total_detected": len(self.detected_history),
"by_pattern": by_name,
"by_severity": by_severity,
"fixes_attempted": fix_attempts,
"fixes_successful": fix_success,
"fix_success_rate": fix_success / fix_attempts if fix_attempts > 0 else 0
}
def export_patterns(self, output_path: Path) -> None:
"""Export patterns to JSON file
Args:
output_path: Path to write patterns to
"""
data = {
"patterns": [asdict(p) for p in self.patterns],
"exported_at": datetime.now().isoformat()
}
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(data, indent=2))
def add_pattern(self, pattern: IssuePattern) -> None:
"""Add a new issue pattern
Args:
pattern: Issue pattern to add
"""
# Check for duplicates
if not any(p.name == pattern.name for p in self.patterns):
self.patterns.append(pattern)
# Save to database
if self.db_path.exists():
self.export_patterns(self.db_path)
def format_issue_report(self, issues: List[DetectedIssue]) -> str:
"""Format detected issues as readable report
Args:
issues: List of detected issues
Returns:
Formatted report text
"""
if not issues:
return "No issues detected."
report = ["# Issue Detection Report\n"]
report.append(f"**Detection Time:** {datetime.now().isoformat()}\n")
# Group by severity
by_severity = {}
for issue in issues:
if issue.severity not in by_severity:
by_severity[issue.severity] = []
by_severity[issue.severity].append(issue)
# Write critical first, then error, then warning
for severity in ["critical", "error", "warning"]:
if severity in by_severity:
report.append(f"\n## {severity.upper()} ({len(by_severity[severity])})\n")
for issue in by_severity[severity]:
report.append(f"### {issue.pattern_name}")
report.append(f"**Description:** {issue.description}")
report.append(f"**Message:** {issue.message}")
report.append(f"**Suggested Fix:** {issue.suggested_fix}")
if issue.auto_fixable:
report.append("✅ **Auto-fixable:** Yes")
report.append("")
return "\n".join(report)