claude-dev-cli 0.16.1__py3-none-any.whl → 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-dev-cli might be problematic. Click here for more details.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +424 -0
- claude_dev_cli/logging/__init__.py +6 -0
- claude_dev_cli/logging/logger.py +84 -0
- claude_dev_cli/logging/markdown_logger.py +131 -0
- claude_dev_cli/notifications/__init__.py +6 -0
- claude_dev_cli/notifications/notifier.py +69 -0
- claude_dev_cli/notifications/ntfy.py +87 -0
- claude_dev_cli/project/__init__.py +10 -0
- claude_dev_cli/project/bug_tracker.py +458 -0
- claude_dev_cli/project/context_gatherer.py +535 -0
- claude_dev_cli/project/executor.py +370 -0
- claude_dev_cli/tickets/__init__.py +7 -0
- claude_dev_cli/tickets/backend.py +229 -0
- claude_dev_cli/tickets/markdown.py +309 -0
- claude_dev_cli/tickets/repo_tickets.py +361 -0
- claude_dev_cli/vcs/__init__.py +6 -0
- claude_dev_cli/vcs/git.py +172 -0
- claude_dev_cli/vcs/manager.py +90 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/METADATA +600 -10
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/RECORD +25 -8
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Abstract notification interface for claude-dev-cli."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotificationPriority(Enum):
|
|
10
|
+
"""Notification priority levels."""
|
|
11
|
+
LOW = "low"
|
|
12
|
+
NORMAL = "normal"
|
|
13
|
+
HIGH = "high"
|
|
14
|
+
URGENT = "urgent"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class NotificationConfig:
|
|
19
|
+
"""Configuration for notification backend."""
|
|
20
|
+
backend: str # ntfy, telegram, discord, slack, email
|
|
21
|
+
enabled: bool = True
|
|
22
|
+
settings: Dict[str, Any] = None
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
"""Initialize settings."""
|
|
26
|
+
if self.settings is None:
|
|
27
|
+
self.settings = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Notifier(ABC):
|
|
31
|
+
"""Abstract base class for notification backends."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def send(
|
|
35
|
+
self,
|
|
36
|
+
title: str,
|
|
37
|
+
message: str,
|
|
38
|
+
priority: NotificationPriority = NotificationPriority.NORMAL,
|
|
39
|
+
tags: Optional[list] = None
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Send a notification.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
title: Notification title
|
|
45
|
+
message: Notification message
|
|
46
|
+
priority: Priority level
|
|
47
|
+
tags: Optional tags/labels
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if sent successfully
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def test_connection(self) -> bool:
|
|
56
|
+
"""Test if notification backend is properly configured.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if backend is accessible
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def get_backend_name(self) -> str:
|
|
64
|
+
"""Get the name of this notification backend.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Backend name
|
|
68
|
+
"""
|
|
69
|
+
return self.__class__.__name__.replace('Notifier', '').lower()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""ntfy.sh notification backend.
|
|
2
|
+
|
|
3
|
+
No signup required - works out of the box with public ntfy.sh server.
|
|
4
|
+
Users can self-host if desired.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NtfyNotifier(Notifier):
|
|
14
|
+
"""ntfy.sh notification backend.
|
|
15
|
+
|
|
16
|
+
Simple, free, no-signup push notifications via HTTP.
|
|
17
|
+
https://ntfy.sh
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, topic: str, server: str = "https://ntfy.sh"):
|
|
21
|
+
"""Initialize ntfy notifier.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
topic: ntfy topic name (choose any unique name)
|
|
25
|
+
server: ntfy server URL (default: public ntfy.sh)
|
|
26
|
+
"""
|
|
27
|
+
self.topic = topic
|
|
28
|
+
self.server = server.rstrip('/')
|
|
29
|
+
self.url = f"{self.server}/{topic}"
|
|
30
|
+
|
|
31
|
+
def send(
|
|
32
|
+
self,
|
|
33
|
+
title: str,
|
|
34
|
+
message: str,
|
|
35
|
+
priority: NotificationPriority = NotificationPriority.NORMAL,
|
|
36
|
+
tags: Optional[list] = None
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Send notification via ntfy."""
|
|
39
|
+
try:
|
|
40
|
+
headers = {
|
|
41
|
+
"Title": title,
|
|
42
|
+
"Priority": self._map_priority(priority)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if tags:
|
|
46
|
+
# ntfy supports emoji tags
|
|
47
|
+
headers["Tags"] = ",".join(tags)
|
|
48
|
+
|
|
49
|
+
response = requests.post(
|
|
50
|
+
self.url,
|
|
51
|
+
data=message.encode('utf-8'),
|
|
52
|
+
headers=headers,
|
|
53
|
+
timeout=10
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return response.status_code == 200
|
|
57
|
+
|
|
58
|
+
except (requests.RequestException, Exception):
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def test_connection(self) -> bool:
|
|
62
|
+
"""Test ntfy server connectivity."""
|
|
63
|
+
try:
|
|
64
|
+
# Send a test message
|
|
65
|
+
response = requests.post(
|
|
66
|
+
self.url,
|
|
67
|
+
data="Test notification from claude-dev-cli".encode('utf-8'),
|
|
68
|
+
headers={"Title": "Test"},
|
|
69
|
+
timeout=5
|
|
70
|
+
)
|
|
71
|
+
return response.status_code == 200
|
|
72
|
+
except requests.RequestException:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def _map_priority(self, priority: NotificationPriority) -> str:
|
|
76
|
+
"""Map NotificationPriority to ntfy priority levels."""
|
|
77
|
+
mapping = {
|
|
78
|
+
NotificationPriority.LOW: "2",
|
|
79
|
+
NotificationPriority.NORMAL: "3",
|
|
80
|
+
NotificationPriority.HIGH: "4",
|
|
81
|
+
NotificationPriority.URGENT: "5"
|
|
82
|
+
}
|
|
83
|
+
return mapping.get(priority, "3")
|
|
84
|
+
|
|
85
|
+
def get_backend_name(self) -> str:
|
|
86
|
+
"""Return backend name."""
|
|
87
|
+
return "ntfy"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Project management and automation for claude-dev-cli.
|
|
2
|
+
|
|
3
|
+
Note: This project will be renamed in the future to reflect its broader
|
|
4
|
+
automation capabilities beyond Claude AI integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from claude_dev_cli.project.executor import TicketExecutor
|
|
8
|
+
from claude_dev_cli.project.bug_tracker import BugTriageSystem, BugReport, BugSeverity, BugCategory
|
|
9
|
+
|
|
10
|
+
__all__ = ["TicketExecutor", "BugTriageSystem", "BugReport", "BugSeverity", "BugCategory"]
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Bug reporting and triage system for claude-dev-cli.
|
|
2
|
+
|
|
3
|
+
Handles bug reports independently from main project workflow,
|
|
4
|
+
with automatic triage, priority assignment, and categorization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from claude_dev_cli.tickets.backend import TicketBackend, Ticket
|
|
13
|
+
from claude_dev_cli.core import ClaudeClient
|
|
14
|
+
from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BugSeverity(Enum):
|
|
18
|
+
"""Bug severity levels."""
|
|
19
|
+
CRITICAL = "critical" # System down, data loss, security breach
|
|
20
|
+
HIGH = "high" # Major functionality broken
|
|
21
|
+
MEDIUM = "medium" # Functionality impaired but workaround exists
|
|
22
|
+
LOW = "low" # Minor issue, cosmetic
|
|
23
|
+
TRIVIAL = "trivial" # Typos, UI inconsistencies
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BugCategory(Enum):
|
|
27
|
+
"""Bug categories for classification."""
|
|
28
|
+
CRASH = "crash" # Application crashes
|
|
29
|
+
DATA_LOSS = "data-loss" # Data corruption or loss
|
|
30
|
+
SECURITY = "security" # Security vulnerabilities
|
|
31
|
+
PERFORMANCE = "performance" # Performance issues
|
|
32
|
+
UI_UX = "ui-ux" # User interface/experience issues
|
|
33
|
+
INTEGRATION = "integration" # Third-party integration issues
|
|
34
|
+
FUNCTIONALITY = "functionality" # Feature not working as expected
|
|
35
|
+
DOCUMENTATION = "documentation" # Documentation errors
|
|
36
|
+
OTHER = "other"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class BugReport:
|
|
41
|
+
"""Structured bug report."""
|
|
42
|
+
id: Optional[str]
|
|
43
|
+
title: str
|
|
44
|
+
description: str
|
|
45
|
+
|
|
46
|
+
# Reproduction information
|
|
47
|
+
steps_to_reproduce: List[str]
|
|
48
|
+
expected_behavior: str
|
|
49
|
+
actual_behavior: str
|
|
50
|
+
|
|
51
|
+
# Environment
|
|
52
|
+
environment: Optional[str] = None # production, staging, development
|
|
53
|
+
version: Optional[str] = None
|
|
54
|
+
platform: Optional[str] = None # OS, browser, etc.
|
|
55
|
+
|
|
56
|
+
# Classification (can be auto-assigned by triage)
|
|
57
|
+
severity: Optional[BugSeverity] = None
|
|
58
|
+
category: Optional[BugCategory] = None
|
|
59
|
+
priority: Optional[str] = None # critical, high, medium, low
|
|
60
|
+
|
|
61
|
+
# Additional context
|
|
62
|
+
stack_trace: Optional[str] = None
|
|
63
|
+
logs: Optional[str] = None
|
|
64
|
+
screenshots: List[str] = None
|
|
65
|
+
related_tickets: List[str] = None
|
|
66
|
+
|
|
67
|
+
# Metadata
|
|
68
|
+
reporter: Optional[str] = None
|
|
69
|
+
reported_at: Optional[datetime] = None
|
|
70
|
+
triaged: bool = False
|
|
71
|
+
assigned_to: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
def __post_init__(self):
|
|
74
|
+
"""Initialize default values."""
|
|
75
|
+
if self.screenshots is None:
|
|
76
|
+
self.screenshots = []
|
|
77
|
+
if self.related_tickets is None:
|
|
78
|
+
self.related_tickets = []
|
|
79
|
+
if self.reported_at is None:
|
|
80
|
+
self.reported_at = datetime.now()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BugTriageSystem:
|
|
84
|
+
"""AI-powered bug triage and classification system.
|
|
85
|
+
|
|
86
|
+
Automatically:
|
|
87
|
+
- Classifies bug severity based on description
|
|
88
|
+
- Categorizes bugs
|
|
89
|
+
- Suggests priority
|
|
90
|
+
- Identifies duplicate bugs
|
|
91
|
+
- Recommends assignment
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
ticket_backend: TicketBackend,
|
|
97
|
+
ai_client: Optional[ClaudeClient] = None,
|
|
98
|
+
notifier: Optional[Notifier] = None
|
|
99
|
+
):
|
|
100
|
+
"""Initialize bug triage system.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
ticket_backend: Ticket management backend
|
|
104
|
+
ai_client: AI client for triage analysis
|
|
105
|
+
notifier: Notification system for critical bugs
|
|
106
|
+
"""
|
|
107
|
+
self.ticket_backend = ticket_backend
|
|
108
|
+
self.ai_client = ai_client or ClaudeClient()
|
|
109
|
+
self.notifier = notifier
|
|
110
|
+
|
|
111
|
+
def submit_bug(self, bug_report: BugReport, auto_triage: bool = True) -> Ticket:
|
|
112
|
+
"""Submit a bug report and optionally auto-triage.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
bug_report: Bug report to submit
|
|
116
|
+
auto_triage: Whether to automatically triage
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Created ticket
|
|
120
|
+
"""
|
|
121
|
+
# Auto-triage if requested
|
|
122
|
+
if auto_triage and not bug_report.triaged:
|
|
123
|
+
bug_report = self.triage_bug(bug_report)
|
|
124
|
+
|
|
125
|
+
# Create ticket in backend
|
|
126
|
+
ticket = self.ticket_backend.create_task(
|
|
127
|
+
story_id=None, # Bugs are independent
|
|
128
|
+
title=f"[BUG] {bug_report.title}",
|
|
129
|
+
description=self._format_bug_description(bug_report),
|
|
130
|
+
ticket_type="bug",
|
|
131
|
+
priority=bug_report.priority or "medium",
|
|
132
|
+
labels=self._get_bug_labels(bug_report),
|
|
133
|
+
assignee=bug_report.assigned_to,
|
|
134
|
+
requirements=bug_report.steps_to_reproduce,
|
|
135
|
+
acceptance_criteria=[
|
|
136
|
+
f"Expected: {bug_report.expected_behavior}",
|
|
137
|
+
f"Actual: {bug_report.actual_behavior}"
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
bug_report.id = ticket.id
|
|
142
|
+
|
|
143
|
+
# Add detailed comment with stack trace/logs
|
|
144
|
+
if bug_report.stack_trace or bug_report.logs:
|
|
145
|
+
self._add_technical_details(ticket.id, bug_report)
|
|
146
|
+
|
|
147
|
+
# Notify if critical/high severity
|
|
148
|
+
if bug_report.severity in [BugSeverity.CRITICAL, BugSeverity.HIGH]:
|
|
149
|
+
self._notify_critical_bug(bug_report)
|
|
150
|
+
|
|
151
|
+
return ticket
|
|
152
|
+
|
|
153
|
+
def triage_bug(self, bug_report: BugReport) -> BugReport:
|
|
154
|
+
"""Use AI to automatically triage a bug report.
|
|
155
|
+
|
|
156
|
+
Analyzes bug description and assigns:
|
|
157
|
+
- Severity level
|
|
158
|
+
- Category
|
|
159
|
+
- Priority
|
|
160
|
+
- Potential duplicates
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
bug_report: Bug report to triage
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Bug report with triage information added
|
|
167
|
+
"""
|
|
168
|
+
triage_prompt = self._build_triage_prompt(bug_report)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
triage_response = self.ai_client.call(
|
|
172
|
+
triage_prompt,
|
|
173
|
+
system_prompt="You are an expert bug triage specialist. Analyze bugs and provide structured classification."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Parse triage response and update bug report
|
|
177
|
+
triage_data = self._parse_triage_response(triage_response)
|
|
178
|
+
|
|
179
|
+
bug_report.severity = triage_data.get('severity')
|
|
180
|
+
bug_report.category = triage_data.get('category')
|
|
181
|
+
bug_report.priority = triage_data.get('priority')
|
|
182
|
+
bug_report.triaged = True
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
# Fallback to manual triage
|
|
186
|
+
print(f"Auto-triage failed: {e}. Using fallback classification.")
|
|
187
|
+
bug_report = self._fallback_triage(bug_report)
|
|
188
|
+
|
|
189
|
+
return bug_report
|
|
190
|
+
|
|
191
|
+
def find_duplicates(self, bug_report: BugReport, threshold: float = 0.7) -> List[str]:
|
|
192
|
+
"""Find potential duplicate bug reports.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
bug_report: Bug to check for duplicates
|
|
196
|
+
threshold: Similarity threshold (0-1)
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of ticket IDs that might be duplicates
|
|
200
|
+
"""
|
|
201
|
+
# Get all existing bug tickets
|
|
202
|
+
existing_bugs = self.ticket_backend.list_tickets(
|
|
203
|
+
status="open"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
duplicates = []
|
|
207
|
+
|
|
208
|
+
# Simple keyword-based duplicate detection
|
|
209
|
+
# Could be enhanced with semantic similarity
|
|
210
|
+
bug_keywords = set(bug_report.title.lower().split())
|
|
211
|
+
|
|
212
|
+
for ticket in existing_bugs:
|
|
213
|
+
if ticket.ticket_type != "bug":
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
ticket_keywords = set(ticket.title.lower().split())
|
|
217
|
+
similarity = len(bug_keywords & ticket_keywords) / len(bug_keywords | ticket_keywords)
|
|
218
|
+
|
|
219
|
+
if similarity >= threshold:
|
|
220
|
+
duplicates.append(ticket.id)
|
|
221
|
+
|
|
222
|
+
return duplicates
|
|
223
|
+
|
|
224
|
+
def assign_bug(self, ticket_id: str, assignee: str, reason: Optional[str] = None) -> bool:
|
|
225
|
+
"""Assign a bug to a developer.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
ticket_id: Bug ticket ID
|
|
229
|
+
assignee: Developer to assign to
|
|
230
|
+
reason: Optional reason for assignment
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if successful
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
self.ticket_backend.update_ticket(ticket_id, assignee=assignee)
|
|
237
|
+
|
|
238
|
+
if reason:
|
|
239
|
+
self.ticket_backend.add_comment(
|
|
240
|
+
ticket_id,
|
|
241
|
+
f"Assigned to {assignee}\nReason: {reason}",
|
|
242
|
+
author="bug-triage-system"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return True
|
|
246
|
+
except Exception:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def escalate_bug(self, ticket_id: str, reason: str) -> bool:
|
|
250
|
+
"""Escalate a bug to higher priority.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
ticket_id: Bug ticket ID
|
|
254
|
+
reason: Escalation reason
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if successful
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
# Increase priority
|
|
261
|
+
ticket = self.ticket_backend.fetch_ticket(ticket_id)
|
|
262
|
+
|
|
263
|
+
priority_escalation = {
|
|
264
|
+
"low": "medium",
|
|
265
|
+
"medium": "high",
|
|
266
|
+
"high": "critical"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
new_priority = priority_escalation.get(ticket.priority, "critical")
|
|
270
|
+
|
|
271
|
+
self.ticket_backend.update_ticket(ticket_id, priority=new_priority)
|
|
272
|
+
self.ticket_backend.add_comment(
|
|
273
|
+
ticket_id,
|
|
274
|
+
f"🚨 Bug escalated to {new_priority} priority\nReason: {reason}",
|
|
275
|
+
author="bug-triage-system"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Notify about escalation
|
|
279
|
+
if self.notifier:
|
|
280
|
+
self.notifier.send(
|
|
281
|
+
f"Bug Escalated: {ticket_id}",
|
|
282
|
+
f"Priority: {new_priority}\nReason: {reason}",
|
|
283
|
+
priority=NotificationPriority.URGENT
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return True
|
|
287
|
+
except Exception:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def _build_triage_prompt(self, bug_report: BugReport) -> str:
|
|
291
|
+
"""Build prompt for AI triage analysis."""
|
|
292
|
+
prompt = f"""Analyze this bug report and provide triage classification.
|
|
293
|
+
|
|
294
|
+
**Bug Title:** {bug_report.title}
|
|
295
|
+
|
|
296
|
+
**Description:**
|
|
297
|
+
{bug_report.description}
|
|
298
|
+
|
|
299
|
+
**Steps to Reproduce:**
|
|
300
|
+
{chr(10).join(f'{i+1}. {step}' for i, step in enumerate(bug_report.steps_to_reproduce))}
|
|
301
|
+
|
|
302
|
+
**Expected Behavior:** {bug_report.expected_behavior}
|
|
303
|
+
**Actual Behavior:** {bug_report.actual_behavior}
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
if bug_report.stack_trace:
|
|
308
|
+
prompt += f"\n**Stack Trace:**\n```\n{bug_report.stack_trace[:500]}\n```\n"
|
|
309
|
+
|
|
310
|
+
if bug_report.environment:
|
|
311
|
+
prompt += f"\n**Environment:** {bug_report.environment}"
|
|
312
|
+
|
|
313
|
+
prompt += """
|
|
314
|
+
|
|
315
|
+
Provide triage analysis in this format:
|
|
316
|
+
|
|
317
|
+
SEVERITY: [critical/high/medium/low/trivial]
|
|
318
|
+
CATEGORY: [crash/data-loss/security/performance/ui-ux/integration/functionality/documentation/other]
|
|
319
|
+
PRIORITY: [critical/high/medium/low]
|
|
320
|
+
REASONING: [Brief explanation of classification]
|
|
321
|
+
SUGGESTED_ACTION: [Immediate action needed, if any]
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
return prompt
|
|
325
|
+
|
|
326
|
+
def _parse_triage_response(self, response: str) -> Dict[str, Any]:
|
|
327
|
+
"""Parse AI triage response."""
|
|
328
|
+
import re
|
|
329
|
+
|
|
330
|
+
result = {}
|
|
331
|
+
|
|
332
|
+
# Extract severity
|
|
333
|
+
severity_match = re.search(r'SEVERITY:\s*(\w+)', response, re.IGNORECASE)
|
|
334
|
+
if severity_match:
|
|
335
|
+
try:
|
|
336
|
+
result['severity'] = BugSeverity(severity_match.group(1).lower())
|
|
337
|
+
except ValueError:
|
|
338
|
+
result['severity'] = BugSeverity.MEDIUM
|
|
339
|
+
|
|
340
|
+
# Extract category
|
|
341
|
+
category_match = re.search(r'CATEGORY:\s*([\w-]+)', response, re.IGNORECASE)
|
|
342
|
+
if category_match:
|
|
343
|
+
try:
|
|
344
|
+
result['category'] = BugCategory(category_match.group(1).lower())
|
|
345
|
+
except ValueError:
|
|
346
|
+
result['category'] = BugCategory.OTHER
|
|
347
|
+
|
|
348
|
+
# Extract priority
|
|
349
|
+
priority_match = re.search(r'PRIORITY:\s*(\w+)', response, re.IGNORECASE)
|
|
350
|
+
if priority_match:
|
|
351
|
+
result['priority'] = priority_match.group(1).lower()
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
def _fallback_triage(self, bug_report: BugReport) -> BugReport:
|
|
356
|
+
"""Fallback triage based on keywords."""
|
|
357
|
+
title_lower = bug_report.title.lower()
|
|
358
|
+
desc_lower = bug_report.description.lower()
|
|
359
|
+
|
|
360
|
+
# Check for critical keywords
|
|
361
|
+
critical_keywords = ['crash', 'data loss', 'security', 'breach', 'exploit', 'down']
|
|
362
|
+
high_keywords = ['broken', 'error', 'fail', 'cannot', "doesn't work"]
|
|
363
|
+
|
|
364
|
+
if any(kw in title_lower or kw in desc_lower for kw in critical_keywords):
|
|
365
|
+
bug_report.severity = BugSeverity.CRITICAL
|
|
366
|
+
bug_report.priority = "critical"
|
|
367
|
+
elif any(kw in title_lower or kw in desc_lower for kw in high_keywords):
|
|
368
|
+
bug_report.severity = BugSeverity.HIGH
|
|
369
|
+
bug_report.priority = "high"
|
|
370
|
+
else:
|
|
371
|
+
bug_report.severity = BugSeverity.MEDIUM
|
|
372
|
+
bug_report.priority = "medium"
|
|
373
|
+
|
|
374
|
+
# Categorize
|
|
375
|
+
if 'crash' in title_lower or 'crash' in desc_lower:
|
|
376
|
+
bug_report.category = BugCategory.CRASH
|
|
377
|
+
elif 'security' in title_lower or 'security' in desc_lower:
|
|
378
|
+
bug_report.category = BugCategory.SECURITY
|
|
379
|
+
elif 'slow' in title_lower or 'performance' in desc_lower:
|
|
380
|
+
bug_report.category = BugCategory.PERFORMANCE
|
|
381
|
+
else:
|
|
382
|
+
bug_report.category = BugCategory.FUNCTIONALITY
|
|
383
|
+
|
|
384
|
+
bug_report.triaged = True
|
|
385
|
+
return bug_report
|
|
386
|
+
|
|
387
|
+
def _format_bug_description(self, bug_report: BugReport) -> str:
|
|
388
|
+
"""Format bug report into ticket description."""
|
|
389
|
+
desc = f"{bug_report.description}\n\n"
|
|
390
|
+
desc += "## Reproduction Steps\n"
|
|
391
|
+
for i, step in enumerate(bug_report.steps_to_reproduce, 1):
|
|
392
|
+
desc += f"{i}. {step}\n"
|
|
393
|
+
|
|
394
|
+
desc += f"\n## Expected Behavior\n{bug_report.expected_behavior}\n"
|
|
395
|
+
desc += f"\n## Actual Behavior\n{bug_report.actual_behavior}\n"
|
|
396
|
+
|
|
397
|
+
if bug_report.environment:
|
|
398
|
+
desc += f"\n## Environment\n{bug_report.environment}"
|
|
399
|
+
|
|
400
|
+
if bug_report.version:
|
|
401
|
+
desc += f"\nVersion: {bug_report.version}"
|
|
402
|
+
|
|
403
|
+
if bug_report.platform:
|
|
404
|
+
desc += f"\nPlatform: {bug_report.platform}"
|
|
405
|
+
|
|
406
|
+
return desc
|
|
407
|
+
|
|
408
|
+
def _get_bug_labels(self, bug_report: BugReport) -> List[str]:
|
|
409
|
+
"""Generate labels for bug ticket."""
|
|
410
|
+
labels = ["bug"]
|
|
411
|
+
|
|
412
|
+
if bug_report.severity:
|
|
413
|
+
labels.append(f"severity-{bug_report.severity.value}")
|
|
414
|
+
|
|
415
|
+
if bug_report.category:
|
|
416
|
+
labels.append(f"category-{bug_report.category.value}")
|
|
417
|
+
|
|
418
|
+
if bug_report.environment:
|
|
419
|
+
labels.append(f"env-{bug_report.environment}")
|
|
420
|
+
|
|
421
|
+
return labels
|
|
422
|
+
|
|
423
|
+
def _add_technical_details(self, ticket_id: str, bug_report: BugReport) -> None:
|
|
424
|
+
"""Add technical details as comment."""
|
|
425
|
+
details = "## Technical Details\n\n"
|
|
426
|
+
|
|
427
|
+
if bug_report.stack_trace:
|
|
428
|
+
details += "### Stack Trace\n```\n"
|
|
429
|
+
details += bug_report.stack_trace
|
|
430
|
+
details += "\n```\n\n"
|
|
431
|
+
|
|
432
|
+
if bug_report.logs:
|
|
433
|
+
details += "### Logs\n```\n"
|
|
434
|
+
details += bug_report.logs
|
|
435
|
+
details += "\n```\n"
|
|
436
|
+
|
|
437
|
+
self.ticket_backend.add_comment(ticket_id, details, author="bug-reporter")
|
|
438
|
+
|
|
439
|
+
def _notify_critical_bug(self, bug_report: BugReport) -> None:
|
|
440
|
+
"""Send notification for critical/high severity bugs."""
|
|
441
|
+
if not self.notifier:
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
severity_emoji = {
|
|
445
|
+
BugSeverity.CRITICAL: "🚨",
|
|
446
|
+
BugSeverity.HIGH: "⚠️"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
emoji = severity_emoji.get(bug_report.severity, "🐛")
|
|
450
|
+
|
|
451
|
+
self.notifier.send(
|
|
452
|
+
f"{emoji} {bug_report.severity.value.upper()} Bug Reported",
|
|
453
|
+
f"Title: {bug_report.title}\n"
|
|
454
|
+
f"Category: {bug_report.category.value if bug_report.category else 'unknown'}\n"
|
|
455
|
+
f"Environment: {bug_report.environment or 'unknown'}",
|
|
456
|
+
priority=NotificationPriority.URGENT if bug_report.severity == BugSeverity.CRITICAL else NotificationPriority.HIGH,
|
|
457
|
+
tags=["bug", "alert"]
|
|
458
|
+
)
|