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>
This commit is contained in:
411
lib/known_issues_detector.py
Normal file
411
lib/known_issues_detector.py
Normal file
@@ -0,0 +1,411 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user