claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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.
Files changed (81) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/research.json +39 -13
  3. claude_mpm/cli/__init__.py +2 -0
  4. claude_mpm/cli/commands/__init__.py +2 -0
  5. claude_mpm/cli/commands/configure.py +1221 -0
  6. claude_mpm/cli/commands/configure_tui.py +1921 -0
  7. claude_mpm/cli/commands/tickets.py +365 -784
  8. claude_mpm/cli/parsers/base_parser.py +7 -0
  9. claude_mpm/cli/parsers/configure_parser.py +119 -0
  10. claude_mpm/cli/startup_logging.py +39 -12
  11. claude_mpm/constants.py +1 -0
  12. claude_mpm/core/output_style_manager.py +24 -0
  13. claude_mpm/core/socketio_pool.py +35 -3
  14. claude_mpm/core/unified_agent_registry.py +46 -15
  15. claude_mpm/dashboard/static/css/connection-status.css +370 -0
  16. claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
  17. claude_mpm/dashboard/static/js/connection-manager.js +536 -0
  18. claude_mpm/dashboard/templates/index.html +11 -0
  19. claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
  20. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
  21. claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
  22. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
  23. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
  24. claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
  25. claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
  26. claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
  27. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
  28. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  29. claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
  30. claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
  31. claude_mpm/services/event_bus/direct_relay.py +173 -0
  32. claude_mpm/services/infrastructure/__init__.py +31 -5
  33. claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
  34. claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
  35. claude_mpm/services/infrastructure/monitoring/base.py +130 -0
  36. claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
  37. claude_mpm/services/infrastructure/monitoring/network.py +218 -0
  38. claude_mpm/services/infrastructure/monitoring/process.py +342 -0
  39. claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
  40. claude_mpm/services/infrastructure/monitoring/service.py +367 -0
  41. claude_mpm/services/infrastructure/monitoring.py +67 -1030
  42. claude_mpm/services/project/analyzer.py +13 -4
  43. claude_mpm/services/project/analyzer_refactored.py +450 -0
  44. claude_mpm/services/project/analyzer_v2.py +566 -0
  45. claude_mpm/services/project/architecture_analyzer.py +461 -0
  46. claude_mpm/services/project/dependency_analyzer.py +462 -0
  47. claude_mpm/services/project/language_analyzer.py +265 -0
  48. claude_mpm/services/project/metrics_collector.py +410 -0
  49. claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
  50. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  51. claude_mpm/services/socketio/server/connection_manager.py +516 -0
  52. claude_mpm/services/socketio/server/core.py +63 -0
  53. claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
  54. claude_mpm/services/socketio/server/main.py +27 -1
  55. claude_mpm/services/ticket_manager.py +5 -1
  56. claude_mpm/services/ticket_services/__init__.py +26 -0
  57. claude_mpm/services/ticket_services/crud_service.py +328 -0
  58. claude_mpm/services/ticket_services/formatter_service.py +290 -0
  59. claude_mpm/services/ticket_services/search_service.py +324 -0
  60. claude_mpm/services/ticket_services/validation_service.py +303 -0
  61. claude_mpm/services/ticket_services/workflow_service.py +244 -0
  62. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
  63. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
  64. claude_mpm/agents/OUTPUT_STYLE.md +0 -73
  65. claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
  66. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
  67. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
  68. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
  69. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
  70. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
  71. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
  72. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
  73. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
  74. claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
  75. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
  76. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
  77. claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
  78. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
  79. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
  80. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
  81. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.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 ...core.logger import get_logger
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
- exit_code = create_ticket_legacy(args)
73
- if exit_code == 0:
74
- return CommandResult.success_result("Ticket created successfully")
75
- return CommandResult.error_result(
76
- "Failed to create ticket", exit_code=exit_code
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
- exit_code = list_tickets_legacy(args)
86
- if exit_code == 0:
87
- return CommandResult.success_result("Tickets listed successfully")
88
- return CommandResult.error_result(
89
- "Failed to list tickets", exit_code=exit_code
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
- exit_code = view_ticket_legacy(args)
99
- if exit_code == 0:
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
- return CommandResult.error_result(
102
- "Failed to view ticket", exit_code=exit_code
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
- exit_code = update_ticket_legacy(args)
112
- if exit_code == 0:
113
- return CommandResult.success_result("Ticket updated successfully")
114
- return CommandResult.error_result(
115
- "Failed to update ticket", exit_code=exit_code
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
- exit_code = close_ticket_legacy(args)
125
- if exit_code == 0:
126
- return CommandResult.success_result("Ticket closed successfully")
127
- return CommandResult.error_result(
128
- "Failed to close ticket", exit_code=exit_code
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
- exit_code = delete_ticket_legacy(args)
138
- if exit_code == 0:
139
- return CommandResult.success_result("Ticket deleted successfully")
140
- return CommandResult.error_result(
141
- "Failed to delete ticket", exit_code=exit_code
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
- exit_code = search_tickets_legacy(args)
151
- if exit_code == 0:
152
- return CommandResult.success_result("Tickets searched successfully")
153
- return CommandResult.error_result(
154
- "Failed to search tickets", exit_code=exit_code
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
- exit_code = add_comment_legacy(args)
164
- if exit_code == 0:
165
- return CommandResult.success_result("Comment added successfully")
166
- return CommandResult.error_result(
167
- "Failed to add comment", exit_code=exit_code
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
- exit_code = update_workflow_legacy(args)
177
- if exit_code == 0:
178
- return CommandResult.success_result("Workflow updated successfully")
179
- return CommandResult.error_result(
180
- "Failed to update workflow", exit_code=exit_code
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
- def manage_tickets_legacy(args):
215
- """
216
- Legacy ticket command dispatcher.
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
- Returns:
228
- Exit code (0 for success, non-zero for errors)
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
- Create a new ticket.
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
- List recent tickets with optional filtering.
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
- View a specific ticket in detail.
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
- Update a ticket's properties.
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
- Close a ticket.
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
- Delete a ticket.
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
- Search tickets by query string.
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
- Add a comment to a ticket.
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
- Update ticket workflow state.
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)