claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__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/agents/BASE_PM.md +0 -106
- claude_mpm/agents/INSTRUCTIONS.md +0 -96
- claude_mpm/agents/MEMORY.md +88 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +2 -7
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +217 -5
- claude_mpm/config/__init__.py +30 -39
- claude_mpm/config/socketio_config.py +8 -5
- claude_mpm/constants.py +13 -0
- claude_mpm/core/__init__.py +8 -18
- claude_mpm/core/cache.py +596 -0
- claude_mpm/core/claude_runner.py +166 -622
- claude_mpm/core/config.py +5 -1
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +461 -22
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +208 -94
- claude_mpm/core/interactive_session.py +432 -0
- claude_mpm/core/interfaces.py +424 -0
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/logging_config.py +444 -0
- claude_mpm/core/oneshot_session.py +465 -0
- claude_mpm/core/optimized_agent_loader.py +485 -0
- claude_mpm/core/optimized_startup.py +490 -0
- claude_mpm/core/service_registry.py +52 -26
- claude_mpm/core/socketio_pool.py +162 -5
- claude_mpm/core/types.py +292 -0
- claude_mpm/core/typing_utils.py +477 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
- claude_mpm/init.py +2 -1
- claude_mpm/services/__init__.py +78 -14
- claude_mpm/services/agent/__init__.py +24 -0
- claude_mpm/services/agent/deployment.py +2548 -0
- claude_mpm/services/agent/management.py +598 -0
- claude_mpm/services/agent/registry.py +813 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +587 -268
- claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
- claude_mpm/services/async_session_logger.py +8 -3
- claude_mpm/services/communication/__init__.py +21 -0
- claude_mpm/services/communication/socketio.py +1933 -0
- claude_mpm/services/communication/websocket.py +479 -0
- claude_mpm/services/core/__init__.py +123 -0
- claude_mpm/services/core/base.py +247 -0
- claude_mpm/services/core/interfaces.py +951 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
- claude_mpm/services/framework_claude_md_generator.py +3 -2
- claude_mpm/services/health_monitor.py +4 -3
- claude_mpm/services/hook_service.py +64 -4
- claude_mpm/services/infrastructure/__init__.py +21 -0
- claude_mpm/services/infrastructure/logging.py +202 -0
- claude_mpm/services/infrastructure/monitoring.py +893 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/project/__init__.py +21 -0
- claude_mpm/services/project/analyzer.py +864 -0
- claude_mpm/services/project/registry.py +608 -0
- claude_mpm/services/project_analyzer.py +95 -2
- claude_mpm/services/recovery_manager.py +15 -9
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/base.py +121 -0
- claude_mpm/services/socketio/handlers/connection.py +198 -0
- claude_mpm/services/socketio/handlers/file.py +213 -0
- claude_mpm/services/socketio/handlers/git.py +723 -0
- claude_mpm/services/socketio/handlers/memory.py +27 -0
- claude_mpm/services/socketio/handlers/project.py +25 -0
- claude_mpm/services/socketio/handlers/registry.py +145 -0
- claude_mpm/services/socketio_client_manager.py +12 -7
- claude_mpm/services/socketio_server.py +156 -30
- claude_mpm/services/ticket_manager.py +170 -7
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +58 -21
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
| @@ -1,28 +1,161 @@ | |
| 1 1 | 
             
            """
         | 
| 2 2 | 
             
            Tickets command implementation for claude-mpm.
         | 
| 3 3 |  | 
| 4 | 
            -
            WHY: This module  | 
| 5 | 
            -
             | 
| 4 | 
            +
            WHY: This module provides comprehensive ticket management functionality, allowing users
         | 
| 5 | 
            +
            to create, view, update, and manage tickets through the CLI. It integrates with
         | 
| 6 | 
            +
            ai-trackdown-pytools for persistent ticket storage.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            DESIGN DECISION: We implement full CRUD operations plus search and workflow management
         | 
| 9 | 
            +
            to provide a complete ticket management system within the claude-mpm CLI. The commands
         | 
| 10 | 
            +
            mirror the scripts/ticket.py interface for consistency.
         | 
| 6 11 | 
             
            """
         | 
| 7 12 |  | 
| 13 | 
            +
            import sys
         | 
| 14 | 
            +
            import subprocess
         | 
| 15 | 
            +
            from typing import Optional, List, Dict, Any
         | 
| 16 | 
            +
            from pathlib import Path
         | 
| 17 | 
            +
             | 
| 8 18 | 
             
            from ...core.logger import get_logger
         | 
| 19 | 
            +
            from ...constants import TicketCommands
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
            def manage_tickets(args):
         | 
| 23 | 
            +
                """
         | 
| 24 | 
            +
                Main ticket command dispatcher.
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                WHY: This function routes ticket subcommands to their appropriate handlers,
         | 
| 27 | 
            +
                providing a single entry point for all ticket-related operations.
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                DESIGN DECISION: We use a subcommand pattern similar to git, allowing for
         | 
| 30 | 
            +
                intuitive command structure like 'claude-mpm tickets create "title"'.
         | 
| 31 | 
            +
                
         | 
| 32 | 
            +
                Args:
         | 
| 33 | 
            +
                    args: Parsed command line arguments with 'tickets_command' attribute
         | 
| 34 | 
            +
                    
         | 
| 35 | 
            +
                Returns:
         | 
| 36 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 37 | 
            +
                """
         | 
| 38 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                # Handle case where no subcommand is provided - default to list
         | 
| 41 | 
            +
                if not hasattr(args, 'tickets_command') or not args.tickets_command:
         | 
| 42 | 
            +
                    # Default to list command for backward compatibility
         | 
| 43 | 
            +
                    args.tickets_command = TicketCommands.LIST.value
         | 
| 44 | 
            +
                    # Set default limit if not present
         | 
| 45 | 
            +
                    if not hasattr(args, 'limit'):
         | 
| 46 | 
            +
                        args.limit = 10
         | 
| 47 | 
            +
                    if not hasattr(args, 'verbose'):
         | 
| 48 | 
            +
                        args.verbose = False
         | 
| 49 | 
            +
                
         | 
| 50 | 
            +
                # Map subcommands to handler functions
         | 
| 51 | 
            +
                handlers = {
         | 
| 52 | 
            +
                    TicketCommands.CREATE.value: create_ticket,
         | 
| 53 | 
            +
                    TicketCommands.LIST.value: list_tickets,
         | 
| 54 | 
            +
                    TicketCommands.VIEW.value: view_ticket,
         | 
| 55 | 
            +
                    TicketCommands.UPDATE.value: update_ticket,
         | 
| 56 | 
            +
                    TicketCommands.CLOSE.value: close_ticket,
         | 
| 57 | 
            +
                    TicketCommands.DELETE.value: delete_ticket,
         | 
| 58 | 
            +
                    TicketCommands.SEARCH.value: search_tickets,
         | 
| 59 | 
            +
                    TicketCommands.COMMENT.value: add_comment,
         | 
| 60 | 
            +
                    TicketCommands.WORKFLOW.value: update_workflow,
         | 
| 61 | 
            +
                }
         | 
| 62 | 
            +
                
         | 
| 63 | 
            +
                # Execute the appropriate handler
         | 
| 64 | 
            +
                handler = handlers.get(args.tickets_command)
         | 
| 65 | 
            +
                if handler:
         | 
| 66 | 
            +
                    try:
         | 
| 67 | 
            +
                        return handler(args)
         | 
| 68 | 
            +
                    except KeyboardInterrupt:
         | 
| 69 | 
            +
                        logger.info("Operation cancelled by user")
         | 
| 70 | 
            +
                        return 1
         | 
| 71 | 
            +
                    except Exception as e:
         | 
| 72 | 
            +
                        logger.error(f"Error executing {args.tickets_command}: {e}")
         | 
| 73 | 
            +
                        if hasattr(args, 'debug') and args.debug:
         | 
| 74 | 
            +
                            import traceback
         | 
| 75 | 
            +
                            traceback.print_exc()
         | 
| 76 | 
            +
                        return 1
         | 
| 77 | 
            +
                else:
         | 
| 78 | 
            +
                    logger.error(f"Unknown ticket command: {args.tickets_command}")
         | 
| 79 | 
            +
                    return 1
         | 
| 80 | 
            +
             | 
| 81 | 
            +
             | 
| 82 | 
            +
            def create_ticket(args):
         | 
| 83 | 
            +
                """
         | 
| 84 | 
            +
                Create a new ticket.
         | 
| 85 | 
            +
                
         | 
| 86 | 
            +
                WHY: Users need to create tickets to track work items, bugs, and features.
         | 
| 87 | 
            +
                This command provides a streamlined interface for ticket creation.
         | 
| 88 | 
            +
                
         | 
| 89 | 
            +
                DESIGN DECISION: We parse description from remaining args to allow natural
         | 
| 90 | 
            +
                command line usage like: tickets create "title" -d This is a description
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                Args:
         | 
| 93 | 
            +
                    args: Arguments with title, type, priority, description, tags, etc.
         | 
| 94 | 
            +
                    
         | 
| 95 | 
            +
                Returns:
         | 
| 96 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 97 | 
            +
                """
         | 
| 98 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 99 | 
            +
                
         | 
| 100 | 
            +
                try:
         | 
| 101 | 
            +
                    from ...services.ticket_manager import TicketManager
         | 
| 102 | 
            +
                except ImportError:
         | 
| 103 | 
            +
                    from claude_mpm.services.ticket_manager import TicketManager
         | 
| 104 | 
            +
                
         | 
| 105 | 
            +
                ticket_manager = TicketManager()
         | 
| 106 | 
            +
                
         | 
| 107 | 
            +
                # Parse description from remaining args or use default
         | 
| 108 | 
            +
                description = " ".join(args.description) if args.description else ""
         | 
| 109 | 
            +
                
         | 
| 110 | 
            +
                # Parse tags
         | 
| 111 | 
            +
                tags = args.tags.split(",") if args.tags else []
         | 
| 112 | 
            +
                
         | 
| 113 | 
            +
                # Create ticket with all provided parameters
         | 
| 114 | 
            +
                ticket_id = ticket_manager.create_ticket(
         | 
| 115 | 
            +
                    title=args.title,
         | 
| 116 | 
            +
                    ticket_type=args.type,
         | 
| 117 | 
            +
                    description=description,
         | 
| 118 | 
            +
                    priority=args.priority,
         | 
| 119 | 
            +
                    tags=tags,
         | 
| 120 | 
            +
                    source="claude-mpm-cli",
         | 
| 121 | 
            +
                    parent_epic=getattr(args, 'parent_epic', None),
         | 
| 122 | 
            +
                    parent_issue=getattr(args, 'parent_issue', None)
         | 
| 123 | 
            +
                )
         | 
| 124 | 
            +
                
         | 
| 125 | 
            +
                if ticket_id:
         | 
| 126 | 
            +
                    print(f"✅ Created ticket: {ticket_id}")
         | 
| 127 | 
            +
                    if args.verbose:
         | 
| 128 | 
            +
                        print(f"   Type: {args.type}")
         | 
| 129 | 
            +
                        print(f"   Priority: {args.priority}")
         | 
| 130 | 
            +
                        if tags:
         | 
| 131 | 
            +
                            print(f"   Tags: {', '.join(tags)}")
         | 
| 132 | 
            +
                        if getattr(args, 'parent_epic', None):
         | 
| 133 | 
            +
                            print(f"   Parent Epic: {args.parent_epic}")
         | 
| 134 | 
            +
                        if getattr(args, 'parent_issue', None):
         | 
| 135 | 
            +
                            print(f"   Parent Issue: {args.parent_issue}")
         | 
| 136 | 
            +
                    return 0
         | 
| 137 | 
            +
                else:
         | 
| 138 | 
            +
                    print("❌ Failed to create ticket")
         | 
| 139 | 
            +
                    return 1
         | 
| 9 140 |  | 
| 10 141 |  | 
| 11 142 | 
             
            def list_tickets(args):
         | 
| 12 143 | 
             
                """
         | 
| 13 | 
            -
                List recent tickets.
         | 
| 144 | 
            +
                List recent tickets with optional filtering.
         | 
| 14 145 |  | 
| 15 146 | 
             
                WHY: Users need to review tickets created during Claude sessions. This command
         | 
| 16 147 | 
             
                provides a quick way to see recent tickets with their status and metadata.
         | 
| 17 148 |  | 
| 18 149 | 
             
                DESIGN DECISION: We show tickets in a compact format with emoji status indicators
         | 
| 19 | 
            -
                for better visual scanning.  | 
| 20 | 
            -
                or fewer tickets as needed.
         | 
| 150 | 
            +
                for better visual scanning. Filters allow focusing on specific ticket types/statuses.
         | 
| 21 151 |  | 
| 22 152 | 
             
                Args:
         | 
| 23 | 
            -
                    args:  | 
| 153 | 
            +
                    args: Arguments with limit, type filter, status filter, verbose flag
         | 
| 154 | 
            +
                    
         | 
| 155 | 
            +
                Returns:
         | 
| 156 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 24 157 | 
             
                """
         | 
| 25 | 
            -
                logger = get_logger("cli")
         | 
| 158 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 26 159 |  | 
| 27 160 | 
             
                try:
         | 
| 28 161 | 
             
                    try:
         | 
| @@ -31,33 +164,477 @@ def list_tickets(args): | |
| 31 164 | 
             
                        from claude_mpm.services.ticket_manager import TicketManager
         | 
| 32 165 |  | 
| 33 166 | 
             
                    ticket_manager = TicketManager()
         | 
| 34 | 
            -
                    tickets = ticket_manager.list_recent_tickets(limit=args.limit)
         | 
| 35 167 |  | 
| 36 | 
            -
                     | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 168 | 
            +
                    # Get tickets with limit
         | 
| 169 | 
            +
                    limit = getattr(args, 'limit', 10)
         | 
| 170 | 
            +
                    tickets = ticket_manager.list_recent_tickets(limit=limit * 2)  # Get extra for filtering
         | 
| 39 171 |  | 
| 40 | 
            -
                     | 
| 172 | 
            +
                    # Apply filters if specified
         | 
| 173 | 
            +
                    filtered_tickets = []
         | 
| 174 | 
            +
                    for ticket in tickets:
         | 
| 175 | 
            +
                        # Type filter
         | 
| 176 | 
            +
                        type_filter = getattr(args, 'type', 'all')
         | 
| 177 | 
            +
                        if type_filter != 'all':
         | 
| 178 | 
            +
                            ticket_type = ticket.get('metadata', {}).get('ticket_type', 'unknown')
         | 
| 179 | 
            +
                            if ticket_type != type_filter:
         | 
| 180 | 
            +
                                continue
         | 
| 181 | 
            +
                        
         | 
| 182 | 
            +
                        # Status filter
         | 
| 183 | 
            +
                        status_filter = getattr(args, 'status', 'all')
         | 
| 184 | 
            +
                        if status_filter != 'all':
         | 
| 185 | 
            +
                            if ticket.get('status') != status_filter:
         | 
| 186 | 
            +
                                continue
         | 
| 187 | 
            +
                        
         | 
| 188 | 
            +
                        filtered_tickets.append(ticket)
         | 
| 189 | 
            +
                        if len(filtered_tickets) >= limit:
         | 
| 190 | 
            +
                            break
         | 
| 191 | 
            +
                    
         | 
| 192 | 
            +
                    if not filtered_tickets:
         | 
| 193 | 
            +
                        print("No tickets found matching criteria")
         | 
| 194 | 
            +
                        return 0
         | 
| 195 | 
            +
                    
         | 
| 196 | 
            +
                    print(f"Recent tickets (showing {len(filtered_tickets)}):")
         | 
| 41 197 | 
             
                    print("-" * 80)
         | 
| 42 198 |  | 
| 43 | 
            -
                    for ticket in  | 
| 199 | 
            +
                    for ticket in filtered_tickets:
         | 
| 44 200 | 
             
                        # Use emoji to indicate status visually
         | 
| 45 201 | 
             
                        status_emoji = {
         | 
| 46 202 | 
             
                            "open": "🔵",
         | 
| 47 203 | 
             
                            "in_progress": "🟡",
         | 
| 48 204 | 
             
                            "done": "🟢",
         | 
| 49 | 
            -
                            "closed": "⚫"
         | 
| 50 | 
            -
             | 
| 205 | 
            +
                            "closed": "⚫",
         | 
| 206 | 
            +
                            "blocked": "🔴"
         | 
| 207 | 
            +
                        }.get(ticket.get('status', 'unknown'), "⚪")
         | 
| 51 208 |  | 
| 52 209 | 
             
                        print(f"{status_emoji} [{ticket['id']}] {ticket['title']}")
         | 
| 53 | 
            -
                         | 
| 54 | 
            -
                         | 
| 55 | 
            -
             | 
| 210 | 
            +
                        
         | 
| 211 | 
            +
                        if getattr(args, 'verbose', False):
         | 
| 212 | 
            +
                            ticket_type = ticket.get('metadata', {}).get('ticket_type', 'task')
         | 
| 213 | 
            +
                            print(f"   Type: {ticket_type} | Status: {ticket['status']} | Priority: {ticket['priority']}")
         | 
| 214 | 
            +
                            if ticket.get('tags'):
         | 
| 215 | 
            +
                                print(f"   Tags: {', '.join(ticket['tags'])}")
         | 
| 216 | 
            +
                            print(f"   Created: {ticket['created_at']}")
         | 
| 217 | 
            +
                            print()
         | 
| 218 | 
            +
                    
         | 
| 219 | 
            +
                    return 0
         | 
| 56 220 |  | 
| 57 221 | 
             
                except ImportError:
         | 
| 58 222 | 
             
                    logger.error("ai-trackdown-pytools not installed")
         | 
| 59 223 | 
             
                    print("Error: ai-trackdown-pytools not installed")
         | 
| 60 224 | 
             
                    print("Install with: pip install ai-trackdown-pytools")
         | 
| 225 | 
            +
                    return 1
         | 
| 61 226 | 
             
                except Exception as e:
         | 
| 62 227 | 
             
                    logger.error(f"Error listing tickets: {e}")
         | 
| 63 | 
            -
                    print(f"Error: {e}")
         | 
| 228 | 
            +
                    print(f"Error: {e}")
         | 
| 229 | 
            +
                    return 1
         | 
| 230 | 
            +
             | 
| 231 | 
            +
             | 
| 232 | 
            +
            def view_ticket(args):
         | 
| 233 | 
            +
                """
         | 
| 234 | 
            +
                View a specific ticket in detail.
         | 
| 235 | 
            +
                
         | 
| 236 | 
            +
                WHY: Users need to see full ticket details including description, metadata,
         | 
| 237 | 
            +
                and all associated information for understanding context and status.
         | 
| 238 | 
            +
                
         | 
| 239 | 
            +
                Args:
         | 
| 240 | 
            +
                    args: Arguments with ticket id and verbose flag
         | 
| 241 | 
            +
                    
         | 
| 242 | 
            +
                Returns:
         | 
| 243 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 244 | 
            +
                """
         | 
| 245 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 246 | 
            +
                
         | 
| 247 | 
            +
                try:
         | 
| 248 | 
            +
                    from ...services.ticket_manager import TicketManager
         | 
| 249 | 
            +
                except ImportError:
         | 
| 250 | 
            +
                    from claude_mpm.services.ticket_manager import TicketManager
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                ticket_manager = TicketManager()
         | 
| 253 | 
            +
                ticket = ticket_manager.get_ticket(args.id)
         | 
| 254 | 
            +
                
         | 
| 255 | 
            +
                if not ticket:
         | 
| 256 | 
            +
                    print(f"❌ Ticket {args.id} not found")
         | 
| 257 | 
            +
                    return 1
         | 
| 258 | 
            +
                
         | 
| 259 | 
            +
                print(f"Ticket: {ticket['id']}")
         | 
| 260 | 
            +
                print("=" * 80)
         | 
| 261 | 
            +
                print(f"Title: {ticket['title']}")
         | 
| 262 | 
            +
                print(f"Type: {ticket.get('metadata', {}).get('ticket_type', 'unknown')}")
         | 
| 263 | 
            +
                print(f"Status: {ticket['status']}")
         | 
| 264 | 
            +
                print(f"Priority: {ticket['priority']}")
         | 
| 265 | 
            +
                
         | 
| 266 | 
            +
                if ticket.get('tags'):
         | 
| 267 | 
            +
                    print(f"Tags: {', '.join(ticket['tags'])}")
         | 
| 268 | 
            +
                
         | 
| 269 | 
            +
                if ticket.get('assignees'):
         | 
| 270 | 
            +
                    print(f"Assignees: {', '.join(ticket['assignees'])}")
         | 
| 271 | 
            +
                
         | 
| 272 | 
            +
                # Show parent references if they exist
         | 
| 273 | 
            +
                metadata = ticket.get('metadata', {})
         | 
| 274 | 
            +
                if metadata.get('parent_epic'):
         | 
| 275 | 
            +
                    print(f"Parent Epic: {metadata['parent_epic']}")
         | 
| 276 | 
            +
                if metadata.get('parent_issue'):
         | 
| 277 | 
            +
                    print(f"Parent Issue: {metadata['parent_issue']}")
         | 
| 278 | 
            +
                
         | 
| 279 | 
            +
                print(f"\nDescription:")
         | 
| 280 | 
            +
                print("-" * 40)
         | 
| 281 | 
            +
                print(ticket.get('description', 'No description'))
         | 
| 282 | 
            +
                
         | 
| 283 | 
            +
                print(f"\nCreated: {ticket['created_at']}")
         | 
| 284 | 
            +
                print(f"Updated: {ticket['updated_at']}")
         | 
| 285 | 
            +
                
         | 
| 286 | 
            +
                if args.verbose and ticket.get('metadata'):
         | 
| 287 | 
            +
                    print(f"\nMetadata:")
         | 
| 288 | 
            +
                    print("-" * 40)
         | 
| 289 | 
            +
                    for key, value in ticket['metadata'].items():
         | 
| 290 | 
            +
                        if key not in ['parent_epic', 'parent_issue', 'ticket_type']:  # Already shown above
         | 
| 291 | 
            +
                            print(f"  {key}: {value}")
         | 
| 292 | 
            +
                
         | 
| 293 | 
            +
                return 0
         | 
| 294 | 
            +
             | 
| 295 | 
            +
             | 
| 296 | 
            +
            def update_ticket(args):
         | 
| 297 | 
            +
                """
         | 
| 298 | 
            +
                Update a ticket's properties.
         | 
| 299 | 
            +
                
         | 
| 300 | 
            +
                WHY: Tickets need to be updated as work progresses, priorities change,
         | 
| 301 | 
            +
                or additional information becomes available.
         | 
| 302 | 
            +
                
         | 
| 303 | 
            +
                DESIGN DECISION: For complex updates, we delegate to aitrackdown CLI
         | 
| 304 | 
            +
                for operations not directly supported by our TicketManager interface.
         | 
| 305 | 
            +
                
         | 
| 306 | 
            +
                Args:
         | 
| 307 | 
            +
                    args: Arguments with ticket id and update fields
         | 
| 308 | 
            +
                    
         | 
| 309 | 
            +
                Returns:
         | 
| 310 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 311 | 
            +
                """
         | 
| 312 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 313 | 
            +
                
         | 
| 314 | 
            +
                try:
         | 
| 315 | 
            +
                    from ...services.ticket_manager import TicketManager
         | 
| 316 | 
            +
                except ImportError:
         | 
| 317 | 
            +
                    from claude_mpm.services.ticket_manager import TicketManager
         | 
| 318 | 
            +
                
         | 
| 319 | 
            +
                ticket_manager = TicketManager()
         | 
| 320 | 
            +
                
         | 
| 321 | 
            +
                # Build update dictionary
         | 
| 322 | 
            +
                updates = {}
         | 
| 323 | 
            +
                
         | 
| 324 | 
            +
                if args.status:
         | 
| 325 | 
            +
                    updates['status'] = args.status
         | 
| 326 | 
            +
                
         | 
| 327 | 
            +
                if args.priority:
         | 
| 328 | 
            +
                    updates['priority'] = args.priority
         | 
| 329 | 
            +
                
         | 
| 330 | 
            +
                if args.description:
         | 
| 331 | 
            +
                    updates['description'] = " ".join(args.description)
         | 
| 332 | 
            +
                
         | 
| 333 | 
            +
                if args.tags:
         | 
| 334 | 
            +
                    updates['tags'] = args.tags.split(",")
         | 
| 335 | 
            +
                
         | 
| 336 | 
            +
                if args.assign:
         | 
| 337 | 
            +
                    updates['assignees'] = [args.assign]
         | 
| 338 | 
            +
                
         | 
| 339 | 
            +
                if not updates:
         | 
| 340 | 
            +
                    print("❌ No updates specified")
         | 
| 341 | 
            +
                    return 1
         | 
| 342 | 
            +
                
         | 
| 343 | 
            +
                # Try to update using TicketManager
         | 
| 344 | 
            +
                success = ticket_manager.update_task(args.id, **updates)
         | 
| 345 | 
            +
                
         | 
| 346 | 
            +
                if success:
         | 
| 347 | 
            +
                    print(f"✅ Updated ticket: {args.id}")
         | 
| 348 | 
            +
                    return 0
         | 
| 349 | 
            +
                else:
         | 
| 350 | 
            +
                    # Fallback to aitrackdown CLI for status transitions
         | 
| 351 | 
            +
                    if args.status:
         | 
| 352 | 
            +
                        logger.info("Attempting update via aitrackdown CLI")
         | 
| 353 | 
            +
                        cmd = ["aitrackdown", "transition", args.id, args.status]
         | 
| 354 | 
            +
                        
         | 
| 355 | 
            +
                        # Add comment with other updates
         | 
| 356 | 
            +
                        comment_parts = []
         | 
| 357 | 
            +
                        if args.priority:
         | 
| 358 | 
            +
                            comment_parts.append(f"Priority: {args.priority}")
         | 
| 359 | 
            +
                        if args.assign:
         | 
| 360 | 
            +
                            comment_parts.append(f"Assigned to: {args.assign}")
         | 
| 361 | 
            +
                        if args.tags:
         | 
| 362 | 
            +
                            comment_parts.append(f"Tags: {args.tags}")
         | 
| 363 | 
            +
                        
         | 
| 364 | 
            +
                        if comment_parts:
         | 
| 365 | 
            +
                            comment = " | ".join(comment_parts)
         | 
| 366 | 
            +
                            cmd.extend(["--comment", comment])
         | 
| 367 | 
            +
                        
         | 
| 368 | 
            +
                        try:
         | 
| 369 | 
            +
                            subprocess.run(cmd, check=True, capture_output=True, text=True)
         | 
| 370 | 
            +
                            print(f"✅ Updated ticket: {args.id}")
         | 
| 371 | 
            +
                            return 0
         | 
| 372 | 
            +
                        except subprocess.CalledProcessError as e:
         | 
| 373 | 
            +
                            logger.error(f"Failed to update via CLI: {e}")
         | 
| 374 | 
            +
                            print(f"❌ Failed to update ticket: {args.id}")
         | 
| 375 | 
            +
                            return 1
         | 
| 376 | 
            +
                    else:
         | 
| 377 | 
            +
                        print(f"❌ Failed to update ticket: {args.id}")
         | 
| 378 | 
            +
                        return 1
         | 
| 379 | 
            +
             | 
| 380 | 
            +
             | 
| 381 | 
            +
            def close_ticket(args):
         | 
| 382 | 
            +
                """
         | 
| 383 | 
            +
                Close a ticket.
         | 
| 384 | 
            +
                
         | 
| 385 | 
            +
                WHY: Tickets need to be closed when work is completed or no longer relevant.
         | 
| 386 | 
            +
                
         | 
| 387 | 
            +
                Args:
         | 
| 388 | 
            +
                    args: Arguments with ticket id and optional resolution
         | 
| 389 | 
            +
                    
         | 
| 390 | 
            +
                Returns:
         | 
| 391 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 392 | 
            +
                """
         | 
| 393 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 394 | 
            +
                
         | 
| 395 | 
            +
                try:
         | 
| 396 | 
            +
                    from ...services.ticket_manager import TicketManager
         | 
| 397 | 
            +
                except ImportError:
         | 
| 398 | 
            +
                    from claude_mpm.services.ticket_manager import TicketManager
         | 
| 399 | 
            +
                
         | 
| 400 | 
            +
                ticket_manager = TicketManager()
         | 
| 401 | 
            +
                
         | 
| 402 | 
            +
                # Try to close using TicketManager
         | 
| 403 | 
            +
                resolution = getattr(args, 'resolution', None)
         | 
| 404 | 
            +
                success = ticket_manager.close_task(args.id, resolution=resolution)
         | 
| 405 | 
            +
                
         | 
| 406 | 
            +
                if success:
         | 
| 407 | 
            +
                    print(f"✅ Closed ticket: {args.id}")
         | 
| 408 | 
            +
                    return 0
         | 
| 409 | 
            +
                else:
         | 
| 410 | 
            +
                    # Fallback to aitrackdown CLI
         | 
| 411 | 
            +
                    logger.info("Attempting close via aitrackdown CLI")
         | 
| 412 | 
            +
                    cmd = ["aitrackdown", "close", args.id]
         | 
| 413 | 
            +
                    
         | 
| 414 | 
            +
                    if resolution:
         | 
| 415 | 
            +
                        cmd.extend(["--comment", resolution])
         | 
| 416 | 
            +
                    
         | 
| 417 | 
            +
                    try:
         | 
| 418 | 
            +
                        subprocess.run(cmd, check=True, capture_output=True, text=True)
         | 
| 419 | 
            +
                        print(f"✅ Closed ticket: {args.id}")
         | 
| 420 | 
            +
                        return 0
         | 
| 421 | 
            +
                    except subprocess.CalledProcessError:
         | 
| 422 | 
            +
                        print(f"❌ Failed to close ticket: {args.id}")
         | 
| 423 | 
            +
                        return 1
         | 
| 424 | 
            +
             | 
| 425 | 
            +
             | 
| 426 | 
            +
            def delete_ticket(args):
         | 
| 427 | 
            +
                """
         | 
| 428 | 
            +
                Delete a ticket.
         | 
| 429 | 
            +
                
         | 
| 430 | 
            +
                WHY: Sometimes tickets are created in error or are no longer needed
         | 
| 431 | 
            +
                and should be removed from the system.
         | 
| 432 | 
            +
                
         | 
| 433 | 
            +
                DESIGN DECISION: We delegate to aitrackdown CLI as deletion is a
         | 
| 434 | 
            +
                destructive operation that should use the official tool.
         | 
| 435 | 
            +
                
         | 
| 436 | 
            +
                Args:
         | 
| 437 | 
            +
                    args: Arguments with ticket id and force flag
         | 
| 438 | 
            +
                    
         | 
| 439 | 
            +
                Returns:
         | 
| 440 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 441 | 
            +
                """
         | 
| 442 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 443 | 
            +
                
         | 
| 444 | 
            +
                # Confirm deletion unless forced
         | 
| 445 | 
            +
                if not args.force:
         | 
| 446 | 
            +
                    response = input(f"Are you sure you want to delete ticket {args.id}? (y/N): ")
         | 
| 447 | 
            +
                    if response.lower() != 'y':
         | 
| 448 | 
            +
                        print("Deletion cancelled")
         | 
| 449 | 
            +
                        return 0
         | 
| 450 | 
            +
                
         | 
| 451 | 
            +
                # Use aitrackdown CLI for deletion
         | 
| 452 | 
            +
                cmd = ["aitrackdown", "delete", args.id]
         | 
| 453 | 
            +
                if args.force:
         | 
| 454 | 
            +
                    cmd.append("--force")
         | 
| 455 | 
            +
                
         | 
| 456 | 
            +
                try:
         | 
| 457 | 
            +
                    subprocess.run(cmd, check=True, capture_output=True, text=True)
         | 
| 458 | 
            +
                    print(f"✅ Deleted ticket: {args.id}")
         | 
| 459 | 
            +
                    return 0
         | 
| 460 | 
            +
                except subprocess.CalledProcessError:
         | 
| 461 | 
            +
                    print(f"❌ Failed to delete ticket: {args.id}")
         | 
| 462 | 
            +
                    return 1
         | 
| 463 | 
            +
             | 
| 464 | 
            +
             | 
| 465 | 
            +
            def search_tickets(args):
         | 
| 466 | 
            +
                """
         | 
| 467 | 
            +
                Search tickets by query string.
         | 
| 468 | 
            +
                
         | 
| 469 | 
            +
                WHY: Users need to find specific tickets based on content, tags, or other criteria.
         | 
| 470 | 
            +
                
         | 
| 471 | 
            +
                DESIGN DECISION: We perform simple text matching on ticket data. For more advanced
         | 
| 472 | 
            +
                search, users should use the aitrackdown CLI directly.
         | 
| 473 | 
            +
                
         | 
| 474 | 
            +
                Args:
         | 
| 475 | 
            +
                    args: Arguments with search query and filters
         | 
| 476 | 
            +
                    
         | 
| 477 | 
            +
                Returns:
         | 
| 478 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 479 | 
            +
                """
         | 
| 480 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 481 | 
            +
                
         | 
| 482 | 
            +
                try:
         | 
| 483 | 
            +
                    from ...services.ticket_manager import TicketManager
         | 
| 484 | 
            +
                except ImportError:
         | 
| 485 | 
            +
                    from claude_mpm.services.ticket_manager import TicketManager
         | 
| 486 | 
            +
                
         | 
| 487 | 
            +
                ticket_manager = TicketManager()
         | 
| 488 | 
            +
                
         | 
| 489 | 
            +
                # Get all available tickets for searching
         | 
| 490 | 
            +
                all_tickets = ticket_manager.list_recent_tickets(limit=100)
         | 
| 491 | 
            +
                
         | 
| 492 | 
            +
                # Search tickets
         | 
| 493 | 
            +
                query = args.query.lower()
         | 
| 494 | 
            +
                matched_tickets = []
         | 
| 495 | 
            +
                
         | 
| 496 | 
            +
                for ticket in all_tickets:
         | 
| 497 | 
            +
                    # Check if query matches title, description, or tags
         | 
| 498 | 
            +
                    if (query in ticket.get('title', '').lower() or
         | 
| 499 | 
            +
                        query in ticket.get('description', '').lower() or
         | 
| 500 | 
            +
                        any(query in tag.lower() for tag in ticket.get('tags', []))):
         | 
| 501 | 
            +
                        
         | 
| 502 | 
            +
                        # Apply type filter
         | 
| 503 | 
            +
                        if args.type != 'all':
         | 
| 504 | 
            +
                            ticket_type = ticket.get('metadata', {}).get('ticket_type', 'unknown')
         | 
| 505 | 
            +
                            if ticket_type != args.type:
         | 
| 506 | 
            +
                                continue
         | 
| 507 | 
            +
                        
         | 
| 508 | 
            +
                        # Apply status filter
         | 
| 509 | 
            +
                        if args.status != 'all':
         | 
| 510 | 
            +
                            if ticket.get('status') != args.status:
         | 
| 511 | 
            +
                                continue
         | 
| 512 | 
            +
                        
         | 
| 513 | 
            +
                        matched_tickets.append(ticket)
         | 
| 514 | 
            +
                        if len(matched_tickets) >= args.limit:
         | 
| 515 | 
            +
                            break
         | 
| 516 | 
            +
                
         | 
| 517 | 
            +
                if not matched_tickets:
         | 
| 518 | 
            +
                    print(f"No tickets found matching '{args.query}'")
         | 
| 519 | 
            +
                    return 0
         | 
| 520 | 
            +
                
         | 
| 521 | 
            +
                print(f"Search results for '{args.query}' (showing {len(matched_tickets)}):")
         | 
| 522 | 
            +
                print("-" * 80)
         | 
| 523 | 
            +
                
         | 
| 524 | 
            +
                for ticket in matched_tickets:
         | 
| 525 | 
            +
                    status_emoji = {
         | 
| 526 | 
            +
                        "open": "🔵",
         | 
| 527 | 
            +
                        "in_progress": "🟡",
         | 
| 528 | 
            +
                        "done": "🟢",
         | 
| 529 | 
            +
                        "closed": "⚫",
         | 
| 530 | 
            +
                        "blocked": "🔴"
         | 
| 531 | 
            +
                    }.get(ticket.get('status', 'unknown'), "⚪")
         | 
| 532 | 
            +
                    
         | 
| 533 | 
            +
                    print(f"{status_emoji} [{ticket['id']}] {ticket['title']}")
         | 
| 534 | 
            +
                    
         | 
| 535 | 
            +
                    # Show snippet of description if it contains the query
         | 
| 536 | 
            +
                    desc = ticket.get('description', '')
         | 
| 537 | 
            +
                    if query in desc.lower():
         | 
| 538 | 
            +
                        # Find and show context around the match
         | 
| 539 | 
            +
                        idx = desc.lower().index(query)
         | 
| 540 | 
            +
                        start = max(0, idx - 30)
         | 
| 541 | 
            +
                        end = min(len(desc), idx + len(query) + 30)
         | 
| 542 | 
            +
                        snippet = desc[start:end]
         | 
| 543 | 
            +
                        if start > 0:
         | 
| 544 | 
            +
                            snippet = "..." + snippet
         | 
| 545 | 
            +
                        if end < len(desc):
         | 
| 546 | 
            +
                            snippet = snippet + "..."
         | 
| 547 | 
            +
                        print(f"   {snippet}")
         | 
| 548 | 
            +
                
         | 
| 549 | 
            +
                return 0
         | 
| 550 | 
            +
             | 
| 551 | 
            +
             | 
| 552 | 
            +
            def add_comment(args):
         | 
| 553 | 
            +
                """
         | 
| 554 | 
            +
                Add a comment to a ticket.
         | 
| 555 | 
            +
                
         | 
| 556 | 
            +
                WHY: Comments allow tracking progress, decisions, and additional context
         | 
| 557 | 
            +
                on tickets over time.
         | 
| 558 | 
            +
                
         | 
| 559 | 
            +
                DESIGN DECISION: We delegate to aitrackdown CLI as it has proper comment
         | 
| 560 | 
            +
                tracking infrastructure.
         | 
| 561 | 
            +
                
         | 
| 562 | 
            +
                Args:
         | 
| 563 | 
            +
                    args: Arguments with ticket id and comment text
         | 
| 564 | 
            +
                    
         | 
| 565 | 
            +
                Returns:
         | 
| 566 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 567 | 
            +
                """
         | 
| 568 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 569 | 
            +
                
         | 
| 570 | 
            +
                # Join comment parts into single string
         | 
| 571 | 
            +
                comment = " ".join(args.comment)
         | 
| 572 | 
            +
                
         | 
| 573 | 
            +
                # Use aitrackdown CLI for comments
         | 
| 574 | 
            +
                cmd = ["aitrackdown", "comment", args.id, comment]
         | 
| 575 | 
            +
                
         | 
| 576 | 
            +
                try:
         | 
| 577 | 
            +
                    subprocess.run(cmd, check=True, capture_output=True, text=True)
         | 
| 578 | 
            +
                    print(f"✅ Added comment to ticket: {args.id}")
         | 
| 579 | 
            +
                    return 0
         | 
| 580 | 
            +
                except subprocess.CalledProcessError:
         | 
| 581 | 
            +
                    print(f"❌ Failed to add comment to ticket: {args.id}")
         | 
| 582 | 
            +
                    return 1
         | 
| 583 | 
            +
             | 
| 584 | 
            +
             | 
| 585 | 
            +
            def update_workflow(args):
         | 
| 586 | 
            +
                """
         | 
| 587 | 
            +
                Update ticket workflow state.
         | 
| 588 | 
            +
                
         | 
| 589 | 
            +
                WHY: Workflow states track the progress of tickets through defined stages
         | 
| 590 | 
            +
                like todo, in_progress, ready, tested, done.
         | 
| 591 | 
            +
                
         | 
| 592 | 
            +
                DESIGN DECISION: We use aitrackdown's transition command for workflow updates
         | 
| 593 | 
            +
                as it maintains proper state machine transitions.
         | 
| 594 | 
            +
                
         | 
| 595 | 
            +
                Args:
         | 
| 596 | 
            +
                    args: Arguments with ticket id, new state, and optional comment
         | 
| 597 | 
            +
                    
         | 
| 598 | 
            +
                Returns:
         | 
| 599 | 
            +
                    Exit code (0 for success, non-zero for errors)
         | 
| 600 | 
            +
                """
         | 
| 601 | 
            +
                logger = get_logger("cli.tickets")
         | 
| 602 | 
            +
                
         | 
| 603 | 
            +
                # Map workflow states to status if needed
         | 
| 604 | 
            +
                state_mapping = {
         | 
| 605 | 
            +
                    'todo': 'open',
         | 
| 606 | 
            +
                    'in_progress': 'in_progress',
         | 
| 607 | 
            +
                    'ready': 'ready',
         | 
| 608 | 
            +
                    'tested': 'tested',
         | 
| 609 | 
            +
                    'done': 'done',
         | 
| 610 | 
            +
                    'blocked': 'blocked'
         | 
| 611 | 
            +
                }
         | 
| 612 | 
            +
                
         | 
| 613 | 
            +
                # Use aitrackdown transition command
         | 
| 614 | 
            +
                cmd = ["aitrackdown", "transition", args.id, args.state]
         | 
| 615 | 
            +
                
         | 
| 616 | 
            +
                if getattr(args, 'comment', None):
         | 
| 617 | 
            +
                    cmd.extend(["--comment", args.comment])
         | 
| 618 | 
            +
                
         | 
| 619 | 
            +
                try:
         | 
| 620 | 
            +
                    subprocess.run(cmd, check=True, capture_output=True, text=True)
         | 
| 621 | 
            +
                    print(f"✅ Updated workflow state for {args.id} to: {args.state}")
         | 
| 622 | 
            +
                    return 0
         | 
| 623 | 
            +
                except subprocess.CalledProcessError:
         | 
| 624 | 
            +
                    print(f"❌ Failed to update workflow state for ticket: {args.id}")
         | 
| 625 | 
            +
                    return 1
         | 
| 626 | 
            +
             | 
| 627 | 
            +
             | 
| 628 | 
            +
            # Maintain backward compatibility with the old list_tickets function signature
         | 
| 629 | 
            +
            def list_tickets_legacy(args):
         | 
| 630 | 
            +
                """
         | 
| 631 | 
            +
                Legacy list_tickets function for backward compatibility.
         | 
| 632 | 
            +
                
         | 
| 633 | 
            +
                WHY: The old CLI interface expected a simple list_tickets function.
         | 
| 634 | 
            +
                This wrapper maintains that interface while using the new implementation.
         | 
| 635 | 
            +
                
         | 
| 636 | 
            +
                Args:
         | 
| 637 | 
            +
                    args: Parsed command line arguments with 'limit' attribute
         | 
| 638 | 
            +
                """
         | 
| 639 | 
            +
                # Call the new list_tickets function
         | 
| 640 | 
            +
                return list_tickets(args)
         |