claude-mpm 4.1.4__py3-none-any.whl → 4.1.5__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/tickets.py +365 -784
- claude_mpm/core/output_style_manager.py +24 -0
- claude_mpm/core/unified_agent_registry.py +46 -15
- claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
- claude_mpm/services/infrastructure/__init__.py +31 -5
- claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
- claude_mpm/services/infrastructure/monitoring/base.py +130 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +218 -0
- claude_mpm/services/infrastructure/monitoring/process.py +342 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
- claude_mpm/services/infrastructure/monitoring/service.py +367 -0
- claude_mpm/services/infrastructure/monitoring.py +67 -1030
- claude_mpm/services/project/analyzer.py +13 -4
- claude_mpm/services/project/analyzer_refactored.py +450 -0
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +410 -0
- claude_mpm/services/ticket_manager.py +5 -1
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/RECORD +41 -17
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/top_level.txt +0 -0
|
@@ -11,24 +11,37 @@ DESIGN DECISIONS:
|
|
|
11
11
|
- Maintain backward compatibility with existing ai-trackdown integration
|
|
12
12
|
- Support multiple output formats (json, yaml, table, text)
|
|
13
13
|
- Implement full CRUD operations plus search and workflow management
|
|
14
|
+
- Use service-oriented architecture to separate concerns
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
|
-
import json
|
|
17
|
-
import subprocess
|
|
18
17
|
import sys
|
|
19
18
|
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from ...constants import TicketCommands
|
|
22
|
-
from ...
|
|
21
|
+
from ...services.ticket_services import (
|
|
22
|
+
TicketCRUDService,
|
|
23
|
+
TicketFormatterService,
|
|
24
|
+
TicketSearchService,
|
|
25
|
+
TicketValidationService,
|
|
26
|
+
TicketWorkflowService,
|
|
27
|
+
)
|
|
23
28
|
from ..shared import BaseCommand, CommandResult
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class TicketsCommand(BaseCommand):
|
|
27
|
-
"""Tickets command using shared utilities."""
|
|
32
|
+
"""Tickets command using shared utilities and service-oriented architecture."""
|
|
28
33
|
|
|
29
34
|
def __init__(self):
|
|
35
|
+
"""Initialize the tickets command with services."""
|
|
30
36
|
super().__init__("tickets")
|
|
31
37
|
|
|
38
|
+
# Initialize services using dependency injection
|
|
39
|
+
self.crud_service = TicketCRUDService()
|
|
40
|
+
self.formatter = TicketFormatterService()
|
|
41
|
+
self.validator = TicketValidationService()
|
|
42
|
+
self.search_service = TicketSearchService()
|
|
43
|
+
self.workflow_service = TicketWorkflowService()
|
|
44
|
+
|
|
32
45
|
def validate_args(self, args) -> Optional[str]:
|
|
33
46
|
"""Validate command arguments."""
|
|
34
47
|
if not hasattr(args, "tickets_command") or not args.tickets_command:
|
|
@@ -67,123 +80,381 @@ class TicketsCommand(BaseCommand):
|
|
|
67
80
|
return CommandResult.error_result(f"Error executing tickets command: {e}")
|
|
68
81
|
|
|
69
82
|
def _create_ticket(self, args) -> CommandResult:
|
|
70
|
-
"""Create a new ticket."""
|
|
83
|
+
"""Create a new ticket using the CRUD service."""
|
|
71
84
|
try:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
# Prepare parameters
|
|
86
|
+
description = self.validator.sanitize_description(args.description)
|
|
87
|
+
tags = self.validator.sanitize_tags(args.tags)
|
|
88
|
+
|
|
89
|
+
# Validate creation parameters
|
|
90
|
+
params = {"title": args.title, "type": args.type, "priority": args.priority}
|
|
91
|
+
valid, error = self.validator.validate_create_params(params)
|
|
92
|
+
if not valid:
|
|
93
|
+
print(self.formatter.format_error(error))
|
|
94
|
+
return CommandResult.error_result(error)
|
|
95
|
+
|
|
96
|
+
# Create ticket via service
|
|
97
|
+
result = self.crud_service.create_ticket(
|
|
98
|
+
title=args.title,
|
|
99
|
+
ticket_type=args.type,
|
|
100
|
+
priority=args.priority,
|
|
101
|
+
description=description,
|
|
102
|
+
tags=tags,
|
|
103
|
+
parent_epic=getattr(args, "parent_epic", None),
|
|
104
|
+
parent_issue=getattr(args, "parent_issue", None),
|
|
77
105
|
)
|
|
106
|
+
|
|
107
|
+
if result["success"]:
|
|
108
|
+
# Format and display output
|
|
109
|
+
output_lines = self.formatter.format_ticket_created(
|
|
110
|
+
result["ticket_id"],
|
|
111
|
+
verbose=args.verbose,
|
|
112
|
+
type=args.type,
|
|
113
|
+
priority=args.priority,
|
|
114
|
+
tags=tags,
|
|
115
|
+
parent_epic=getattr(args, "parent_epic", None),
|
|
116
|
+
parent_issue=getattr(args, "parent_issue", None),
|
|
117
|
+
)
|
|
118
|
+
for line in output_lines:
|
|
119
|
+
print(line)
|
|
120
|
+
return CommandResult.success_result(result["message"])
|
|
121
|
+
print(self.formatter.format_error(result["error"]))
|
|
122
|
+
return CommandResult.error_result(result["error"])
|
|
123
|
+
|
|
78
124
|
except Exception as e:
|
|
79
125
|
self.logger.error(f"Error creating ticket: {e}")
|
|
80
126
|
return CommandResult.error_result(f"Error creating ticket: {e}")
|
|
81
127
|
|
|
82
128
|
def _list_tickets(self, args) -> CommandResult:
|
|
83
|
-
"""List tickets."""
|
|
129
|
+
"""List tickets using the CRUD service."""
|
|
84
130
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
# Get pagination parameters
|
|
132
|
+
page = getattr(args, "page", 1)
|
|
133
|
+
page_size = getattr(args, "page_size", 20)
|
|
134
|
+
limit = getattr(args, "limit", page_size)
|
|
135
|
+
|
|
136
|
+
# Validate pagination
|
|
137
|
+
valid, error = self.validator.validate_pagination(page, page_size)
|
|
138
|
+
if not valid:
|
|
139
|
+
print(self.formatter.format_error(error))
|
|
140
|
+
return CommandResult.error_result(error)
|
|
141
|
+
|
|
142
|
+
# Get filters
|
|
143
|
+
type_filter = getattr(args, "type", None) or "all"
|
|
144
|
+
status_filter = getattr(args, "status", None) or "all"
|
|
145
|
+
|
|
146
|
+
# List tickets via service
|
|
147
|
+
result = self.crud_service.list_tickets(
|
|
148
|
+
limit=limit,
|
|
149
|
+
page=page,
|
|
150
|
+
page_size=page_size,
|
|
151
|
+
type_filter=type_filter,
|
|
152
|
+
status_filter=status_filter,
|
|
90
153
|
)
|
|
154
|
+
|
|
155
|
+
if result["success"]:
|
|
156
|
+
# Format and display output
|
|
157
|
+
output_lines = self.formatter.format_ticket_list(
|
|
158
|
+
result["tickets"],
|
|
159
|
+
page=page,
|
|
160
|
+
page_size=page_size,
|
|
161
|
+
verbose=getattr(args, "verbose", False),
|
|
162
|
+
)
|
|
163
|
+
for line in output_lines:
|
|
164
|
+
print(line)
|
|
165
|
+
return CommandResult.success_result("Tickets listed successfully")
|
|
166
|
+
print(self.formatter.format_error(result["error"]))
|
|
167
|
+
return CommandResult.error_result(result["error"])
|
|
168
|
+
|
|
91
169
|
except Exception as e:
|
|
92
170
|
self.logger.error(f"Error listing tickets: {e}")
|
|
93
171
|
return CommandResult.error_result(f"Error listing tickets: {e}")
|
|
94
172
|
|
|
95
173
|
def _view_ticket(self, args) -> CommandResult:
|
|
96
|
-
"""View a specific ticket."""
|
|
174
|
+
"""View a specific ticket using the CRUD service."""
|
|
97
175
|
try:
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
# Get ticket ID
|
|
177
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
178
|
+
|
|
179
|
+
# Validate ticket ID
|
|
180
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
181
|
+
if not valid:
|
|
182
|
+
print(self.formatter.format_error(error))
|
|
183
|
+
return CommandResult.error_result(error)
|
|
184
|
+
|
|
185
|
+
# Get ticket via service
|
|
186
|
+
ticket = self.crud_service.get_ticket(ticket_id)
|
|
187
|
+
|
|
188
|
+
if ticket:
|
|
189
|
+
# Format and display output
|
|
190
|
+
output_lines = self.formatter.format_ticket_detail(
|
|
191
|
+
ticket, verbose=getattr(args, "verbose", False)
|
|
192
|
+
)
|
|
193
|
+
for line in output_lines:
|
|
194
|
+
print(line)
|
|
100
195
|
return CommandResult.success_result("Ticket viewed successfully")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
196
|
+
error_msg = f"Ticket {ticket_id} not found"
|
|
197
|
+
print(self.formatter.format_error(error_msg))
|
|
198
|
+
return CommandResult.error_result(error_msg)
|
|
199
|
+
|
|
104
200
|
except Exception as e:
|
|
105
201
|
self.logger.error(f"Error viewing ticket: {e}")
|
|
106
202
|
return CommandResult.error_result(f"Error viewing ticket: {e}")
|
|
107
203
|
|
|
108
204
|
def _update_ticket(self, args) -> CommandResult:
|
|
109
|
-
"""Update a ticket."""
|
|
205
|
+
"""Update a ticket using the CRUD service."""
|
|
110
206
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
207
|
+
# Get ticket ID
|
|
208
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
209
|
+
|
|
210
|
+
# Validate ticket ID
|
|
211
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
212
|
+
if not valid:
|
|
213
|
+
print(self.formatter.format_error(error))
|
|
214
|
+
return CommandResult.error_result(error)
|
|
215
|
+
|
|
216
|
+
# Prepare update parameters
|
|
217
|
+
description = None
|
|
218
|
+
if args.description:
|
|
219
|
+
description = self.validator.sanitize_description(args.description)
|
|
220
|
+
|
|
221
|
+
tags = None
|
|
222
|
+
if args.tags:
|
|
223
|
+
tags = self.validator.sanitize_tags(args.tags)
|
|
224
|
+
|
|
225
|
+
assignees = None
|
|
226
|
+
if args.assign:
|
|
227
|
+
assignees = [args.assign]
|
|
228
|
+
|
|
229
|
+
# Validate update parameters
|
|
230
|
+
update_params = {}
|
|
231
|
+
if args.status:
|
|
232
|
+
update_params["status"] = args.status
|
|
233
|
+
if args.priority:
|
|
234
|
+
update_params["priority"] = args.priority
|
|
235
|
+
|
|
236
|
+
valid, error = self.validator.validate_update_params(update_params)
|
|
237
|
+
if not valid:
|
|
238
|
+
print(self.formatter.format_error(error))
|
|
239
|
+
return CommandResult.error_result(error)
|
|
240
|
+
|
|
241
|
+
# Update ticket via service
|
|
242
|
+
result = self.crud_service.update_ticket(
|
|
243
|
+
ticket_id=ticket_id,
|
|
244
|
+
status=args.status,
|
|
245
|
+
priority=args.priority,
|
|
246
|
+
description=description,
|
|
247
|
+
tags=tags,
|
|
248
|
+
assignees=assignees,
|
|
116
249
|
)
|
|
250
|
+
|
|
251
|
+
if result["success"]:
|
|
252
|
+
print(self.formatter.format_operation_result("update", ticket_id, True))
|
|
253
|
+
return CommandResult.success_result(result["message"])
|
|
254
|
+
print(self.formatter.format_operation_result("update", ticket_id, False))
|
|
255
|
+
return CommandResult.error_result(result["error"])
|
|
256
|
+
|
|
117
257
|
except Exception as e:
|
|
118
258
|
self.logger.error(f"Error updating ticket: {e}")
|
|
119
259
|
return CommandResult.error_result(f"Error updating ticket: {e}")
|
|
120
260
|
|
|
121
261
|
def _close_ticket(self, args) -> CommandResult:
|
|
122
|
-
"""Close a ticket."""
|
|
262
|
+
"""Close a ticket using the CRUD service."""
|
|
123
263
|
try:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
264
|
+
# Get ticket ID
|
|
265
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
266
|
+
|
|
267
|
+
# Validate ticket ID
|
|
268
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
269
|
+
if not valid:
|
|
270
|
+
print(self.formatter.format_error(error))
|
|
271
|
+
return CommandResult.error_result(error)
|
|
272
|
+
|
|
273
|
+
# Get resolution
|
|
274
|
+
resolution = getattr(args, "resolution", getattr(args, "comment", None))
|
|
275
|
+
|
|
276
|
+
# Close ticket via service
|
|
277
|
+
result = self.crud_service.close_ticket(ticket_id, resolution)
|
|
278
|
+
|
|
279
|
+
if result["success"]:
|
|
280
|
+
print(self.formatter.format_operation_result("close", ticket_id, True))
|
|
281
|
+
return CommandResult.success_result(result["message"])
|
|
282
|
+
print(self.formatter.format_operation_result("close", ticket_id, False))
|
|
283
|
+
return CommandResult.error_result(result["error"])
|
|
284
|
+
|
|
130
285
|
except Exception as e:
|
|
131
286
|
self.logger.error(f"Error closing ticket: {e}")
|
|
132
287
|
return CommandResult.error_result(f"Error closing ticket: {e}")
|
|
133
288
|
|
|
134
289
|
def _delete_ticket(self, args) -> CommandResult:
|
|
135
|
-
"""Delete a ticket."""
|
|
290
|
+
"""Delete a ticket using the CRUD service."""
|
|
136
291
|
try:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
292
|
+
# Get ticket ID
|
|
293
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
294
|
+
|
|
295
|
+
# Validate ticket ID
|
|
296
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
297
|
+
if not valid:
|
|
298
|
+
print(self.formatter.format_error(error))
|
|
299
|
+
return CommandResult.error_result(error)
|
|
300
|
+
|
|
301
|
+
# Confirm deletion unless forced
|
|
302
|
+
if not args.force:
|
|
303
|
+
sys.stdout.flush()
|
|
304
|
+
|
|
305
|
+
# Check if we're in a TTY environment
|
|
306
|
+
if not sys.stdin.isatty():
|
|
307
|
+
print(
|
|
308
|
+
f"Are you sure you want to delete ticket {ticket_id}? (y/N): ",
|
|
309
|
+
end="",
|
|
310
|
+
flush=True,
|
|
311
|
+
)
|
|
312
|
+
try:
|
|
313
|
+
response = sys.stdin.readline().strip().lower()
|
|
314
|
+
response = response.replace("\r", "").replace("\n", "").strip()
|
|
315
|
+
except (EOFError, KeyboardInterrupt):
|
|
316
|
+
response = "n"
|
|
317
|
+
else:
|
|
318
|
+
try:
|
|
319
|
+
response = (
|
|
320
|
+
input(
|
|
321
|
+
f"Are you sure you want to delete ticket {ticket_id}? (y/N): "
|
|
322
|
+
)
|
|
323
|
+
.strip()
|
|
324
|
+
.lower()
|
|
325
|
+
)
|
|
326
|
+
except (EOFError, KeyboardInterrupt):
|
|
327
|
+
response = "n"
|
|
328
|
+
|
|
329
|
+
if response != "y":
|
|
330
|
+
print("Deletion cancelled")
|
|
331
|
+
return CommandResult.success_result("Deletion cancelled")
|
|
332
|
+
|
|
333
|
+
# Delete ticket via service
|
|
334
|
+
result = self.crud_service.delete_ticket(ticket_id, args.force)
|
|
335
|
+
|
|
336
|
+
if result["success"]:
|
|
337
|
+
print(self.formatter.format_operation_result("delete", ticket_id, True))
|
|
338
|
+
return CommandResult.success_result(result["message"])
|
|
339
|
+
print(self.formatter.format_operation_result("delete", ticket_id, False))
|
|
340
|
+
return CommandResult.error_result(result["error"])
|
|
341
|
+
|
|
143
342
|
except Exception as e:
|
|
144
343
|
self.logger.error(f"Error deleting ticket: {e}")
|
|
145
344
|
return CommandResult.error_result(f"Error deleting ticket: {e}")
|
|
146
345
|
|
|
147
346
|
def _search_tickets(self, args) -> CommandResult:
|
|
148
|
-
"""Search tickets."""
|
|
347
|
+
"""Search tickets using the search service."""
|
|
149
348
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
349
|
+
# Validate search query
|
|
350
|
+
valid, error = self.validator.validate_search_query(args.query)
|
|
351
|
+
if not valid:
|
|
352
|
+
print(self.formatter.format_error(error))
|
|
353
|
+
return CommandResult.error_result(error)
|
|
354
|
+
|
|
355
|
+
# Search tickets via service
|
|
356
|
+
tickets = self.search_service.search_tickets(
|
|
357
|
+
query=args.query,
|
|
358
|
+
type_filter=args.type if args.type else "all",
|
|
359
|
+
status_filter=args.status if args.status else "all",
|
|
360
|
+
limit=args.limit,
|
|
155
361
|
)
|
|
362
|
+
|
|
363
|
+
# Format and display results
|
|
364
|
+
output_lines = self.formatter.format_search_results(
|
|
365
|
+
tickets, args.query, show_snippets=True
|
|
366
|
+
)
|
|
367
|
+
for line in output_lines:
|
|
368
|
+
print(line)
|
|
369
|
+
|
|
370
|
+
return CommandResult.success_result("Tickets searched successfully")
|
|
371
|
+
|
|
156
372
|
except Exception as e:
|
|
157
373
|
self.logger.error(f"Error searching tickets: {e}")
|
|
158
374
|
return CommandResult.error_result(f"Error searching tickets: {e}")
|
|
159
375
|
|
|
160
376
|
def _add_comment(self, args) -> CommandResult:
|
|
161
|
-
"""Add a comment to a ticket."""
|
|
377
|
+
"""Add a comment to a ticket using the workflow service."""
|
|
162
378
|
try:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
379
|
+
# Get ticket ID
|
|
380
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
381
|
+
|
|
382
|
+
# Validate ticket ID
|
|
383
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
384
|
+
if not valid:
|
|
385
|
+
print(self.formatter.format_error(error))
|
|
386
|
+
return CommandResult.error_result(error)
|
|
387
|
+
|
|
388
|
+
# Prepare comment
|
|
389
|
+
comment = self.validator.sanitize_description(args.comment)
|
|
390
|
+
|
|
391
|
+
# Validate comment
|
|
392
|
+
valid, error = self.validator.validate_comment(comment)
|
|
393
|
+
if not valid:
|
|
394
|
+
print(self.formatter.format_error(error))
|
|
395
|
+
return CommandResult.error_result(error)
|
|
396
|
+
|
|
397
|
+
# Add comment via service
|
|
398
|
+
result = self.workflow_service.add_comment(ticket_id, comment)
|
|
399
|
+
|
|
400
|
+
if result["success"]:
|
|
401
|
+
print(
|
|
402
|
+
self.formatter.format_operation_result("comment", ticket_id, True)
|
|
403
|
+
)
|
|
404
|
+
return CommandResult.success_result(result["message"])
|
|
405
|
+
print(self.formatter.format_operation_result("comment", ticket_id, False))
|
|
406
|
+
return CommandResult.error_result(result["error"])
|
|
407
|
+
|
|
169
408
|
except Exception as e:
|
|
170
409
|
self.logger.error(f"Error adding comment: {e}")
|
|
171
410
|
return CommandResult.error_result(f"Error adding comment: {e}")
|
|
172
411
|
|
|
173
412
|
def _update_workflow(self, args) -> CommandResult:
|
|
174
|
-
"""Update workflow state."""
|
|
413
|
+
"""Update workflow state using the workflow service."""
|
|
175
414
|
try:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
415
|
+
# Get ticket ID
|
|
416
|
+
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
417
|
+
|
|
418
|
+
# Validate ticket ID
|
|
419
|
+
valid, error = self.validator.validate_ticket_id(ticket_id)
|
|
420
|
+
if not valid:
|
|
421
|
+
print(self.formatter.format_error(error))
|
|
422
|
+
return CommandResult.error_result(error)
|
|
423
|
+
|
|
424
|
+
# Validate workflow state
|
|
425
|
+
valid, error = self.validator.validate_workflow_state(args.state)
|
|
426
|
+
if not valid:
|
|
427
|
+
print(self.formatter.format_error(error))
|
|
428
|
+
return CommandResult.error_result(error)
|
|
429
|
+
|
|
430
|
+
# Get optional comment
|
|
431
|
+
comment = getattr(args, "comment", None)
|
|
432
|
+
|
|
433
|
+
# Update workflow via service
|
|
434
|
+
result = self.workflow_service.transition_ticket(
|
|
435
|
+
ticket_id, args.state, comment
|
|
181
436
|
)
|
|
437
|
+
|
|
438
|
+
if result["success"]:
|
|
439
|
+
print(
|
|
440
|
+
self.formatter.format_operation_result(
|
|
441
|
+
"workflow", ticket_id, True, result["message"]
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
return CommandResult.success_result(result["message"])
|
|
445
|
+
print(self.formatter.format_operation_result("workflow", ticket_id, False))
|
|
446
|
+
return CommandResult.error_result(result["error"])
|
|
447
|
+
|
|
182
448
|
except Exception as e:
|
|
183
449
|
self.logger.error(f"Error updating workflow: {e}")
|
|
184
450
|
return CommandResult.error_result(f"Error updating workflow: {e}")
|
|
185
451
|
|
|
186
452
|
|
|
453
|
+
# ========================================
|
|
454
|
+
# Backward compatibility functions
|
|
455
|
+
# ========================================
|
|
456
|
+
|
|
457
|
+
|
|
187
458
|
def manage_tickets(args):
|
|
188
459
|
"""
|
|
189
460
|
Main entry point for tickets command.
|
|
@@ -211,755 +482,65 @@ def list_tickets(args):
|
|
|
211
482
|
return manage_tickets(args)
|
|
212
483
|
|
|
213
484
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
WHY: This contains the original manage_tickets logic, preserved during migration
|
|
219
|
-
to BaseCommand pattern. Will be gradually refactored into the TicketsCommand class.
|
|
220
|
-
|
|
221
|
-
DESIGN DECISION: We use a subcommand pattern similar to git, allowing for
|
|
222
|
-
intuitive command structure like 'claude-mpm tickets create "title"'.
|
|
485
|
+
# ========================================
|
|
486
|
+
# Legacy function stubs for compatibility
|
|
487
|
+
# ========================================
|
|
223
488
|
|
|
224
|
-
Args:
|
|
225
|
-
args: Parsed command line arguments with 'tickets_command' attribute
|
|
226
489
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
logger = get_logger("cli.tickets")
|
|
231
|
-
|
|
232
|
-
# Handle case where no subcommand is provided - default to list
|
|
233
|
-
if not hasattr(args, "tickets_command") or not args.tickets_command:
|
|
234
|
-
# Default to list command for backward compatibility
|
|
235
|
-
args.tickets_command = TicketCommands.LIST.value
|
|
236
|
-
# Set default limit if not present
|
|
237
|
-
if not hasattr(args, "limit"):
|
|
238
|
-
args.limit = 10
|
|
239
|
-
if not hasattr(args, "verbose"):
|
|
240
|
-
args.verbose = False
|
|
241
|
-
|
|
242
|
-
# Map subcommands to handler functions
|
|
243
|
-
handlers = {
|
|
244
|
-
TicketCommands.CREATE.value: create_ticket_legacy,
|
|
245
|
-
TicketCommands.LIST.value: list_tickets_legacy,
|
|
246
|
-
TicketCommands.VIEW.value: view_ticket_legacy,
|
|
247
|
-
TicketCommands.UPDATE.value: update_ticket_legacy,
|
|
248
|
-
TicketCommands.CLOSE.value: close_ticket_legacy,
|
|
249
|
-
TicketCommands.DELETE.value: delete_ticket_legacy,
|
|
250
|
-
TicketCommands.SEARCH.value: search_tickets_legacy,
|
|
251
|
-
TicketCommands.COMMENT.value: add_comment_legacy,
|
|
252
|
-
TicketCommands.WORKFLOW.value: update_workflow_legacy,
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
# Execute the appropriate handler
|
|
256
|
-
handler = handlers.get(args.tickets_command)
|
|
257
|
-
if handler:
|
|
258
|
-
try:
|
|
259
|
-
return handler(args)
|
|
260
|
-
except KeyboardInterrupt:
|
|
261
|
-
logger.info("Operation cancelled by user")
|
|
262
|
-
return 1
|
|
263
|
-
except Exception as e:
|
|
264
|
-
logger.error(f"Error executing {args.tickets_command}: {e}")
|
|
265
|
-
if hasattr(args, "debug") and args.debug:
|
|
266
|
-
import traceback
|
|
267
|
-
|
|
268
|
-
traceback.print_exc()
|
|
269
|
-
return 1
|
|
270
|
-
else:
|
|
271
|
-
logger.error(f"Unknown ticket command: {args.tickets_command}")
|
|
272
|
-
return 1
|
|
490
|
+
def manage_tickets_legacy(args):
|
|
491
|
+
"""Legacy wrapper - redirects to new implementation."""
|
|
492
|
+
return manage_tickets(args)
|
|
273
493
|
|
|
274
494
|
|
|
275
495
|
def create_ticket_legacy(args):
|
|
276
|
-
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
WHY: Users need to create tickets to track work items, bugs, and features.
|
|
280
|
-
This command provides a streamlined interface for ticket creation.
|
|
281
|
-
|
|
282
|
-
DESIGN DECISION: We parse description from remaining args to allow natural
|
|
283
|
-
command line usage like: tickets create "title" -d This is a description
|
|
284
|
-
|
|
285
|
-
Args:
|
|
286
|
-
args: Arguments with title, type, priority, description, tags, etc.
|
|
287
|
-
|
|
288
|
-
Returns:
|
|
289
|
-
Exit code (0 for success, non-zero for errors)
|
|
290
|
-
"""
|
|
291
|
-
get_logger("cli.tickets")
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
from ...services.ticket_manager import TicketManager
|
|
295
|
-
except ImportError:
|
|
296
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
297
|
-
|
|
298
|
-
ticket_manager = TicketManager()
|
|
299
|
-
|
|
300
|
-
# Parse description from remaining args or use default
|
|
301
|
-
description = " ".join(args.description) if args.description else ""
|
|
302
|
-
|
|
303
|
-
# Parse tags
|
|
304
|
-
tags = args.tags.split(",") if args.tags else []
|
|
305
|
-
|
|
306
|
-
# Create ticket with all provided parameters
|
|
307
|
-
ticket_id = ticket_manager.create_ticket(
|
|
308
|
-
title=args.title,
|
|
309
|
-
ticket_type=args.type,
|
|
310
|
-
description=description,
|
|
311
|
-
priority=args.priority,
|
|
312
|
-
tags=tags,
|
|
313
|
-
source="claude-mpm-cli",
|
|
314
|
-
parent_epic=getattr(args, "parent_epic", None),
|
|
315
|
-
parent_issue=getattr(args, "parent_issue", None),
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
if ticket_id:
|
|
319
|
-
print(f"✅ Created ticket: {ticket_id}")
|
|
320
|
-
if args.verbose:
|
|
321
|
-
print(f" Type: {args.type}")
|
|
322
|
-
print(f" Priority: {args.priority}")
|
|
323
|
-
if tags:
|
|
324
|
-
print(f" Tags: {', '.join(tags)}")
|
|
325
|
-
if getattr(args, "parent_epic", None):
|
|
326
|
-
print(f" Parent Epic: {args.parent_epic}")
|
|
327
|
-
if getattr(args, "parent_issue", None):
|
|
328
|
-
print(f" Parent Issue: {args.parent_issue}")
|
|
329
|
-
return 0
|
|
330
|
-
print("❌ Failed to create ticket")
|
|
331
|
-
return 1
|
|
496
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
497
|
+
args.tickets_command = TicketCommands.CREATE.value
|
|
498
|
+
return manage_tickets(args)
|
|
332
499
|
|
|
333
500
|
|
|
334
501
|
def list_tickets_legacy(args):
|
|
335
|
-
"""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
WHY: Users need to review tickets created during Claude sessions. This command
|
|
339
|
-
provides a quick way to see recent tickets with their status and metadata.
|
|
340
|
-
|
|
341
|
-
DESIGN DECISION: We show tickets in a compact format with emoji status indicators
|
|
342
|
-
for better visual scanning. Filters allow focusing on specific ticket types/statuses.
|
|
343
|
-
|
|
344
|
-
Args:
|
|
345
|
-
args: Arguments with limit, type filter, status filter, verbose flag
|
|
346
|
-
|
|
347
|
-
Returns:
|
|
348
|
-
Exit code (0 for success, non-zero for errors)
|
|
349
|
-
"""
|
|
350
|
-
logger = get_logger("cli.tickets")
|
|
351
|
-
|
|
352
|
-
try:
|
|
353
|
-
# Get pagination parameters
|
|
354
|
-
page = getattr(args, "page", 1)
|
|
355
|
-
page_size = getattr(args, "page_size", 20)
|
|
356
|
-
limit = getattr(args, "limit", page_size) # Use page_size as default limit
|
|
357
|
-
|
|
358
|
-
# Validate pagination parameters
|
|
359
|
-
if page < 1:
|
|
360
|
-
print("❌ Page number must be 1 or greater")
|
|
361
|
-
return 1
|
|
362
|
-
if page_size < 1:
|
|
363
|
-
print("❌ Page size must be 1 or greater")
|
|
364
|
-
return 1
|
|
365
|
-
|
|
366
|
-
# Try to use ai-trackdown CLI directly for better pagination support
|
|
367
|
-
tickets = []
|
|
368
|
-
try:
|
|
369
|
-
# Build aitrackdown command with pagination
|
|
370
|
-
cmd = ["aitrackdown", "status", "tasks"]
|
|
371
|
-
|
|
372
|
-
# Calculate offset for pagination
|
|
373
|
-
offset = (page - 1) * page_size
|
|
374
|
-
total_needed = offset + page_size
|
|
375
|
-
|
|
376
|
-
# Request more tickets than needed to handle filtering
|
|
377
|
-
cmd.extend(["--limit", str(total_needed * 2)])
|
|
378
|
-
|
|
379
|
-
# Add filters
|
|
380
|
-
type_filter = getattr(args, "type", None) or "all"
|
|
381
|
-
if type_filter != "all" and type_filter is not None:
|
|
382
|
-
cmd.extend(["--type", type_filter])
|
|
383
|
-
|
|
384
|
-
status_filter = getattr(args, "status", None) or "all"
|
|
385
|
-
if status_filter != "all" and status_filter is not None:
|
|
386
|
-
cmd.extend(["--status", status_filter])
|
|
387
|
-
|
|
388
|
-
# Execute command
|
|
389
|
-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
390
|
-
|
|
391
|
-
# Parse JSON output
|
|
392
|
-
if result.stdout.strip():
|
|
393
|
-
try:
|
|
394
|
-
all_tickets = json.loads(result.stdout)
|
|
395
|
-
if isinstance(all_tickets, list):
|
|
396
|
-
# Apply pagination
|
|
397
|
-
start_idx = offset
|
|
398
|
-
end_idx = start_idx + page_size
|
|
399
|
-
tickets = all_tickets[start_idx:end_idx]
|
|
400
|
-
else:
|
|
401
|
-
tickets = []
|
|
402
|
-
except json.JSONDecodeError:
|
|
403
|
-
logger.warning(
|
|
404
|
-
"Failed to parse aitrackdown JSON output, falling back to stub"
|
|
405
|
-
)
|
|
406
|
-
tickets = []
|
|
407
|
-
|
|
408
|
-
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
409
|
-
logger.warning(f"aitrackdown command failed: {e}, falling back to stub")
|
|
410
|
-
# Fallback to stub implementation
|
|
411
|
-
try:
|
|
412
|
-
from ...services.ticket_manager import TicketManager
|
|
413
|
-
except ImportError:
|
|
414
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
415
|
-
|
|
416
|
-
ticket_manager = TicketManager()
|
|
417
|
-
all_tickets = ticket_manager.list_recent_tickets(limit=limit * 2)
|
|
418
|
-
|
|
419
|
-
# Apply filters and pagination manually for stub
|
|
420
|
-
filtered_tickets = []
|
|
421
|
-
for ticket in all_tickets:
|
|
422
|
-
# Type filter
|
|
423
|
-
if type_filter != "all":
|
|
424
|
-
ticket_type = ticket.get("metadata", {}).get(
|
|
425
|
-
"ticket_type", "unknown"
|
|
426
|
-
)
|
|
427
|
-
if ticket_type != type_filter:
|
|
428
|
-
continue
|
|
429
|
-
|
|
430
|
-
# Status filter
|
|
431
|
-
if status_filter != "all":
|
|
432
|
-
if ticket.get("status") != status_filter:
|
|
433
|
-
continue
|
|
434
|
-
|
|
435
|
-
filtered_tickets.append(ticket)
|
|
436
|
-
|
|
437
|
-
# Apply pagination
|
|
438
|
-
start_idx = (page - 1) * page_size
|
|
439
|
-
end_idx = start_idx + page_size
|
|
440
|
-
tickets = filtered_tickets[start_idx:end_idx]
|
|
441
|
-
|
|
442
|
-
if not tickets:
|
|
443
|
-
print("No tickets found matching criteria")
|
|
444
|
-
if page > 1:
|
|
445
|
-
print(f"Try a lower page number (current: {page})")
|
|
446
|
-
return 0
|
|
447
|
-
|
|
448
|
-
# Display pagination info
|
|
449
|
-
total_shown = len(tickets)
|
|
450
|
-
print(f"Tickets (page {page}, showing {total_shown} tickets):")
|
|
451
|
-
print("-" * 80)
|
|
452
|
-
|
|
453
|
-
for ticket in tickets:
|
|
454
|
-
# Use emoji to indicate status visually
|
|
455
|
-
status_emoji = {
|
|
456
|
-
"open": "🔵",
|
|
457
|
-
"in_progress": "🟡",
|
|
458
|
-
"done": "🟢",
|
|
459
|
-
"closed": "⚫",
|
|
460
|
-
"blocked": "🔴",
|
|
461
|
-
}.get(ticket.get("status", "unknown"), "⚪")
|
|
462
|
-
|
|
463
|
-
print(f"{status_emoji} [{ticket['id']}] {ticket['title']}")
|
|
464
|
-
|
|
465
|
-
if getattr(args, "verbose", False):
|
|
466
|
-
ticket_type = ticket.get("metadata", {}).get("ticket_type", "task")
|
|
467
|
-
print(
|
|
468
|
-
f" Type: {ticket_type} | Status: {ticket['status']} | Priority: {ticket['priority']}"
|
|
469
|
-
)
|
|
470
|
-
if ticket.get("tags"):
|
|
471
|
-
print(f" Tags: {', '.join(ticket['tags'])}")
|
|
472
|
-
print(f" Created: {ticket['created_at']}")
|
|
473
|
-
print()
|
|
474
|
-
|
|
475
|
-
# Show pagination navigation hints
|
|
476
|
-
if total_shown == page_size:
|
|
477
|
-
print("-" * 80)
|
|
478
|
-
print(f"📄 Page {page} | Showing {total_shown} tickets")
|
|
479
|
-
print(
|
|
480
|
-
f"💡 Next page: claude-mpm tickets list --page {page + 1} --page-size {page_size}"
|
|
481
|
-
)
|
|
482
|
-
if page > 1:
|
|
483
|
-
print(
|
|
484
|
-
f"💡 Previous page: claude-mpm tickets list --page {page - 1} --page-size {page_size}"
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
return 0
|
|
488
|
-
|
|
489
|
-
except ImportError:
|
|
490
|
-
logger.error("ai-trackdown-pytools not installed")
|
|
491
|
-
print("Error: ai-trackdown-pytools not installed")
|
|
492
|
-
print("Install with: pip install ai-trackdown-pytools")
|
|
493
|
-
return 1
|
|
494
|
-
except Exception as e:
|
|
495
|
-
logger.error(f"Error listing tickets: {e}")
|
|
496
|
-
print(f"Error: {e}")
|
|
497
|
-
return 1
|
|
502
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
503
|
+
args.tickets_command = TicketCommands.LIST.value
|
|
504
|
+
return manage_tickets(args)
|
|
498
505
|
|
|
499
506
|
|
|
500
507
|
def view_ticket_legacy(args):
|
|
501
|
-
"""
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
WHY: Users need to see full ticket details including description, metadata,
|
|
505
|
-
and all associated information for understanding context and status.
|
|
506
|
-
|
|
507
|
-
Args:
|
|
508
|
-
args: Arguments with ticket id and verbose flag
|
|
509
|
-
|
|
510
|
-
Returns:
|
|
511
|
-
Exit code (0 for success, non-zero for errors)
|
|
512
|
-
"""
|
|
513
|
-
get_logger("cli.tickets")
|
|
514
|
-
|
|
515
|
-
try:
|
|
516
|
-
from ...services.ticket_manager import TicketManager
|
|
517
|
-
except ImportError:
|
|
518
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
519
|
-
|
|
520
|
-
ticket_manager = TicketManager()
|
|
521
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
522
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
523
|
-
if not ticket_id:
|
|
524
|
-
print("❌ No ticket ID provided")
|
|
525
|
-
return 1
|
|
526
|
-
ticket = ticket_manager.get_ticket(ticket_id)
|
|
527
|
-
|
|
528
|
-
if not ticket:
|
|
529
|
-
print(f"❌ Ticket {ticket_id} not found")
|
|
530
|
-
return 1
|
|
531
|
-
|
|
532
|
-
print(f"Ticket: {ticket['id']}")
|
|
533
|
-
print("=" * 80)
|
|
534
|
-
print(f"Title: {ticket['title']}")
|
|
535
|
-
print(f"Type: {ticket.get('metadata', {}).get('ticket_type', 'unknown')}")
|
|
536
|
-
print(f"Status: {ticket['status']}")
|
|
537
|
-
print(f"Priority: {ticket['priority']}")
|
|
538
|
-
|
|
539
|
-
if ticket.get("tags"):
|
|
540
|
-
print(f"Tags: {', '.join(ticket['tags'])}")
|
|
541
|
-
|
|
542
|
-
if ticket.get("assignees"):
|
|
543
|
-
print(f"Assignees: {', '.join(ticket['assignees'])}")
|
|
544
|
-
|
|
545
|
-
# Show parent references if they exist
|
|
546
|
-
metadata = ticket.get("metadata", {})
|
|
547
|
-
if metadata.get("parent_epic"):
|
|
548
|
-
print(f"Parent Epic: {metadata['parent_epic']}")
|
|
549
|
-
if metadata.get("parent_issue"):
|
|
550
|
-
print(f"Parent Issue: {metadata['parent_issue']}")
|
|
551
|
-
|
|
552
|
-
print("\nDescription:")
|
|
553
|
-
print("-" * 40)
|
|
554
|
-
print(ticket.get("description", "No description"))
|
|
555
|
-
|
|
556
|
-
print(f"\nCreated: {ticket['created_at']}")
|
|
557
|
-
print(f"Updated: {ticket['updated_at']}")
|
|
558
|
-
|
|
559
|
-
if args.verbose and ticket.get("metadata"):
|
|
560
|
-
print("\nMetadata:")
|
|
561
|
-
print("-" * 40)
|
|
562
|
-
for key, value in ticket["metadata"].items():
|
|
563
|
-
if key not in [
|
|
564
|
-
"parent_epic",
|
|
565
|
-
"parent_issue",
|
|
566
|
-
"ticket_type",
|
|
567
|
-
]: # Already shown above
|
|
568
|
-
print(f" {key}: {value}")
|
|
569
|
-
|
|
570
|
-
return 0
|
|
508
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
509
|
+
args.tickets_command = TicketCommands.VIEW.value
|
|
510
|
+
return manage_tickets(args)
|
|
571
511
|
|
|
572
512
|
|
|
573
513
|
def update_ticket_legacy(args):
|
|
574
|
-
"""
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
WHY: Tickets need to be updated as work progresses, priorities change,
|
|
578
|
-
or additional information becomes available.
|
|
579
|
-
|
|
580
|
-
DESIGN DECISION: For complex updates, we delegate to aitrackdown CLI
|
|
581
|
-
for operations not directly supported by our TicketManager interface.
|
|
582
|
-
|
|
583
|
-
Args:
|
|
584
|
-
args: Arguments with ticket id and update fields
|
|
585
|
-
|
|
586
|
-
Returns:
|
|
587
|
-
Exit code (0 for success, non-zero for errors)
|
|
588
|
-
"""
|
|
589
|
-
logger = get_logger("cli.tickets")
|
|
590
|
-
|
|
591
|
-
try:
|
|
592
|
-
from ...services.ticket_manager import TicketManager
|
|
593
|
-
except ImportError:
|
|
594
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
595
|
-
|
|
596
|
-
ticket_manager = TicketManager()
|
|
597
|
-
|
|
598
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
599
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
600
|
-
if not ticket_id:
|
|
601
|
-
print("❌ No ticket ID provided")
|
|
602
|
-
return 1
|
|
603
|
-
|
|
604
|
-
# Build update dictionary
|
|
605
|
-
updates = {}
|
|
606
|
-
|
|
607
|
-
if args.status:
|
|
608
|
-
updates["status"] = args.status
|
|
609
|
-
|
|
610
|
-
if args.priority:
|
|
611
|
-
updates["priority"] = args.priority
|
|
612
|
-
|
|
613
|
-
if args.description:
|
|
614
|
-
updates["description"] = " ".join(args.description)
|
|
615
|
-
|
|
616
|
-
if args.tags:
|
|
617
|
-
updates["tags"] = args.tags.split(",")
|
|
618
|
-
|
|
619
|
-
if args.assign:
|
|
620
|
-
updates["assignees"] = [args.assign]
|
|
621
|
-
|
|
622
|
-
if not updates:
|
|
623
|
-
print("❌ No updates specified")
|
|
624
|
-
return 1
|
|
625
|
-
|
|
626
|
-
# Try to update using TicketManager
|
|
627
|
-
success = ticket_manager.update_task(ticket_id, **updates)
|
|
628
|
-
|
|
629
|
-
if success:
|
|
630
|
-
print(f"✅ Updated ticket: {ticket_id}")
|
|
631
|
-
return 0
|
|
632
|
-
# Fallback to aitrackdown CLI for status transitions
|
|
633
|
-
if args.status:
|
|
634
|
-
logger.info("Attempting update via aitrackdown CLI")
|
|
635
|
-
cmd = ["aitrackdown", "transition", ticket_id, args.status]
|
|
636
|
-
|
|
637
|
-
# Add comment with other updates
|
|
638
|
-
comment_parts = []
|
|
639
|
-
if args.priority:
|
|
640
|
-
comment_parts.append(f"Priority: {args.priority}")
|
|
641
|
-
if args.assign:
|
|
642
|
-
comment_parts.append(f"Assigned to: {args.assign}")
|
|
643
|
-
if args.tags:
|
|
644
|
-
comment_parts.append(f"Tags: {args.tags}")
|
|
645
|
-
|
|
646
|
-
if comment_parts:
|
|
647
|
-
comment = " | ".join(comment_parts)
|
|
648
|
-
cmd.extend(["--comment", comment])
|
|
649
|
-
|
|
650
|
-
try:
|
|
651
|
-
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
652
|
-
print(f"✅ Updated ticket: {ticket_id}")
|
|
653
|
-
return 0
|
|
654
|
-
except subprocess.CalledProcessError as e:
|
|
655
|
-
logger.error(f"Failed to update via CLI: {e}")
|
|
656
|
-
print(f"❌ Failed to update ticket: {ticket_id}")
|
|
657
|
-
return 1
|
|
658
|
-
else:
|
|
659
|
-
print(f"❌ Failed to update ticket: {ticket_id}")
|
|
660
|
-
return 1
|
|
514
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
515
|
+
args.tickets_command = TicketCommands.UPDATE.value
|
|
516
|
+
return manage_tickets(args)
|
|
661
517
|
|
|
662
518
|
|
|
663
519
|
def close_ticket_legacy(args):
|
|
664
|
-
"""
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
WHY: Tickets need to be closed when work is completed or no longer relevant.
|
|
668
|
-
|
|
669
|
-
Args:
|
|
670
|
-
args: Arguments with ticket id and optional resolution
|
|
671
|
-
|
|
672
|
-
Returns:
|
|
673
|
-
Exit code (0 for success, non-zero for errors)
|
|
674
|
-
"""
|
|
675
|
-
logger = get_logger("cli.tickets")
|
|
676
|
-
|
|
677
|
-
try:
|
|
678
|
-
from ...services.ticket_manager import TicketManager
|
|
679
|
-
except ImportError:
|
|
680
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
681
|
-
|
|
682
|
-
ticket_manager = TicketManager()
|
|
683
|
-
|
|
684
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
685
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
686
|
-
if not ticket_id:
|
|
687
|
-
print("❌ No ticket ID provided")
|
|
688
|
-
return 1
|
|
689
|
-
|
|
690
|
-
# Try to close using TicketManager
|
|
691
|
-
resolution = getattr(args, "resolution", getattr(args, "comment", None))
|
|
692
|
-
success = ticket_manager.close_task(ticket_id, resolution=resolution)
|
|
693
|
-
|
|
694
|
-
if success:
|
|
695
|
-
print(f"✅ Closed ticket: {ticket_id}")
|
|
696
|
-
return 0
|
|
697
|
-
# Fallback to aitrackdown CLI
|
|
698
|
-
logger.info("Attempting close via aitrackdown CLI")
|
|
699
|
-
cmd = ["aitrackdown", "close", ticket_id]
|
|
700
|
-
|
|
701
|
-
if resolution:
|
|
702
|
-
cmd.extend(["--comment", resolution])
|
|
703
|
-
|
|
704
|
-
try:
|
|
705
|
-
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
706
|
-
print(f"✅ Closed ticket: {ticket_id}")
|
|
707
|
-
return 0
|
|
708
|
-
except subprocess.CalledProcessError:
|
|
709
|
-
print(f"❌ Failed to close ticket: {ticket_id}")
|
|
710
|
-
return 1
|
|
520
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
521
|
+
args.tickets_command = TicketCommands.CLOSE.value
|
|
522
|
+
return manage_tickets(args)
|
|
711
523
|
|
|
712
524
|
|
|
713
525
|
def delete_ticket_legacy(args):
|
|
714
|
-
"""
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
WHY: Sometimes tickets are created in error or are no longer needed
|
|
718
|
-
and should be removed from the system.
|
|
719
|
-
|
|
720
|
-
DESIGN DECISION: We delegate to aitrackdown CLI as deletion is a
|
|
721
|
-
destructive operation that should use the official tool.
|
|
722
|
-
|
|
723
|
-
Args:
|
|
724
|
-
args: Arguments with ticket id and force flag
|
|
725
|
-
|
|
726
|
-
Returns:
|
|
727
|
-
Exit code (0 for success, non-zero for errors)
|
|
728
|
-
"""
|
|
729
|
-
get_logger("cli.tickets")
|
|
730
|
-
|
|
731
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
732
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
733
|
-
if not ticket_id:
|
|
734
|
-
print("❌ No ticket ID provided")
|
|
735
|
-
return 1
|
|
736
|
-
|
|
737
|
-
# Confirm deletion unless forced
|
|
738
|
-
if not args.force:
|
|
739
|
-
sys.stdout.flush() # Ensure prompt is displayed before input
|
|
740
|
-
|
|
741
|
-
# Check if we're in a TTY environment for proper input handling
|
|
742
|
-
if not sys.stdin.isatty():
|
|
743
|
-
# In non-TTY environment (like pipes), use readline
|
|
744
|
-
print(
|
|
745
|
-
f"Are you sure you want to delete ticket {ticket_id}? (y/N): ",
|
|
746
|
-
end="",
|
|
747
|
-
flush=True,
|
|
748
|
-
)
|
|
749
|
-
try:
|
|
750
|
-
response = sys.stdin.readline().strip().lower()
|
|
751
|
-
# Handle various line endings and control characters
|
|
752
|
-
response = response.replace("\r", "").replace("\n", "").strip()
|
|
753
|
-
except (EOFError, KeyboardInterrupt):
|
|
754
|
-
response = "n"
|
|
755
|
-
else:
|
|
756
|
-
# In TTY environment, use normal input()
|
|
757
|
-
try:
|
|
758
|
-
response = (
|
|
759
|
-
input(
|
|
760
|
-
f"Are you sure you want to delete ticket {ticket_id}? (y/N): "
|
|
761
|
-
)
|
|
762
|
-
.strip()
|
|
763
|
-
.lower()
|
|
764
|
-
)
|
|
765
|
-
except (EOFError, KeyboardInterrupt):
|
|
766
|
-
response = "n"
|
|
767
|
-
|
|
768
|
-
if response != "y":
|
|
769
|
-
print("Deletion cancelled")
|
|
770
|
-
return 0
|
|
771
|
-
|
|
772
|
-
# Use aitrackdown CLI for deletion
|
|
773
|
-
cmd = ["aitrackdown", "delete", ticket_id]
|
|
774
|
-
if args.force:
|
|
775
|
-
cmd.append("--force")
|
|
776
|
-
|
|
777
|
-
try:
|
|
778
|
-
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
779
|
-
print(f"✅ Deleted ticket: {ticket_id}")
|
|
780
|
-
return 0
|
|
781
|
-
except subprocess.CalledProcessError:
|
|
782
|
-
print(f"❌ Failed to delete ticket: {ticket_id}")
|
|
783
|
-
return 1
|
|
526
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
527
|
+
args.tickets_command = TicketCommands.DELETE.value
|
|
528
|
+
return manage_tickets(args)
|
|
784
529
|
|
|
785
530
|
|
|
786
531
|
def search_tickets_legacy(args):
|
|
787
|
-
"""
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
WHY: Users need to find specific tickets based on content, tags, or other criteria.
|
|
791
|
-
|
|
792
|
-
DESIGN DECISION: We perform simple text matching on ticket data. For more advanced
|
|
793
|
-
search, users should use the aitrackdown CLI directly.
|
|
794
|
-
|
|
795
|
-
Args:
|
|
796
|
-
args: Arguments with search query and filters
|
|
797
|
-
|
|
798
|
-
Returns:
|
|
799
|
-
Exit code (0 for success, non-zero for errors)
|
|
800
|
-
"""
|
|
801
|
-
get_logger("cli.tickets")
|
|
802
|
-
|
|
803
|
-
try:
|
|
804
|
-
from ...services.ticket_manager import TicketManager
|
|
805
|
-
except ImportError:
|
|
806
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
807
|
-
|
|
808
|
-
ticket_manager = TicketManager()
|
|
809
|
-
|
|
810
|
-
# Get all available tickets for searching
|
|
811
|
-
all_tickets = ticket_manager.list_recent_tickets(limit=100)
|
|
812
|
-
|
|
813
|
-
# Search tickets
|
|
814
|
-
query = args.query.lower()
|
|
815
|
-
matched_tickets = []
|
|
816
|
-
|
|
817
|
-
for ticket in all_tickets:
|
|
818
|
-
# Check if query matches title, description, or tags
|
|
819
|
-
if (
|
|
820
|
-
query in ticket.get("title", "").lower()
|
|
821
|
-
or query in ticket.get("description", "").lower()
|
|
822
|
-
or any(query in tag.lower() for tag in ticket.get("tags", []))
|
|
823
|
-
):
|
|
824
|
-
# Apply type filter
|
|
825
|
-
if args.type != "all":
|
|
826
|
-
ticket_type = ticket.get("metadata", {}).get("ticket_type", "unknown")
|
|
827
|
-
if ticket_type != args.type:
|
|
828
|
-
continue
|
|
829
|
-
|
|
830
|
-
# Apply status filter
|
|
831
|
-
if args.status != "all" and ticket.get("status") != args.status:
|
|
832
|
-
continue
|
|
833
|
-
|
|
834
|
-
matched_tickets.append(ticket)
|
|
835
|
-
if len(matched_tickets) >= args.limit:
|
|
836
|
-
break
|
|
837
|
-
|
|
838
|
-
if not matched_tickets:
|
|
839
|
-
print(f"No tickets found matching '{args.query}'")
|
|
840
|
-
return 0
|
|
841
|
-
|
|
842
|
-
print(f"Search results for '{args.query}' (showing {len(matched_tickets)}):")
|
|
843
|
-
print("-" * 80)
|
|
844
|
-
|
|
845
|
-
for ticket in matched_tickets:
|
|
846
|
-
status_emoji = {
|
|
847
|
-
"open": "🔵",
|
|
848
|
-
"in_progress": "🟡",
|
|
849
|
-
"done": "🟢",
|
|
850
|
-
"closed": "⚫",
|
|
851
|
-
"blocked": "🔴",
|
|
852
|
-
}.get(ticket.get("status", "unknown"), "⚪")
|
|
853
|
-
|
|
854
|
-
print(f"{status_emoji} [{ticket['id']}] {ticket['title']}")
|
|
855
|
-
|
|
856
|
-
# Show snippet of description if it contains the query
|
|
857
|
-
desc = ticket.get("description", "")
|
|
858
|
-
if query in desc.lower():
|
|
859
|
-
# Find and show context around the match
|
|
860
|
-
idx = desc.lower().index(query)
|
|
861
|
-
start = max(0, idx - 30)
|
|
862
|
-
end = min(len(desc), idx + len(query) + 30)
|
|
863
|
-
snippet = desc[start:end]
|
|
864
|
-
if start > 0:
|
|
865
|
-
snippet = "..." + snippet
|
|
866
|
-
if end < len(desc):
|
|
867
|
-
snippet = snippet + "..."
|
|
868
|
-
print(f" {snippet}")
|
|
869
|
-
|
|
870
|
-
return 0
|
|
532
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
533
|
+
args.tickets_command = TicketCommands.SEARCH.value
|
|
534
|
+
return manage_tickets(args)
|
|
871
535
|
|
|
872
536
|
|
|
873
537
|
def add_comment_legacy(args):
|
|
874
|
-
"""
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
WHY: Comments allow tracking progress, decisions, and additional context
|
|
878
|
-
on tickets over time.
|
|
879
|
-
|
|
880
|
-
DESIGN DECISION: We delegate to aitrackdown CLI as it has proper comment
|
|
881
|
-
tracking infrastructure.
|
|
882
|
-
|
|
883
|
-
Args:
|
|
884
|
-
args: Arguments with ticket id and comment text
|
|
885
|
-
|
|
886
|
-
Returns:
|
|
887
|
-
Exit code (0 for success, non-zero for errors)
|
|
888
|
-
"""
|
|
889
|
-
get_logger("cli.tickets")
|
|
890
|
-
|
|
891
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
892
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
893
|
-
if not ticket_id:
|
|
894
|
-
print("❌ No ticket ID provided")
|
|
895
|
-
return 1
|
|
896
|
-
|
|
897
|
-
# Join comment parts into single string
|
|
898
|
-
comment = " ".join(args.comment) if isinstance(args.comment, list) else args.comment
|
|
899
|
-
|
|
900
|
-
# Use aitrackdown CLI for comments
|
|
901
|
-
cmd = ["aitrackdown", "comment", ticket_id, comment]
|
|
902
|
-
|
|
903
|
-
try:
|
|
904
|
-
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
905
|
-
print(f"✅ Added comment to ticket: {ticket_id}")
|
|
906
|
-
return 0
|
|
907
|
-
except subprocess.CalledProcessError:
|
|
908
|
-
print(f"❌ Failed to add comment to ticket: {ticket_id}")
|
|
909
|
-
return 1
|
|
538
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
539
|
+
args.tickets_command = TicketCommands.COMMENT.value
|
|
540
|
+
return manage_tickets(args)
|
|
910
541
|
|
|
911
542
|
|
|
912
543
|
def update_workflow_legacy(args):
|
|
913
|
-
"""
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
WHY: Workflow states track the progress of tickets through defined stages
|
|
917
|
-
like todo, in_progress, ready, tested, done.
|
|
918
|
-
|
|
919
|
-
DESIGN DECISION: We use aitrackdown's transition command for workflow updates
|
|
920
|
-
as it maintains proper state machine transitions.
|
|
921
|
-
|
|
922
|
-
Args:
|
|
923
|
-
args: Arguments with ticket id, new state, and optional comment
|
|
924
|
-
|
|
925
|
-
Returns:
|
|
926
|
-
Exit code (0 for success, non-zero for errors)
|
|
927
|
-
"""
|
|
928
|
-
get_logger("cli.tickets")
|
|
929
|
-
|
|
930
|
-
# Handle both 'id' and 'ticket_id' attributes for compatibility
|
|
931
|
-
ticket_id = getattr(args, "ticket_id", getattr(args, "id", None))
|
|
932
|
-
if not ticket_id:
|
|
933
|
-
print("❌ No ticket ID provided")
|
|
934
|
-
return 1
|
|
935
|
-
|
|
936
|
-
# Map workflow states to status if needed
|
|
937
|
-
|
|
938
|
-
# Use aitrackdown transition command
|
|
939
|
-
cmd = ["aitrackdown", "transition", ticket_id, args.state]
|
|
940
|
-
|
|
941
|
-
if getattr(args, "comment", None):
|
|
942
|
-
cmd.extend(["--comment", args.comment])
|
|
943
|
-
|
|
944
|
-
try:
|
|
945
|
-
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
946
|
-
print(f"✅ Updated workflow state for {ticket_id} to: {args.state}")
|
|
947
|
-
return 0
|
|
948
|
-
except subprocess.CalledProcessError:
|
|
949
|
-
print(f"❌ Failed to update workflow state for ticket: {ticket_id}")
|
|
950
|
-
return 1
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
# Maintain backward compatibility with the old list_tickets function signature
|
|
954
|
-
def list_tickets_legacy(args):
|
|
955
|
-
"""
|
|
956
|
-
Legacy list_tickets function for backward compatibility.
|
|
957
|
-
|
|
958
|
-
WHY: The old CLI interface expected a simple list_tickets function.
|
|
959
|
-
This wrapper maintains that interface while using the new implementation.
|
|
960
|
-
|
|
961
|
-
Args:
|
|
962
|
-
args: Parsed command line arguments with 'limit' attribute
|
|
963
|
-
"""
|
|
964
|
-
# Call the new list_tickets function
|
|
965
|
-
return list_tickets(args)
|
|
544
|
+
"""Legacy wrapper - uses new service implementation."""
|
|
545
|
+
args.tickets_command = TicketCommands.WORKFLOW.value
|
|
546
|
+
return manage_tickets(args)
|