claude-mpm 3.7.4__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.
Files changed (117) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -78
  4. claude_mpm/agents/MEMORY.md +88 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/schema/agent_schema.json +1 -1
  7. claude_mpm/agents/templates/code_analyzer.json +26 -11
  8. claude_mpm/agents/templates/data_engineer.json +4 -7
  9. claude_mpm/agents/templates/documentation.json +2 -2
  10. claude_mpm/agents/templates/engineer.json +2 -2
  11. claude_mpm/agents/templates/ops.json +3 -8
  12. claude_mpm/agents/templates/qa.json +2 -3
  13. claude_mpm/agents/templates/research.json +2 -3
  14. claude_mpm/agents/templates/security.json +3 -6
  15. claude_mpm/agents/templates/ticketing.json +4 -9
  16. claude_mpm/agents/templates/version_control.json +3 -3
  17. claude_mpm/agents/templates/web_qa.json +4 -4
  18. claude_mpm/agents/templates/web_ui.json +4 -4
  19. claude_mpm/cli/__init__.py +2 -2
  20. claude_mpm/cli/commands/__init__.py +2 -1
  21. claude_mpm/cli/commands/agents.py +118 -1
  22. claude_mpm/cli/commands/tickets.py +596 -19
  23. claude_mpm/cli/parser.py +228 -5
  24. claude_mpm/config/__init__.py +30 -39
  25. claude_mpm/config/socketio_config.py +8 -5
  26. claude_mpm/constants.py +13 -0
  27. claude_mpm/core/__init__.py +8 -18
  28. claude_mpm/core/cache.py +596 -0
  29. claude_mpm/core/claude_runner.py +166 -622
  30. claude_mpm/core/config.py +5 -1
  31. claude_mpm/core/constants.py +339 -0
  32. claude_mpm/core/container.py +461 -22
  33. claude_mpm/core/exceptions.py +392 -0
  34. claude_mpm/core/framework_loader.py +208 -93
  35. claude_mpm/core/interactive_session.py +432 -0
  36. claude_mpm/core/interfaces.py +424 -0
  37. claude_mpm/core/lazy.py +467 -0
  38. claude_mpm/core/logging_config.py +444 -0
  39. claude_mpm/core/oneshot_session.py +465 -0
  40. claude_mpm/core/optimized_agent_loader.py +485 -0
  41. claude_mpm/core/optimized_startup.py +490 -0
  42. claude_mpm/core/service_registry.py +52 -26
  43. claude_mpm/core/socketio_pool.py +162 -5
  44. claude_mpm/core/types.py +292 -0
  45. claude_mpm/core/typing_utils.py +477 -0
  46. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +46 -2
  47. claude_mpm/dashboard/templates/index.html +5 -5
  48. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  49. claude_mpm/init.py +2 -1
  50. claude_mpm/services/__init__.py +78 -14
  51. claude_mpm/services/agent/__init__.py +24 -0
  52. claude_mpm/services/agent/deployment.py +2548 -0
  53. claude_mpm/services/agent/management.py +598 -0
  54. claude_mpm/services/agent/registry.py +813 -0
  55. claude_mpm/services/agents/deployment/agent_deployment.py +592 -269
  56. claude_mpm/services/agents/deployment/async_agent_deployment.py +5 -1
  57. claude_mpm/services/agents/management/agent_capabilities_generator.py +21 -11
  58. claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
  59. claude_mpm/services/async_session_logger.py +8 -3
  60. claude_mpm/services/communication/__init__.py +21 -0
  61. claude_mpm/services/communication/socketio.py +1933 -0
  62. claude_mpm/services/communication/websocket.py +479 -0
  63. claude_mpm/services/core/__init__.py +123 -0
  64. claude_mpm/services/core/base.py +247 -0
  65. claude_mpm/services/core/interfaces.py +951 -0
  66. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  67. claude_mpm/services/framework_claude_md_generator.py +3 -2
  68. claude_mpm/services/health_monitor.py +4 -3
  69. claude_mpm/services/hook_service.py +64 -4
  70. claude_mpm/services/infrastructure/__init__.py +21 -0
  71. claude_mpm/services/infrastructure/logging.py +202 -0
  72. claude_mpm/services/infrastructure/monitoring.py +893 -0
  73. claude_mpm/services/memory/indexed_memory.py +648 -0
  74. claude_mpm/services/project/__init__.py +21 -0
  75. claude_mpm/services/project/analyzer.py +864 -0
  76. claude_mpm/services/project/registry.py +608 -0
  77. claude_mpm/services/project_analyzer.py +95 -2
  78. claude_mpm/services/recovery_manager.py +15 -9
  79. claude_mpm/services/socketio/__init__.py +25 -0
  80. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  81. claude_mpm/services/socketio/handlers/base.py +121 -0
  82. claude_mpm/services/socketio/handlers/connection.py +198 -0
  83. claude_mpm/services/socketio/handlers/file.py +213 -0
  84. claude_mpm/services/socketio/handlers/git.py +723 -0
  85. claude_mpm/services/socketio/handlers/memory.py +27 -0
  86. claude_mpm/services/socketio/handlers/project.py +25 -0
  87. claude_mpm/services/socketio/handlers/registry.py +145 -0
  88. claude_mpm/services/socketio_client_manager.py +12 -7
  89. claude_mpm/services/socketio_server.py +156 -30
  90. claude_mpm/services/ticket_manager.py +377 -51
  91. claude_mpm/utils/agent_dependency_loader.py +66 -15
  92. claude_mpm/utils/error_handler.py +1 -1
  93. claude_mpm/utils/robust_installer.py +587 -0
  94. claude_mpm/validation/agent_validator.py +27 -14
  95. claude_mpm/validation/frontmatter_validator.py +231 -0
  96. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +74 -41
  97. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +101 -76
  98. claude_mpm/.claude-mpm/logs/hooks_20250728.log +0 -10
  99. claude_mpm/agents/agent-template.yaml +0 -83
  100. claude_mpm/cli/README.md +0 -108
  101. claude_mpm/cli_module/refactoring_guide.md +0 -253
  102. claude_mpm/config/async_logging_config.yaml +0 -145
  103. claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +0 -34
  104. claude_mpm/dashboard/.claude-mpm/memories/README.md +0 -36
  105. claude_mpm/dashboard/README.md +0 -121
  106. claude_mpm/dashboard/static/js/dashboard.js.backup +0 -1973
  107. claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +0 -36
  108. claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +0 -39
  109. claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +0 -38
  110. claude_mpm/hooks/README.md +0 -96
  111. claude_mpm/schemas/agent_schema.json +0 -435
  112. claude_mpm/services/framework_claude_md_generator/README.md +0 -92
  113. claude_mpm/services/version_control/VERSION +0 -1
  114. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
  115. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
  116. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
  117. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
@@ -7,22 +7,26 @@ import sys
7
7
  import time
8
8
  from datetime import datetime
9
9
  from pathlib import Path
10
- from typing import Optional
10
+ from typing import Optional, TYPE_CHECKING
11
11
  import uuid
12
12
  from claude_mpm.config.paths import paths
13
13
 
14
- try:
15
- from claude_mpm.services.agents.deployment import AgentDeploymentService
16
- from claude_mpm.services.ticket_manager import TicketManager
17
- from claude_mpm.services.hook_service import HookService
18
- from claude_mpm.core.config import Config
19
- from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
20
- except ImportError:
14
+ # Core imports that don't cause circular dependencies
15
+ from claude_mpm.core.config import Config
16
+ from claude_mpm.core.logging_config import get_logger, log_operation, log_performance_context
17
+ from claude_mpm.core.logger import get_project_logger, ProjectLogger
18
+ from claude_mpm.core.container import get_container, ServiceLifetime
19
+ from claude_mpm.core.interfaces import (
20
+ AgentDeploymentInterface,
21
+ TicketManagerInterface,
22
+ HookServiceInterface
23
+ )
24
+
25
+ # Type checking imports to avoid circular dependencies
26
+ if TYPE_CHECKING:
21
27
  from claude_mpm.services.agents.deployment import AgentDeploymentService
22
28
  from claude_mpm.services.ticket_manager import TicketManager
23
29
  from claude_mpm.services.hook_service import HookService
24
- from claude_mpm.core.config import Config
25
- from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
26
30
 
27
31
 
28
32
  class ClaudeRunner:
@@ -52,7 +56,7 @@ class ClaudeRunner:
52
56
  """Initialize the Claude runner."""
53
57
  self.enable_tickets = enable_tickets
54
58
  self.log_level = log_level
55
- self.logger = get_logger("claude_runner")
59
+ self.logger = get_logger(__name__)
56
60
  self.claude_args = claude_args or []
57
61
  self.launch_method = launch_method
58
62
  self.enable_websocket = enable_websocket
@@ -73,49 +77,56 @@ class ClaudeRunner:
73
77
  except Exception as e:
74
78
  self.logger.warning(f"Failed to initialize project logger: {e}")
75
79
 
76
- # Initialize services with proper error handling
80
+ # Initialize services using dependency injection
77
81
  # Determine the user's working directory from environment
78
82
  user_working_dir = None
79
83
  if 'CLAUDE_MPM_USER_PWD' in os.environ:
80
84
  user_working_dir = Path(os.environ['CLAUDE_MPM_USER_PWD'])
81
- self.logger.info(f"Using user working directory from CLAUDE_MPM_USER_PWD: {user_working_dir}")
85
+ self.logger.info(f"Using user working directory from CLAUDE_MPM_USER_PWD", extra={"directory": str(user_working_dir)})
86
+
87
+ # Get DI container and resolve services
88
+ container = get_container()
89
+
90
+ # Register and resolve deployment service
91
+ if not container.is_registered(AgentDeploymentInterface):
92
+ # Lazy import to avoid circular dependencies
93
+ from claude_mpm.services.agents.deployment import AgentDeploymentService
94
+ container.register_factory(
95
+ AgentDeploymentInterface,
96
+ lambda c: AgentDeploymentService(working_directory=user_working_dir),
97
+ lifetime=ServiceLifetime.SINGLETON
98
+ )
82
99
 
83
100
  try:
84
- # Pass the user working directory to the deployment service
85
- # This ensures agents are deployed to the correct user directory, not the framework directory
86
- self.deployment_service = AgentDeploymentService(working_directory=user_working_dir)
87
- except ImportError as e:
88
- self.logger.error(f"Failed to import AgentDeploymentService: {e}")
89
- raise RuntimeError("Required module AgentDeploymentService not available. Please reinstall claude-mpm.") from e
101
+ self.deployment_service = container.get(AgentDeploymentInterface)
90
102
  except Exception as e:
91
- self.logger.error(f"Failed to initialize AgentDeploymentService: {e}")
103
+ self.logger.error(f"Failed to resolve AgentDeploymentService", exc_info=True)
92
104
  raise RuntimeError(f"Agent deployment service initialization failed: {e}") from e
93
105
 
94
- # Initialize ticket manager if enabled
106
+ # Initialize ticket manager if enabled using DI
95
107
  if enable_tickets:
108
+ if not container.is_registered(TicketManagerInterface):
109
+ # Lazy import to avoid circular dependencies
110
+ from claude_mpm.services.ticket_manager import TicketManager
111
+ container.register_singleton(TicketManagerInterface, TicketManager)
112
+
96
113
  try:
97
- self.ticket_manager = TicketManager()
98
- except ImportError as e:
99
- self.logger.warning(f"Ticket manager module not available: {e}")
100
- self.ticket_manager = None
101
- self.enable_tickets = False
102
- except TypeError as e:
103
- self.logger.warning(f"Ticket manager initialization error: {e}")
104
- self.ticket_manager = None
105
- self.enable_tickets = False
114
+ self.ticket_manager = container.get(TicketManagerInterface)
106
115
  except Exception as e:
107
- self.logger.warning(f"Unexpected error initializing ticket manager: {e}")
116
+ self.logger.warning("Failed to initialize TicketManager", exc_info=True)
108
117
  self.ticket_manager = None
109
118
  self.enable_tickets = False
119
+ else:
120
+ self.ticket_manager = None
110
121
 
111
122
  # Initialize configuration
112
123
  try:
113
124
  self.config = Config()
114
125
  except FileNotFoundError as e:
115
- self.logger.warning(f"Configuration file not found, using defaults: {e}")
126
+ self.logger.warning("Configuration file not found, using defaults", extra={"error": str(e)})
116
127
  self.config = Config() # Will use defaults
117
128
  except Exception as e:
118
- self.logger.error(f"Failed to load configuration: {e}")
129
+ self.logger.error("Failed to load configuration", exc_info=True)
119
130
  raise RuntimeError(f"Configuration initialization failed: {e}") from e
120
131
 
121
132
  # Initialize response logging if enabled
@@ -132,17 +143,23 @@ class ClaudeRunner:
132
143
  component="logging"
133
144
  )
134
145
  except Exception as e:
135
- self.logger.warning(f"Failed to initialize response logger: {e}")
146
+ self.logger.warning("Failed to initialize response logger", exc_info=True)
147
+
148
+ # Initialize hook service using DI
149
+ if not container.is_registered(HookServiceInterface):
150
+ # Lazy import to avoid circular dependencies
151
+ from claude_mpm.services.hook_service import HookService
152
+ container.register_factory(
153
+ HookServiceInterface,
154
+ lambda c: HookService(self.config),
155
+ lifetime=ServiceLifetime.SINGLETON
156
+ )
136
157
 
137
- # Initialize hook service
138
158
  try:
139
- self.hook_service = HookService(self.config)
159
+ self.hook_service = container.get(HookServiceInterface)
140
160
  self._register_memory_hooks()
141
- except ImportError as e:
142
- self.logger.warning(f"Hook service module not available: {e}")
143
- self.hook_service = None
144
161
  except Exception as e:
145
- self.logger.warning(f"Failed to initialize hook service: {e}")
162
+ self.logger.warning("Failed to initialize hook service", exc_info=True)
146
163
  self.hook_service = None
147
164
 
148
165
  # Load system instructions
@@ -473,614 +490,96 @@ class ClaudeRunner:
473
490
  def run_interactive(self, initial_context: Optional[str] = None):
474
491
  """Run Claude in interactive mode.
475
492
 
476
- WHY: This method manages the interactive Claude session with optional response
477
- logging through the hook system. Response logging works seamlessly with both
478
- exec and subprocess launch methods via Claude Code's built-in hook infrastructure.
493
+ WHY: This method now delegates to InteractiveSession class for better
494
+ maintainability and reduced complexity. The session class handles all
495
+ the details while this method provides the simple interface.
479
496
 
480
- DESIGN DECISION: The hook system captures Claude events (UserPromptSubmit,
481
- PreToolUse, PostToolUse, Task delegations) directly from Claude Code, enabling
482
- response logging without process control overhead. This architecture provides:
483
- - Better performance (no I/O stream interception needed)
484
- - Full compatibility with exec mode (preserves default behavior)
485
- - Clean separation of concerns (hooks handle logging independently)
486
- - Comprehensive event capture (including agent delegations)
497
+ DESIGN DECISION: Using delegation pattern to reduce complexity from
498
+ 39 to <10 and lines from 262 to <80, while maintaining 100% backward
499
+ compatibility. All functionality including response logging through
500
+ the hook system is preserved.
501
+
502
+ The hook system continues to capture Claude events (UserPromptSubmit,
503
+ PreToolUse, PostToolUse, Task delegations) directly from Claude Code,
504
+ providing comprehensive event capture without process control overhead.
487
505
 
488
506
  Args:
489
507
  initial_context: Optional initial context to pass to Claude
490
508
  """
491
- # Use the launch method as specified
492
- effective_launch_method = self.launch_method
493
-
494
- # Connect to Socket.IO server if enabled
495
- if self.enable_websocket:
496
- try:
497
- # Use Socket.IO client proxy to connect to monitoring server
498
- from claude_mpm.services.socketio_server import SocketIOClientProxy
499
- self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
500
- self.websocket_server.start()
501
- self.logger.info("Connected to Socket.IO monitoring server")
502
-
503
- # Generate session ID
504
- session_id = str(uuid.uuid4())
505
- working_dir = os.getcwd()
506
-
507
- # Notify session start
508
- self.websocket_server.session_started(
509
- session_id=session_id,
510
- launch_method=effective_launch_method,
511
- working_dir=working_dir
512
- )
513
- except ImportError as e:
514
- self.logger.warning(f"Socket.IO module not available: {e}")
515
- self.websocket_server = None
516
- except ConnectionError as e:
517
- self.logger.warning(f"Cannot connect to Socket.IO server on port {self.websocket_port}: {e}")
518
- self.websocket_server = None
519
- except Exception as e:
520
- self.logger.warning(f"Unexpected error with Socket.IO server: {e}")
521
- self.websocket_server = None
522
-
523
- # Get version with robust fallback mechanisms
524
- version_str = self._get_version()
525
-
526
- # Print styled welcome box
527
- print("\033[32m╭───────────────────────────────────────────────────╮\033[0m")
528
- print("\033[32m│\033[0m ✻ Claude MPM - Interactive Session \033[32m│\033[0m")
529
- print(f"\033[32m│\033[0m Version {version_str:<40}\033[32m│\033[0m")
530
- print("\033[32m│ │\033[0m")
531
- print("\033[32m│\033[0m Type '/agents' to see available agents \033[32m│\033[0m")
532
- print("\033[32m╰───────────────────────────────────────────────────╯\033[0m")
533
- print("") # Add blank line after box
534
-
535
- if self.project_logger:
536
- self.project_logger.log_system(
537
- "Starting interactive session",
538
- level="INFO",
539
- component="session"
540
- )
509
+ from claude_mpm.core.interactive_session import InteractiveSession
541
510
 
542
- # Setup agents - first deploy system agents, then project agents
543
- if not self.setup_agents():
544
- print("Continuing without native agents...")
545
-
546
- # Deploy project-specific agents if they exist
547
- self.deploy_project_agents_to_claude()
548
-
549
- # Build command with system instructions
550
- cmd = [
551
- "claude",
552
- "--model", "opus",
553
- "--dangerously-skip-permissions"
554
- ]
511
+ # Create session handler
512
+ session = InteractiveSession(self)
555
513
 
556
- # Add any custom Claude arguments
557
- if self.claude_args:
558
- cmd.extend(self.claude_args)
559
-
560
- # Add system instructions if available
561
- system_prompt = self._create_system_prompt()
562
- if system_prompt and system_prompt != create_simple_context():
563
- cmd.extend(["--append-system-prompt", system_prompt])
564
-
565
- # Run interactive Claude directly
566
514
  try:
567
- # Use execvp to replace the current process with Claude
568
- # This should avoid any subprocess issues
569
-
570
- # Clean environment
571
- clean_env = os.environ.copy()
572
- claude_vars_to_remove = [
573
- 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDECODE', 'CLAUDE_CONFIG_DIR',
574
- 'CLAUDE_MAX_PARALLEL_SUBAGENTS', 'CLAUDE_TIMEOUT'
575
- ]
576
- for var in claude_vars_to_remove:
577
- clean_env.pop(var, None)
578
-
579
- # Set the correct working directory for Claude Code
580
- # If CLAUDE_MPM_USER_PWD is set, use that as the working directory
581
- if 'CLAUDE_MPM_USER_PWD' in clean_env:
582
- user_pwd = clean_env['CLAUDE_MPM_USER_PWD']
583
- clean_env['CLAUDE_WORKSPACE'] = user_pwd
584
- # Also change to that directory before launching Claude
585
- try:
586
- os.chdir(user_pwd)
587
- self.logger.info(f"Changed working directory to: {user_pwd}")
588
- except PermissionError as e:
589
- self.logger.warning(f"Permission denied accessing directory {user_pwd}: {e}")
590
- except FileNotFoundError as e:
591
- self.logger.warning(f"Directory not found {user_pwd}: {e}")
592
- except OSError as e:
593
- self.logger.warning(f"OS error changing to directory {user_pwd}: {e}")
594
-
595
- print("Launching Claude...")
596
-
597
- if self.project_logger:
598
- self.project_logger.log_system(
599
- f"Launching Claude interactive mode with {effective_launch_method}",
600
- level="INFO",
601
- component="session"
602
- )
603
- self._log_session_event({
604
- "event": "launching_claude_interactive",
605
- "command": " ".join(cmd),
606
- "method": effective_launch_method
607
- })
608
-
609
- # Notify WebSocket clients
610
- if self.websocket_server:
611
- self.websocket_server.claude_status_changed(
612
- status="starting",
613
- message="Launching Claude interactive session"
614
- )
515
+ # Step 1: Initialize session
516
+ success, error = session.initialize_interactive_session()
517
+ if not success:
518
+ self.logger.error(f"Failed to initialize interactive session: {error}")
519
+ return
615
520
 
616
- # Launch using selected method
617
- if effective_launch_method == "subprocess":
618
- self._launch_subprocess_interactive(cmd, clean_env)
619
- else:
620
- # Default to exec for backward compatibility
621
- if self.websocket_server:
622
- # Notify before exec (we won't be able to after)
623
- self.websocket_server.claude_status_changed(
624
- status="running",
625
- message="Claude process started (exec mode)"
626
- )
627
- os.execvpe(cmd[0], cmd, clean_env)
521
+ # Step 2: Set up environment
522
+ success, environment = session.setup_interactive_environment()
523
+ if not success:
524
+ self.logger.error("Failed to setup interactive environment")
525
+ return
628
526
 
629
- except FileNotFoundError as e:
630
- error_msg = f"Claude CLI not found. Please ensure 'claude' is installed and in your PATH: {e}"
631
- print(f"❌ {error_msg}")
632
- if self.project_logger:
633
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
634
- self._log_session_event({
635
- "event": "interactive_launch_failed",
636
- "error": str(e),
637
- "exception_type": "FileNotFoundError",
638
- "recovery_action": "fallback_to_subprocess"
639
- })
640
- except PermissionError as e:
641
- error_msg = f"Permission denied executing Claude CLI: {e}"
642
- print(f"❌ {error_msg}")
643
- if self.project_logger:
644
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
645
- self._log_session_event({
646
- "event": "interactive_launch_failed",
647
- "error": str(e),
648
- "exception_type": "PermissionError",
649
- "recovery_action": "check_file_permissions"
650
- })
651
- except OSError as e:
652
- error_msg = f"OS error launching Claude: {e}"
653
- print(f"❌ {error_msg}")
654
- if self.project_logger:
655
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
656
- self._log_session_event({
657
- "event": "interactive_launch_failed",
658
- "error": str(e),
659
- "exception_type": "OSError",
660
- "recovery_action": "fallback_to_subprocess"
661
- })
662
- except KeyboardInterrupt:
663
- print("\n⚠️ Session interrupted by user")
664
- if self.project_logger:
665
- self.project_logger.log_system(
666
- "Session interrupted by user",
667
- level="INFO",
668
- component="session"
669
- )
670
- self._log_session_event({
671
- "event": "session_interrupted",
672
- "reason": "user_interrupt"
673
- })
674
- return # Clean exit on user interrupt
675
- except Exception as e:
676
- error_msg = f"Unexpected error launching Claude: {e}"
677
- print(f"❌ {error_msg}")
678
- if self.project_logger:
679
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
680
- self._log_session_event({
681
- "event": "interactive_launch_failed",
682
- "error": str(e),
683
- "exception_type": type(e).__name__,
684
- "recovery_action": "fallback_to_subprocess"
685
- })
527
+ # Step 3: Handle interactive input/output
528
+ # This is where the actual Claude process runs
529
+ session.handle_interactive_input(environment)
686
530
 
687
- # Notify WebSocket clients of error
688
- if self.websocket_server:
689
- self.websocket_server.claude_status_changed(
690
- status="error",
691
- message=f"Failed to launch Claude: {e}"
692
- )
693
- # Fallback to subprocess
694
- print("\n🔄 Attempting fallback launch method...")
695
- try:
696
- # Use the same clean_env we prepared earlier
697
- result = subprocess.run(cmd, stdin=None, stdout=None, stderr=None, env=clean_env)
698
- if result.returncode == 0:
699
- if self.project_logger:
700
- self.project_logger.log_system(
701
- "Interactive session completed (subprocess fallback)",
702
- level="INFO",
703
- component="session"
704
- )
705
- self._log_session_event({
706
- "event": "interactive_session_complete",
707
- "fallback": True,
708
- "return_code": result.returncode
709
- })
710
- else:
711
- print(f"⚠️ Claude exited with code {result.returncode}")
712
- if self.project_logger:
713
- self.project_logger.log_system(
714
- f"Claude exited with non-zero code: {result.returncode}",
715
- level="WARNING",
716
- component="session"
717
- )
718
- except FileNotFoundError as e:
719
- print(f"❌ Fallback failed: Claude CLI not found in PATH")
720
- print("\n💡 To fix this issue:")
721
- print(" 1. Install Claude CLI: npm install -g @anthropic-ai/claude-ai")
722
- print(" 2. Or specify the full path to the claude binary")
723
- if self.project_logger:
724
- self.project_logger.log_system(
725
- f"Fallback failed - Claude CLI not found: {e}",
726
- level="ERROR",
727
- component="session"
728
- )
729
- except KeyboardInterrupt:
730
- print("\n⚠️ Fallback interrupted by user")
731
- if self.project_logger:
732
- self.project_logger.log_system(
733
- "Fallback interrupted by user",
734
- level="INFO",
735
- component="session"
736
- )
737
- except Exception as fallback_error:
738
- print(f"❌ Fallback failed with unexpected error: {fallback_error}")
739
- print(f" Error type: {type(fallback_error).__name__}")
740
- if self.project_logger:
741
- self.project_logger.log_system(
742
- f"Fallback launch failed: {fallback_error}",
743
- level="ERROR",
744
- component="session"
745
- )
746
- self._log_session_event({
747
- "event": "interactive_fallback_failed",
748
- "error": str(fallback_error),
749
- "exception_type": type(fallback_error).__name__
750
- })
531
+ finally:
532
+ # Step 4: Clean up session
533
+ session.cleanup_interactive_session()
751
534
 
752
535
  def run_oneshot(self, prompt: str, context: Optional[str] = None) -> bool:
753
- """Run Claude with a single prompt and return success status."""
754
- start_time = time.time()
536
+ """Run Claude with a single prompt and return success status.
755
537
 
756
- # Connect to Socket.IO server if enabled
757
- if self.enable_websocket:
758
- try:
759
- # Use Socket.IO client proxy to connect to monitoring server
760
- from claude_mpm.services.socketio_server import SocketIOClientProxy
761
- self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
762
- self.websocket_server.start()
763
- self.logger.info("Connected to Socket.IO monitoring server")
764
-
765
- # Generate session ID
766
- session_id = str(uuid.uuid4())
767
- working_dir = os.getcwd()
768
-
769
- # Notify session start
770
- self.websocket_server.session_started(
771
- session_id=session_id,
772
- launch_method="oneshot",
773
- working_dir=working_dir
774
- )
775
- except ImportError as e:
776
- self.logger.warning(f"Socket.IO module not available: {e}")
777
- self.websocket_server = None
778
- except ConnectionError as e:
779
- self.logger.warning(f"Cannot connect to Socket.IO server on port {self.websocket_port}: {e}")
780
- self.websocket_server = None
781
- except Exception as e:
782
- self.logger.warning(f"Unexpected error with Socket.IO server: {e}")
783
- self.websocket_server = None
784
-
785
- # Check for /mpm: commands
786
- if prompt.strip().startswith("/mpm:"):
787
- return self._handle_mpm_command(prompt.strip())
788
-
789
- if self.project_logger:
790
- self.project_logger.log_system(
791
- f"Starting non-interactive session with prompt: {prompt[:100]}",
792
- level="INFO",
793
- component="session"
794
- )
795
-
796
- # Setup agents - first deploy system agents, then project agents
797
- if not self.setup_agents():
798
- print("Continuing without native agents...")
799
-
800
- # Deploy project-specific agents if they exist
801
- self.deploy_project_agents_to_claude()
802
-
803
- # Combine context and prompt
804
- full_prompt = prompt
805
- if context:
806
- full_prompt = f"{context}\n\n{prompt}"
807
-
808
- # Build command with system instructions
809
- cmd = [
810
- "claude",
811
- "--model", "opus",
812
- "--dangerously-skip-permissions"
813
- ]
538
+ WHY: This method now delegates to OneshotSession class for better
539
+ maintainability and reduced complexity. The session class handles
540
+ all the details while this method provides the simple interface.
814
541
 
815
- # Add any custom Claude arguments
816
- if self.claude_args:
817
- cmd.extend(self.claude_args)
542
+ DESIGN DECISION: Using delegation pattern to reduce complexity from
543
+ 50 to <10 and lines from 332 to <80, while maintaining 100% backward
544
+ compatibility. All functionality is preserved through the session class.
818
545
 
819
- # Add print and prompt
820
- cmd.extend(["--print", full_prompt])
546
+ Args:
547
+ prompt: The command or prompt to execute
548
+ context: Optional context to prepend to the prompt
549
+
550
+ Returns:
551
+ bool: True if successful, False otherwise
552
+ """
553
+ from claude_mpm.core.oneshot_session import OneshotSession
821
554
 
822
- # Add system instructions if available
823
- system_prompt = self._create_system_prompt()
824
- if system_prompt and system_prompt != create_simple_context():
825
- # Insert system prompt before the user prompt
826
- cmd.insert(-2, "--append-system-prompt")
827
- cmd.insert(-2, system_prompt)
555
+ # Create session handler
556
+ session = OneshotSession(self)
828
557
 
829
558
  try:
830
- # Set up environment with correct working directory
831
- env = os.environ.copy()
832
-
833
- # Set the correct working directory for Claude Code
834
- if 'CLAUDE_MPM_USER_PWD' in env:
835
- user_pwd = env['CLAUDE_MPM_USER_PWD']
836
- env['CLAUDE_WORKSPACE'] = user_pwd
837
- # Change to that directory before running Claude
838
- try:
839
- original_cwd = os.getcwd()
840
- os.chdir(user_pwd)
841
- self.logger.info(f"Changed working directory to: {user_pwd}")
842
- except PermissionError as e:
843
- self.logger.warning(f"Permission denied accessing directory {user_pwd}: {e}")
844
- original_cwd = None
845
- except FileNotFoundError as e:
846
- self.logger.warning(f"Directory not found {user_pwd}: {e}")
847
- original_cwd = None
848
- except OSError as e:
849
- self.logger.warning(f"OS error changing to directory {user_pwd}: {e}")
850
- original_cwd = None
851
- else:
852
- original_cwd = None
559
+ # Step 1: Initialize session
560
+ success, error = session.initialize_session(prompt)
561
+ if not success:
562
+ return False
853
563
 
854
- # Run Claude
855
- if self.project_logger:
856
- self.project_logger.log_system(
857
- "Executing Claude subprocess",
858
- level="INFO",
859
- component="session"
860
- )
564
+ # Special case: MPM commands return early
565
+ if error is None and prompt.strip().startswith("/mpm:"):
566
+ return success
861
567
 
862
- # Notify WebSocket clients
863
- if self.websocket_server:
864
- self.websocket_server.claude_status_changed(
865
- status="running",
866
- message="Executing Claude oneshot command"
867
- )
568
+ # Step 2: Deploy agents
569
+ if not session.deploy_agents():
570
+ self.logger.warning("Agent deployment had issues, continuing...")
868
571
 
869
- result = subprocess.run(cmd, capture_output=True, text=True, env=env)
572
+ # Step 3: Set up infrastructure
573
+ infrastructure = session.setup_infrastructure()
870
574
 
871
- # Restore original directory if we changed it
872
- if original_cwd:
873
- try:
874
- os.chdir(original_cwd)
875
- except Exception:
876
- pass
877
- execution_time = time.time() - start_time
878
-
879
- if result.returncode == 0:
880
- response = result.stdout.strip()
881
- print(response)
882
-
883
- # Log response if logging enabled
884
- if self.response_logger and response:
885
- execution_time = time.time() - start_time
886
- response_summary = prompt[:200] + "..." if len(prompt) > 200 else prompt
887
- self.response_logger.log_response(
888
- request_summary=response_summary,
889
- response_content=response,
890
- metadata={
891
- "mode": "oneshot",
892
- "model": "opus",
893
- "exit_code": result.returncode,
894
- "execution_time": execution_time
895
- },
896
- agent="claude-direct"
897
- )
898
-
899
- # Broadcast output to WebSocket clients
900
- if self.websocket_server and response:
901
- self.websocket_server.claude_output(response, "stdout")
902
-
903
- if self.project_logger:
904
- # Log successful completion
905
- self.project_logger.log_system(
906
- f"Non-interactive session completed successfully in {execution_time:.2f}s",
907
- level="INFO",
908
- component="session"
909
- )
910
-
911
- # Log session event
912
- self._log_session_event({
913
- "event": "session_complete",
914
- "success": True,
915
- "execution_time": execution_time,
916
- "response_length": len(response)
917
- })
918
-
919
- # Log agent invocation if we detect delegation patterns
920
- if self._contains_delegation(response):
921
- self.project_logger.log_system(
922
- "Detected potential agent delegation in response",
923
- level="INFO",
924
- component="delegation"
925
- )
926
- self._log_session_event({
927
- "event": "delegation_detected",
928
- "prompt": prompt[:200],
929
- "indicators": [p for p in ["Task(", "subagent_type=", "engineer agent", "qa agent"]
930
- if p.lower() in response.lower()]
931
- })
932
-
933
- # Notify WebSocket clients about delegation
934
- if self.websocket_server:
935
- # Try to extract agent name
936
- agent_name = self._extract_agent_from_response(response)
937
- if agent_name:
938
- self.websocket_server.agent_delegated(
939
- agent=agent_name,
940
- task=prompt[:100],
941
- status="detected"
942
- )
943
-
944
- # Extract tickets if enabled
945
- if self.enable_tickets and self.ticket_manager and response:
946
- self._extract_tickets(response)
947
-
948
- return True
949
- else:
950
- error_msg = result.stderr or "Unknown error"
951
- print(f"Error: {error_msg}")
952
-
953
- # Broadcast error to WebSocket clients
954
- if self.websocket_server:
955
- self.websocket_server.claude_output(error_msg, "stderr")
956
- self.websocket_server.claude_status_changed(
957
- status="error",
958
- message=f"Command failed with code {result.returncode}"
959
- )
960
-
961
- if self.project_logger:
962
- self.project_logger.log_system(
963
- f"Non-interactive session failed: {error_msg}",
964
- level="ERROR",
965
- component="session"
966
- )
967
- self._log_session_event({
968
- "event": "session_failed",
969
- "success": False,
970
- "error": error_msg,
971
- "return_code": result.returncode
972
- })
973
-
974
- return False
975
-
976
- except subprocess.TimeoutExpired as e:
977
- error_msg = f"Command timed out after {e.timeout} seconds"
978
- print(f"⏱️ {error_msg}")
979
- if self.project_logger:
980
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
981
- self._log_session_event({
982
- "event": "session_timeout",
983
- "success": False,
984
- "timeout": e.timeout,
985
- "exception_type": "TimeoutExpired"
986
- })
987
- return False
988
-
989
- except FileNotFoundError as e:
990
- error_msg = "Claude CLI not found. Please ensure 'claude' is installed and in your PATH"
991
- print(f"❌ {error_msg}")
992
- print("\n💡 To fix: Install Claude CLI with 'npm install -g @anthropic-ai/claude-ai'")
993
- if self.project_logger:
994
- self.project_logger.log_system(f"{error_msg}: {e}", level="ERROR", component="session")
995
- self._log_session_event({
996
- "event": "session_exception",
997
- "success": False,
998
- "exception": str(e),
999
- "exception_type": "FileNotFoundError"
1000
- })
1001
- return False
1002
-
1003
- except PermissionError as e:
1004
- error_msg = f"Permission denied executing Claude CLI: {e}"
1005
- print(f"❌ {error_msg}")
1006
- if self.project_logger:
1007
- self.project_logger.log_system(error_msg, level="ERROR", component="session")
1008
- self._log_session_event({
1009
- "event": "session_exception",
1010
- "success": False,
1011
- "exception": str(e),
1012
- "exception_type": "PermissionError"
1013
- })
1014
- return False
1015
-
1016
- except KeyboardInterrupt:
1017
- print("\n⚠️ Command interrupted by user")
1018
- if self.project_logger:
1019
- self.project_logger.log_system(
1020
- "Session interrupted by user",
1021
- level="INFO",
1022
- component="session"
1023
- )
1024
- self._log_session_event({
1025
- "event": "session_interrupted",
1026
- "success": False,
1027
- "reason": "user_interrupt"
1028
- })
1029
- return False
1030
-
1031
- except MemoryError as e:
1032
- error_msg = "Out of memory while processing command"
1033
- print(f"❌ {error_msg}")
1034
- if self.project_logger:
1035
- self.project_logger.log_system(f"{error_msg}: {e}", level="ERROR", component="session")
1036
- self._log_session_event({
1037
- "event": "session_exception",
1038
- "success": False,
1039
- "exception": str(e),
1040
- "exception_type": "MemoryError"
1041
- })
1042
- return False
1043
-
1044
- except Exception as e:
1045
- error_msg = f"Unexpected error: {e}"
1046
- print(f"❌ {error_msg}")
1047
- print(f" Error type: {type(e).__name__}")
575
+ # Step 4: Execute command
576
+ success, response = session.execute_command(prompt, context, infrastructure)
1048
577
 
1049
- if self.project_logger:
1050
- self.project_logger.log_system(
1051
- f"Exception during non-interactive session: {e}",
1052
- level="ERROR",
1053
- component="session"
1054
- )
1055
- self._log_session_event({
1056
- "event": "session_exception",
1057
- "success": False,
1058
- "exception": str(e),
1059
- "exception_type": type(e).__name__
1060
- })
578
+ return success
1061
579
 
1062
- return False
1063
580
  finally:
1064
- # Ensure logs are flushed
1065
- if self.project_logger:
1066
- try:
1067
- # Log session summary
1068
- summary = self.project_logger.get_session_summary()
1069
- self.project_logger.log_system(
1070
- f"Session {summary['session_id']} completed",
1071
- level="INFO",
1072
- component="session"
1073
- )
1074
- except Exception as e:
1075
- self.logger.debug(f"Failed to log session summary: {e}")
1076
-
1077
- # End WebSocket session
1078
- if self.websocket_server:
1079
- self.websocket_server.claude_status_changed(
1080
- status="stopped",
1081
- message="Session completed"
1082
- )
1083
- self.websocket_server.session_ended()
581
+ # Step 5: Clean up session
582
+ session.cleanup_session()
1084
583
 
1085
584
  def _extract_tickets(self, text: str):
1086
585
  """Extract tickets from Claude's response."""
@@ -1153,6 +652,9 @@ class ClaudeRunner:
1153
652
  # Read raw instructions
1154
653
  raw_instructions = instructions_path.read_text()
1155
654
 
655
+ # Strip HTML metadata comments before processing
656
+ raw_instructions = self._strip_metadata_comments(raw_instructions)
657
+
1156
658
  # Process template variables if ContentAssembler is available
1157
659
  try:
1158
660
  from claude_mpm.services.framework_claude_md_generator.content_assembler import ContentAssembler
@@ -1164,6 +666,9 @@ class ClaudeRunner:
1164
666
  if base_pm_path.exists():
1165
667
  base_pm_content = base_pm_path.read_text()
1166
668
 
669
+ # Strip metadata comments from BASE_PM.md as well
670
+ base_pm_content = self._strip_metadata_comments(base_pm_content)
671
+
1167
672
  # Process BASE_PM.md with dynamic content injection
1168
673
  base_pm_content = self._process_base_pm_content(base_pm_content)
1169
674
 
@@ -1205,6 +710,45 @@ class ClaudeRunner:
1205
710
 
1206
711
  return base_pm_content
1207
712
 
713
+ def _strip_metadata_comments(self, content: str) -> str:
714
+ """Strip HTML metadata comments from content.
715
+
716
+ Removes comments like:
717
+ <!-- FRAMEWORK_VERSION: 0010 -->
718
+ <!-- LAST_MODIFIED: 2025-08-10T00:00:00Z -->
719
+ <!-- WORKFLOW_VERSION: ... -->
720
+ <!-- PROJECT_WORKFLOW_VERSION: ... -->
721
+ <!-- CUSTOM_PROJECT_WORKFLOW -->
722
+
723
+ WHY: These metadata comments are useful for internal tracking but should not
724
+ appear in the final instructions passed to Claude via --append-system-prompt.
725
+ They clutter the instructions and provide no value to the Claude agent.
726
+
727
+ DESIGN DECISION: Using regex to remove all HTML comments that contain known
728
+ metadata patterns. Also removes any resulting leading blank lines.
729
+ """
730
+ import re
731
+
732
+ # Remove HTML comments that contain metadata
733
+ patterns_to_strip = [
734
+ 'FRAMEWORK_VERSION',
735
+ 'LAST_MODIFIED',
736
+ 'WORKFLOW_VERSION',
737
+ 'PROJECT_WORKFLOW_VERSION',
738
+ 'CUSTOM_PROJECT_WORKFLOW',
739
+ 'AGENT_VERSION',
740
+ 'METADATA_VERSION'
741
+ ]
742
+
743
+ # Build regex pattern to match any of these metadata comments
744
+ pattern = r'<!--\s*(' + '|'.join(patterns_to_strip) + r')[^>]*-->\n?'
745
+ cleaned = re.sub(pattern, '', content)
746
+
747
+ # Also remove any leading blank lines that might result
748
+ cleaned = cleaned.lstrip('\n')
749
+
750
+ return cleaned
751
+
1208
752
  def _generate_deployed_agent_capabilities(self) -> str:
1209
753
  """Generate agent capabilities from deployed agents following Claude Code's hierarchy.
1210
754