claude-dev-cli 0.16.2__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.2.dist-info → claude_dev_cli-0.18.0.dist-info}/METADATA +335 -4
- {claude_dev_cli-0.16.2.dist-info → claude_dev_cli-0.18.0.dist-info}/RECORD +25 -8
- {claude_dev_cli-0.16.2.dist-info → claude_dev_cli-0.18.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.16.2.dist-info → claude_dev_cli-0.18.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.16.2.dist-info → claude_dev_cli-0.18.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.16.2.dist-info → claude_dev_cli-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Ticket execution engine for automated code generation.
|
|
2
|
+
|
|
3
|
+
Fetches tickets from external systems, analyzes requirements,
|
|
4
|
+
generates code/tests, and updates ticket status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from claude_dev_cli.tickets.backend import TicketBackend, Ticket
|
|
12
|
+
from claude_dev_cli.core import ClaudeClient
|
|
13
|
+
from claude_dev_cli.logging.logger import ProgressLogger
|
|
14
|
+
from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
|
|
15
|
+
from claude_dev_cli.vcs.manager import VCSManager
|
|
16
|
+
from claude_dev_cli.project.context_gatherer import TicketContextGatherer, CodeContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TicketExecutor:
|
|
20
|
+
"""Executes tickets by generating code/tests based on requirements.
|
|
21
|
+
|
|
22
|
+
This is the core automation engine that:
|
|
23
|
+
1. Fetches a ticket from backend (repo-tickets, Jira, etc.)
|
|
24
|
+
2. Analyzes requirements and acceptance criteria
|
|
25
|
+
3. Uses AI to generate implementation code
|
|
26
|
+
4. Uses AI to generate tests
|
|
27
|
+
5. Updates ticket status
|
|
28
|
+
6. Commits changes (optional)
|
|
29
|
+
7. Logs progress
|
|
30
|
+
8. Sends notifications
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
ticket_backend: TicketBackend,
|
|
36
|
+
ai_client: Optional[ClaudeClient] = None,
|
|
37
|
+
logger: Optional[ProgressLogger] = None,
|
|
38
|
+
notifier: Optional[Notifier] = None,
|
|
39
|
+
vcs: Optional[VCSManager] = None,
|
|
40
|
+
auto_commit: bool = False,
|
|
41
|
+
gather_context: bool = True,
|
|
42
|
+
project_root: Optional[Path] = None
|
|
43
|
+
):
|
|
44
|
+
"""Initialize ticket executor.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ticket_backend: Ticket management backend
|
|
48
|
+
ai_client: AI client for code generation
|
|
49
|
+
logger: Progress logger
|
|
50
|
+
notifier: Notification system
|
|
51
|
+
vcs: VCS manager
|
|
52
|
+
auto_commit: Whether to auto-commit changes
|
|
53
|
+
gather_context: Whether to gather codebase context before execution
|
|
54
|
+
project_root: Root of the project (default: current directory)
|
|
55
|
+
"""
|
|
56
|
+
self.ticket_backend = ticket_backend
|
|
57
|
+
self.ai_client = ai_client or ClaudeClient()
|
|
58
|
+
self.logger = logger
|
|
59
|
+
self.notifier = notifier
|
|
60
|
+
self.vcs = vcs
|
|
61
|
+
self.auto_commit = auto_commit
|
|
62
|
+
self.gather_context = gather_context
|
|
63
|
+
self.context_gatherer = TicketContextGatherer(project_root) if gather_context else None
|
|
64
|
+
|
|
65
|
+
def execute_ticket(self, ticket_id: str) -> bool:
|
|
66
|
+
"""Execute a single ticket end-to-end.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
ticket_id: Ticket identifier
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if execution successful
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# Step 1: Fetch ticket
|
|
76
|
+
self._log(f"Fetching ticket {ticket_id}...")
|
|
77
|
+
ticket = self.ticket_backend.fetch_ticket(ticket_id)
|
|
78
|
+
|
|
79
|
+
if not ticket:
|
|
80
|
+
self._log(f"Ticket {ticket_id} not found", level="error")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
self._log(f"✅ Fetched ticket: {ticket.title}", ticket_id=ticket_id, level="success")
|
|
84
|
+
self._notify(f"Starting {ticket_id}", f"Task: {ticket.title}")
|
|
85
|
+
|
|
86
|
+
# Step 2: Gather codebase context (optional pre-processing)
|
|
87
|
+
context = None
|
|
88
|
+
if self.gather_context and self.context_gatherer:
|
|
89
|
+
self._log("Gathering codebase context...", ticket_id=ticket_id)
|
|
90
|
+
context = self.context_gatherer.gather_context(ticket, self.ai_client)
|
|
91
|
+
self._log(
|
|
92
|
+
f"✅ Context gathered: {context.language}" +
|
|
93
|
+
(f" ({context.framework})" if context.framework else ""),
|
|
94
|
+
ticket_id=ticket_id,
|
|
95
|
+
level="success"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Step 3: Analyze requirements
|
|
99
|
+
self._log("Analyzing requirements...", ticket_id=ticket_id)
|
|
100
|
+
requirements_prompt = self._build_requirements_prompt(ticket, context)
|
|
101
|
+
|
|
102
|
+
# Step 4: Generate implementation plan
|
|
103
|
+
self._log("Generating implementation plan...", ticket_id=ticket_id)
|
|
104
|
+
plan = self.ai_client.call(
|
|
105
|
+
requirements_prompt,
|
|
106
|
+
system_prompt="You are an expert software engineer. Analyze the ticket and create a detailed implementation plan."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self._log("Implementation plan created", ticket_id=ticket_id, level="success")
|
|
110
|
+
|
|
111
|
+
# Step 5: Generate code
|
|
112
|
+
self._log("Generating code...", ticket_id=ticket_id)
|
|
113
|
+
code_prompt = self._build_code_generation_prompt(ticket, plan, context)
|
|
114
|
+
|
|
115
|
+
generated_code = self.ai_client.call(
|
|
116
|
+
code_prompt,
|
|
117
|
+
system_prompt="You are an expert software engineer. Generate clean, well-documented code based on the requirements."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Extract code from response (assuming it's in markdown code blocks)
|
|
121
|
+
code_files = self._extract_code_from_response(generated_code)
|
|
122
|
+
|
|
123
|
+
self._log(f"Generated {len(code_files)} file(s)", ticket_id=ticket_id, files=list(code_files.keys()))
|
|
124
|
+
|
|
125
|
+
# Step 6: Write files
|
|
126
|
+
for file_path, code_content in code_files.items():
|
|
127
|
+
self._write_file(file_path, code_content)
|
|
128
|
+
self._log(f"Created {file_path}", ticket_id=ticket_id)
|
|
129
|
+
|
|
130
|
+
if self.logger:
|
|
131
|
+
self.logger.link_artifact(ticket_id, file_path)
|
|
132
|
+
|
|
133
|
+
# Step 7: Generate tests
|
|
134
|
+
if ticket.acceptance_criteria:
|
|
135
|
+
self._log("Generating tests...", ticket_id=ticket_id)
|
|
136
|
+
test_prompt = self._build_test_generation_prompt(ticket, code_files)
|
|
137
|
+
|
|
138
|
+
test_code = self.ai_client.call(
|
|
139
|
+
test_prompt,
|
|
140
|
+
system_prompt="You are an expert test engineer. Generate comprehensive tests based on acceptance criteria."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
test_files = self._extract_code_from_response(test_code)
|
|
144
|
+
|
|
145
|
+
for test_file, test_content in test_files.items():
|
|
146
|
+
self._write_file(test_file, test_content)
|
|
147
|
+
self._log(f"Created test: {test_file}", ticket_id=ticket_id)
|
|
148
|
+
|
|
149
|
+
# Step 8: Update ticket status
|
|
150
|
+
self._log("Updating ticket status...", ticket_id=ticket_id)
|
|
151
|
+
self.ticket_backend.update_ticket(ticket_id, status="completed")
|
|
152
|
+
|
|
153
|
+
# Add completion comment
|
|
154
|
+
self.ticket_backend.add_comment(
|
|
155
|
+
ticket_id,
|
|
156
|
+
f"✅ Implementation completed by claude-dev-cli\n\nGenerated files:\n" +
|
|
157
|
+
"\n".join(f"- {f}" for f in code_files.keys()),
|
|
158
|
+
author="claude-dev-cli"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Step 9: Commit changes
|
|
162
|
+
if self.auto_commit and self.vcs and self.vcs.is_repository():
|
|
163
|
+
self._log("Committing changes...", ticket_id=ticket_id)
|
|
164
|
+
commit_message = f"feat({ticket_id}): {ticket.title}\n\nGenerated by claude-dev-cli"
|
|
165
|
+
|
|
166
|
+
commit_info = self.vcs.commit(
|
|
167
|
+
commit_message,
|
|
168
|
+
co_author="Warp <agent@warp.dev>"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._log(f"Committed: {commit_info.sha[:7]}", ticket_id=ticket_id, level="success")
|
|
172
|
+
|
|
173
|
+
# Step 10: Final notification
|
|
174
|
+
self._log(f"✅ Ticket {ticket_id} completed!", ticket_id=ticket_id, level="success")
|
|
175
|
+
self._notify(
|
|
176
|
+
f"✅ {ticket_id} Complete",
|
|
177
|
+
f"Task: {ticket.title}\nFiles: {len(code_files)}",
|
|
178
|
+
priority=NotificationPriority.HIGH
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self._log(f"Error executing ticket: {e}", ticket_id=ticket_id, level="error")
|
|
185
|
+
self._notify(
|
|
186
|
+
f"❌ {ticket_id} Failed",
|
|
187
|
+
f"Error: {str(e)}",
|
|
188
|
+
priority=NotificationPriority.URGENT
|
|
189
|
+
)
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def _build_requirements_prompt(self, ticket: Ticket, context: Optional[CodeContext] = None) -> str:
|
|
193
|
+
"""Build prompt for requirements analysis.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
ticket: Ticket to analyze
|
|
197
|
+
context: Optional codebase context
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Formatted prompt string
|
|
201
|
+
"""
|
|
202
|
+
prompt = f"""Analyze this software development ticket and create an implementation plan.
|
|
203
|
+
|
|
204
|
+
**Ticket:** {ticket.id}
|
|
205
|
+
**Title:** {ticket.title}
|
|
206
|
+
**Description:**
|
|
207
|
+
{ticket.description}
|
|
208
|
+
|
|
209
|
+
**Type:** {ticket.ticket_type}
|
|
210
|
+
**Priority:** {ticket.priority}
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
if ticket.requirements:
|
|
214
|
+
prompt += "\n**Requirements:**\n"
|
|
215
|
+
for req in ticket.requirements:
|
|
216
|
+
prompt += f"- {req}\n"
|
|
217
|
+
|
|
218
|
+
if ticket.acceptance_criteria:
|
|
219
|
+
prompt += "\n**Acceptance Criteria:**\n"
|
|
220
|
+
for criteria in ticket.acceptance_criteria:
|
|
221
|
+
prompt += f"- {criteria}\n"
|
|
222
|
+
|
|
223
|
+
# Add codebase context if available
|
|
224
|
+
if context:
|
|
225
|
+
prompt += "\n\n" + "="*50 + "\n"
|
|
226
|
+
prompt += "# CODEBASE CONTEXT\n"
|
|
227
|
+
prompt += "="*50 + "\n\n"
|
|
228
|
+
prompt += context.format_for_prompt()
|
|
229
|
+
prompt += "\n\n" + "="*50 + "\n\n"
|
|
230
|
+
|
|
231
|
+
prompt += "\n\nProvide a detailed implementation plan with:\n"
|
|
232
|
+
prompt += "1. Technical approach\n"
|
|
233
|
+
prompt += "2. Files to create/modify\n"
|
|
234
|
+
prompt += "3. Key functions/classes needed\n"
|
|
235
|
+
prompt += "4. Dependencies required\n"
|
|
236
|
+
|
|
237
|
+
if context:
|
|
238
|
+
prompt += "\n**IMPORTANT:** Follow the existing codebase patterns and conventions shown above.\n"
|
|
239
|
+
|
|
240
|
+
return prompt
|
|
241
|
+
|
|
242
|
+
def _build_code_generation_prompt(self, ticket: Ticket, plan: str, context: Optional[CodeContext] = None) -> str:
|
|
243
|
+
"""Build prompt for code generation.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
ticket: Ticket to implement
|
|
247
|
+
plan: Implementation plan from previous step
|
|
248
|
+
context: Optional codebase context
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Formatted prompt string
|
|
252
|
+
"""
|
|
253
|
+
prompt = f"""Generate production-ready code based on this ticket and implementation plan.
|
|
254
|
+
|
|
255
|
+
**Ticket:** {ticket.id} - {ticket.title}
|
|
256
|
+
|
|
257
|
+
**Implementation Plan:**
|
|
258
|
+
{plan}
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# Add context if available
|
|
262
|
+
if context:
|
|
263
|
+
prompt += "\n\n" + "="*50 + "\n"
|
|
264
|
+
prompt += "# EXISTING CODEBASE CONTEXT\n"
|
|
265
|
+
prompt += "="*50 + "\n\n"
|
|
266
|
+
|
|
267
|
+
if context.similar_files:
|
|
268
|
+
prompt += "**Similar existing files to reference:**\n"
|
|
269
|
+
for file_info in context.similar_files[:3]:
|
|
270
|
+
prompt += f"- {file_info['path']}: {file_info['purpose']}\n"
|
|
271
|
+
prompt += "\n"
|
|
272
|
+
|
|
273
|
+
if context.naming_conventions:
|
|
274
|
+
prompt += "**Project naming conventions:**\n"
|
|
275
|
+
for type_name, pattern in context.naming_conventions.items():
|
|
276
|
+
prompt += f"- {type_name}: {pattern}\n"
|
|
277
|
+
prompt += "\n"
|
|
278
|
+
|
|
279
|
+
if context.common_imports:
|
|
280
|
+
prompt += f"**Common imports in this project:** {', '.join(context.common_imports[:10])}\n\n"
|
|
281
|
+
|
|
282
|
+
prompt += "="*50 + "\n\n"
|
|
283
|
+
|
|
284
|
+
prompt += """**Requirements:**
|
|
285
|
+
Generate clean, well-documented, production-quality code. Include:
|
|
286
|
+
- Proper error handling
|
|
287
|
+
- Type hints (if Python)
|
|
288
|
+
- Docstrings/comments
|
|
289
|
+
- Follow best practices
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
if context:
|
|
293
|
+
prompt += "- **IMPORTANT:** Follow the existing codebase patterns and conventions shown above\n"
|
|
294
|
+
|
|
295
|
+
prompt += """\nOutput the code in markdown code blocks with file names as headers.
|
|
296
|
+
Example:
|
|
297
|
+
```python path/to/file.py
|
|
298
|
+
# code here
|
|
299
|
+
```
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
return prompt
|
|
303
|
+
|
|
304
|
+
def _build_test_generation_prompt(self, ticket: Ticket, code_files: dict) -> str:
|
|
305
|
+
"""Build prompt for test generation."""
|
|
306
|
+
prompt = f"""Generate comprehensive tests for the implemented code.
|
|
307
|
+
|
|
308
|
+
**Ticket:** {ticket.id} - {ticket.title}
|
|
309
|
+
|
|
310
|
+
**Acceptance Criteria:**
|
|
311
|
+
"""
|
|
312
|
+
for criteria in ticket.acceptance_criteria:
|
|
313
|
+
prompt += f"- {criteria}\n"
|
|
314
|
+
|
|
315
|
+
prompt += "\n**Generated Files:**\n"
|
|
316
|
+
for file_path in code_files.keys():
|
|
317
|
+
prompt += f"- {file_path}\n"
|
|
318
|
+
|
|
319
|
+
prompt += "\n\nGenerate test files that verify all acceptance criteria."
|
|
320
|
+
prompt += "\nUse appropriate testing framework (pytest for Python, jest for JS, etc.)"
|
|
321
|
+
|
|
322
|
+
return prompt
|
|
323
|
+
|
|
324
|
+
def _extract_code_from_response(self, response: str) -> dict:
|
|
325
|
+
"""Extract code blocks from AI response.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Dict mapping file paths to code content
|
|
329
|
+
"""
|
|
330
|
+
import re
|
|
331
|
+
|
|
332
|
+
code_files = {}
|
|
333
|
+
|
|
334
|
+
# Match markdown code blocks with file paths
|
|
335
|
+
# Pattern: ```language path/to/file.ext
|
|
336
|
+
pattern = r'```(?:\w+)?\s+([^\n]+)\n(.*?)```'
|
|
337
|
+
|
|
338
|
+
matches = re.findall(pattern, response, re.DOTALL)
|
|
339
|
+
|
|
340
|
+
for file_path, code_content in matches:
|
|
341
|
+
# Clean up file path
|
|
342
|
+
file_path = file_path.strip()
|
|
343
|
+
code_content = code_content.strip()
|
|
344
|
+
|
|
345
|
+
if file_path and code_content:
|
|
346
|
+
code_files[file_path] = code_content
|
|
347
|
+
|
|
348
|
+
return code_files
|
|
349
|
+
|
|
350
|
+
def _write_file(self, file_path: str, content: str) -> None:
|
|
351
|
+
"""Write content to file, creating directories if needed."""
|
|
352
|
+
path = Path(file_path)
|
|
353
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
|
|
355
|
+
with open(path, 'w') as f:
|
|
356
|
+
f.write(content)
|
|
357
|
+
|
|
358
|
+
def _log(self, message: str, ticket_id: Optional[str] = None, level: str = "info", **metadata) -> None:
|
|
359
|
+
"""Log a message."""
|
|
360
|
+
if self.logger:
|
|
361
|
+
self.logger.log(message, ticket_id=ticket_id, level=level, **metadata)
|
|
362
|
+
else:
|
|
363
|
+
# Fallback to print
|
|
364
|
+
timestamp = datetime.now().strftime('%H:%M:%S')
|
|
365
|
+
print(f"[{timestamp}] {message}")
|
|
366
|
+
|
|
367
|
+
def _notify(self, title: str, message: str, priority: NotificationPriority = NotificationPriority.NORMAL) -> None:
|
|
368
|
+
"""Send a notification."""
|
|
369
|
+
if self.notifier:
|
|
370
|
+
self.notifier.send(title, message, priority=priority)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Ticket management backend abstraction for claude-dev-cli."""
|
|
2
|
+
|
|
3
|
+
from claude_dev_cli.tickets.backend import TicketBackend
|
|
4
|
+
from claude_dev_cli.tickets.repo_tickets import RepoTicketsBackend
|
|
5
|
+
from claude_dev_cli.tickets.markdown import MarkdownBackend
|
|
6
|
+
|
|
7
|
+
__all__ = ["TicketBackend", "RepoTicketsBackend", "MarkdownBackend"]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Abstract ticket backend interface for claude-dev-cli.
|
|
2
|
+
|
|
3
|
+
Provides pluggable architecture for different ticket management systems.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Ticket:
|
|
14
|
+
"""Unified ticket representation across backends."""
|
|
15
|
+
id: str
|
|
16
|
+
title: str
|
|
17
|
+
description: str
|
|
18
|
+
status: str # open, in-progress, blocked, closed, etc.
|
|
19
|
+
priority: str # critical, high, medium, low
|
|
20
|
+
ticket_type: str # feature, bug, refactor, test, doc
|
|
21
|
+
assignee: Optional[str] = None
|
|
22
|
+
labels: List[str] = None
|
|
23
|
+
created_at: Optional[datetime] = None
|
|
24
|
+
updated_at: Optional[datetime] = None
|
|
25
|
+
|
|
26
|
+
# Epic/Story hierarchy
|
|
27
|
+
epic_id: Optional[str] = None
|
|
28
|
+
story_id: Optional[str] = None
|
|
29
|
+
parent_id: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
# Requirements and acceptance criteria
|
|
32
|
+
requirements: List[str] = None
|
|
33
|
+
acceptance_criteria: List[str] = None
|
|
34
|
+
user_stories: List[str] = None
|
|
35
|
+
|
|
36
|
+
# File context
|
|
37
|
+
files: List[str] = None
|
|
38
|
+
|
|
39
|
+
# Custom metadata
|
|
40
|
+
metadata: Dict[str, Any] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
"""Initialize default values."""
|
|
44
|
+
if self.labels is None:
|
|
45
|
+
self.labels = []
|
|
46
|
+
if self.requirements is None:
|
|
47
|
+
self.requirements = []
|
|
48
|
+
if self.acceptance_criteria is None:
|
|
49
|
+
self.acceptance_criteria = []
|
|
50
|
+
if self.user_stories is None:
|
|
51
|
+
self.user_stories = []
|
|
52
|
+
if self.files is None:
|
|
53
|
+
self.files = []
|
|
54
|
+
if self.metadata is None:
|
|
55
|
+
self.metadata = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Epic:
|
|
60
|
+
"""Unified epic representation."""
|
|
61
|
+
id: str
|
|
62
|
+
title: str
|
|
63
|
+
description: str
|
|
64
|
+
status: str
|
|
65
|
+
priority: str
|
|
66
|
+
owner: Optional[str] = None
|
|
67
|
+
ticket_ids: List[str] = None
|
|
68
|
+
goals: List[str] = None
|
|
69
|
+
created_at: Optional[datetime] = None
|
|
70
|
+
|
|
71
|
+
def __post_init__(self):
|
|
72
|
+
"""Initialize default values."""
|
|
73
|
+
if self.ticket_ids is None:
|
|
74
|
+
self.ticket_ids = []
|
|
75
|
+
if self.goals is None:
|
|
76
|
+
self.goals = []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Story:
|
|
81
|
+
"""Unified user story representation."""
|
|
82
|
+
id: str
|
|
83
|
+
title: str
|
|
84
|
+
description: str
|
|
85
|
+
epic_id: Optional[str] = None
|
|
86
|
+
status: str = "draft"
|
|
87
|
+
priority: str = "medium"
|
|
88
|
+
story_points: Optional[int] = None
|
|
89
|
+
acceptance_criteria: List[str] = None
|
|
90
|
+
|
|
91
|
+
def __post_init__(self):
|
|
92
|
+
"""Initialize default values."""
|
|
93
|
+
if self.acceptance_criteria is None:
|
|
94
|
+
self.acceptance_criteria = []
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TicketBackend(ABC):
|
|
98
|
+
"""Abstract base class for ticket management backends.
|
|
99
|
+
|
|
100
|
+
Implementations: RepoTicketsBackend, JiraBackend, LinearBackend, GitHubBackend, MarkdownBackend
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def connect(self) -> bool:
|
|
105
|
+
"""Connect to ticket backend and verify access.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if connection successful
|
|
109
|
+
"""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def fetch_ticket(self, ticket_id: str) -> Optional[Ticket]:
|
|
114
|
+
"""Fetch a ticket by ID.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
ticket_id: Ticket identifier
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Ticket object or None if not found
|
|
121
|
+
"""
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def create_epic(self, title: str, description: str = "", **kwargs) -> Epic:
|
|
126
|
+
"""Create a new epic.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
title: Epic title
|
|
130
|
+
description: Epic description
|
|
131
|
+
**kwargs: Additional epic fields (priority, owner, etc.)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Created Epic object
|
|
135
|
+
"""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def create_story(self, epic_id: str, title: str, description: str = "", **kwargs) -> Story:
|
|
140
|
+
"""Create a new user story within an epic.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
epic_id: Parent epic ID
|
|
144
|
+
title: Story title
|
|
145
|
+
description: Story description
|
|
146
|
+
**kwargs: Additional story fields
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Created Story object
|
|
150
|
+
"""
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def create_task(self, story_id: Optional[str], title: str, description: str = "", **kwargs) -> Ticket:
|
|
155
|
+
"""Create a new task/ticket.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
story_id: Parent story ID (optional)
|
|
159
|
+
title: Task title
|
|
160
|
+
description: Task description
|
|
161
|
+
**kwargs: Additional task fields (priority, assignee, labels, etc.)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Created Ticket object
|
|
165
|
+
"""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def update_ticket(self, ticket_id: str, **kwargs) -> Ticket:
|
|
170
|
+
"""Update a ticket's fields.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
ticket_id: Ticket identifier
|
|
174
|
+
**kwargs: Fields to update (status, description, assignee, etc.)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Updated Ticket object
|
|
178
|
+
"""
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def list_tickets(self, status: Optional[str] = None, epic_id: Optional[str] = None,
|
|
183
|
+
**filters) -> List[Ticket]:
|
|
184
|
+
"""List tickets with optional filters.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
status: Filter by status
|
|
188
|
+
epic_id: Filter by epic
|
|
189
|
+
**filters: Additional filters
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of matching tickets
|
|
193
|
+
"""
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def add_comment(self, ticket_id: str, comment: str, author: str = "") -> bool:
|
|
198
|
+
"""Add a comment to a ticket.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
ticket_id: Ticket identifier
|
|
202
|
+
comment: Comment text
|
|
203
|
+
author: Comment author
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if successful
|
|
207
|
+
"""
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def attach_file(self, ticket_id: str, file_path: str) -> bool:
|
|
212
|
+
"""Attach a file or reference to a ticket.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
ticket_id: Ticket identifier
|
|
216
|
+
file_path: Path to file
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if successful
|
|
220
|
+
"""
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
def get_backend_name(self) -> str:
|
|
224
|
+
"""Get the name of this backend.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Backend name (e.g., 'repo-tickets', 'jira', 'linear')
|
|
228
|
+
"""
|
|
229
|
+
return self.__class__.__name__.replace('Backend', '').lower()
|