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.

@@ -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()