claude-mpm 5.0.2__py3-none-any.whl → 5.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (184) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
  4. claude_mpm/agents/agent_loader.py +10 -17
  5. claude_mpm/agents/base_agent_loader.py +10 -35
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  8. claude_mpm/cli/__init__.py +0 -1
  9. claude_mpm/cli/commands/__init__.py +2 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +67 -23
  11. claude_mpm/cli/commands/agents.py +446 -25
  12. claude_mpm/cli/commands/auto_configure.py +535 -233
  13. claude_mpm/cli/commands/configure.py +1500 -147
  14. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/postmortem.py +401 -0
  19. claude_mpm/cli/commands/run.py +1 -39
  20. claude_mpm/cli/commands/skills.py +322 -19
  21. claude_mpm/cli/commands/summarize.py +413 -0
  22. claude_mpm/cli/executor.py +8 -0
  23. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  24. claude_mpm/cli/parsers/agents_parser.py +137 -0
  25. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  26. claude_mpm/cli/parsers/base_parser.py +9 -0
  27. claude_mpm/cli/parsers/skills_parser.py +7 -0
  28. claude_mpm/cli/startup.py +133 -85
  29. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  30. claude_mpm/commands/mpm-agents-list.md +2 -2
  31. claude_mpm/commands/mpm-config-view.md +2 -2
  32. claude_mpm/commands/mpm-help.md +3 -0
  33. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  34. claude_mpm/commands/mpm-postmortem.md +123 -0
  35. claude_mpm/commands/mpm-session-resume.md +2 -2
  36. claude_mpm/commands/mpm-ticket-view.md +2 -2
  37. claude_mpm/config/agent_presets.py +312 -82
  38. claude_mpm/config/agent_sources.py +27 -0
  39. claude_mpm/config/skill_presets.py +392 -0
  40. claude_mpm/constants.py +1 -0
  41. claude_mpm/core/claude_runner.py +2 -25
  42. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  43. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  44. claude_mpm/core/interactive_session.py +19 -5
  45. claude_mpm/core/oneshot_session.py +16 -4
  46. claude_mpm/core/output_style_manager.py +173 -43
  47. claude_mpm/core/protocols/__init__.py +23 -0
  48. claude_mpm/core/protocols/runner_protocol.py +103 -0
  49. claude_mpm/core/protocols/session_protocol.py +131 -0
  50. claude_mpm/core/shared/singleton_manager.py +11 -4
  51. claude_mpm/core/socketio_pool.py +3 -3
  52. claude_mpm/core/system_context.py +38 -0
  53. claude_mpm/core/unified_agent_registry.py +134 -16
  54. claude_mpm/core/unified_config.py +22 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  63. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  64. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  65. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  72. claude_mpm/models/agent_definition.py +7 -0
  73. claude_mpm/scripts/launch_monitor.py +93 -13
  74. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  75. claude_mpm/services/agents/cache_git_manager.py +621 -0
  76. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  77. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  78. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
  79. claude_mpm/services/agents/git_source_manager.py +20 -0
  80. claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
  81. claude_mpm/services/agents/toolchain_detector.py +6 -5
  82. claude_mpm/services/analysis/__init__.py +35 -0
  83. claude_mpm/services/analysis/clone_detector.py +1030 -0
  84. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  85. claude_mpm/services/analysis/postmortem_service.py +765 -0
  86. claude_mpm/services/command_deployment_service.py +106 -5
  87. claude_mpm/services/core/base.py +7 -2
  88. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  89. claude_mpm/services/event_bus/config.py +3 -1
  90. claude_mpm/services/git/git_operations_service.py +8 -8
  91. claude_mpm/services/mcp_config_manager.py +75 -145
  92. claude_mpm/services/mcp_service_verifier.py +6 -3
  93. claude_mpm/services/monitor/daemon.py +37 -10
  94. claude_mpm/services/monitor/daemon_manager.py +134 -21
  95. claude_mpm/services/monitor/server.py +225 -19
  96. claude_mpm/services/project/project_organizer.py +4 -0
  97. claude_mpm/services/runner_configuration_service.py +16 -3
  98. claude_mpm/services/session_management_service.py +16 -4
  99. claude_mpm/services/socketio/event_normalizer.py +15 -1
  100. claude_mpm/services/socketio/server/core.py +160 -21
  101. claude_mpm/services/version_control/git_operations.py +103 -0
  102. claude_mpm/utils/agent_filters.py +261 -0
  103. claude_mpm/utils/gitignore.py +3 -0
  104. claude_mpm/utils/migration.py +372 -0
  105. claude_mpm/utils/progress.py +5 -1
  106. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
  107. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
  108. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  109. claude_mpm/dashboard/analysis_runner.py +0 -455
  110. claude_mpm/dashboard/index.html +0 -13
  111. claude_mpm/dashboard/open_dashboard.py +0 -66
  112. claude_mpm/dashboard/static/css/activity.css +0 -1958
  113. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  114. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  115. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  116. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  117. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  118. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  119. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  120. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  121. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  122. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  123. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  124. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  125. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  126. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  127. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  128. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  129. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  130. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  131. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  132. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  133. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  134. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  135. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  136. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  137. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  138. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  139. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  140. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  141. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  142. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  143. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  144. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  145. claude_mpm/dashboard/templates/code_simple.html +0 -153
  146. claude_mpm/dashboard/templates/index.html +0 -606
  147. claude_mpm/dashboard/test_dashboard.html +0 -372
  148. claude_mpm/scripts/mcp_server.py +0 -75
  149. claude_mpm/scripts/mcp_wrapper.py +0 -39
  150. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  151. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  152. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  153. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  154. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  155. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  156. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  157. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  158. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  159. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  160. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
  161. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  162. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  163. claude_mpm/services/mcp_gateway/main.py +0 -589
  164. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  165. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  166. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  167. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  168. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  169. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  170. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  171. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  172. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  173. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  174. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  175. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  176. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  177. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  178. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  179. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  180. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  181. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  182. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  183. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  184. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -44,6 +44,7 @@ class UnifiedMonitorDaemon:
44
44
  daemon_mode: bool = False,
45
45
  pid_file: Optional[str] = None,
46
46
  log_file: Optional[str] = None,
47
+ enable_hot_reload: bool = False,
47
48
  ):
48
49
  """Initialize the unified monitor daemon.
49
50
 
@@ -53,10 +54,12 @@ class UnifiedMonitorDaemon:
53
54
  daemon_mode: Whether to run as background daemon
54
55
  pid_file: Path to PID file for daemon mode
55
56
  log_file: Path to log file for daemon mode
57
+ enable_hot_reload: Enable file watching and hot reload for development
56
58
  """
57
59
  self.host = host
58
60
  self.port = port
59
61
  self.daemon_mode = daemon_mode
62
+ self.enable_hot_reload = enable_hot_reload
60
63
  self.logger = get_logger(__name__)
61
64
 
62
65
  # Use new consolidated DaemonManager for all daemon operations
@@ -75,7 +78,9 @@ class UnifiedMonitorDaemon:
75
78
  )
76
79
 
77
80
  # Core server
78
- self.server = UnifiedMonitorServer(host=host, port=port)
81
+ self.server = UnifiedMonitorServer(
82
+ host=host, port=port, enable_hot_reload=enable_hot_reload
83
+ )
79
84
 
80
85
  # Health monitoring
81
86
  self.health_monitor = HealthMonitor(port=port)
@@ -88,11 +93,12 @@ class UnifiedMonitorDaemon:
88
93
  self.shutdown_event = threading.Event()
89
94
 
90
95
  def _get_default_pid_file(self) -> str:
91
- """Get default PID file path."""
96
+ """Get default PID file path with port number to support multiple daemons."""
92
97
  project_root = Path.cwd()
93
98
  claude_mpm_dir = project_root / ".claude-mpm"
94
99
  claude_mpm_dir.mkdir(exist_ok=True)
95
- return str(claude_mpm_dir / "monitor-daemon.pid")
100
+ # Include port in filename to support multiple daemon instances
101
+ return str(claude_mpm_dir / f"monitor-daemon-{self.port}.pid")
96
102
 
97
103
  def start(self, force_restart: bool = False) -> bool:
98
104
  """Start the unified monitor daemon.
@@ -145,16 +151,26 @@ class UnifiedMonitorDaemon:
145
151
  if self.daemon_manager.is_running():
146
152
  existing_pid = self.daemon_manager.get_pid()
147
153
  if not force_restart:
148
- self.logger.warning(f"Daemon already running with PID {existing_pid}")
154
+ msg = f"Daemon already running on port {self.port} with PID {existing_pid}"
155
+ self.logger.warning(msg)
156
+ # If we're in subprocess mode, this is an error - we should have cleaned up
157
+ if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
158
+ self.logger.error(
159
+ f"SUBPROCESS ERROR: {msg} - This should not happen in subprocess mode!"
160
+ )
149
161
  return False
150
162
  # Force restart was already handled above
151
163
 
152
164
  # Check for our service on the port
153
165
  is_ours, pid = self.daemon_manager.is_our_service()
154
166
  if is_ours and pid and not force_restart:
155
- self.logger.warning(
156
- f"Our service already running on port {self.port} (PID: {pid})"
157
- )
167
+ msg = f"Our service already running on port {self.port} (PID: {pid})"
168
+ self.logger.warning(msg)
169
+ # If we're in subprocess mode, this is an error - we should have cleaned up
170
+ if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
171
+ self.logger.error(
172
+ f"SUBPROCESS ERROR: {msg} - This should not happen in subprocess mode!"
173
+ )
158
174
  return False
159
175
 
160
176
  # Use subprocess approach for clean daemon startup (v4.2.40)
@@ -169,20 +185,29 @@ class UnifiedMonitorDaemon:
169
185
  if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
170
186
  # We're in a subprocess started by start_daemon_subprocess
171
187
  # We need to write the PID file ourselves since parent didn't
172
- self.logger.info("Running in subprocess daemon mode, writing PID file")
188
+ self.logger.info(f"Running in subprocess daemon mode on port {self.port}")
189
+ self.logger.info(f"Subprocess PID: {os.getpid()}")
190
+ self.logger.info(f"PID file path: {self.daemon_manager.pid_file}")
173
191
  self.daemon_manager.write_pid_file()
192
+ self.logger.info("PID file written successfully")
174
193
 
175
194
  # Setup signal handlers for graceful shutdown
176
195
  self._setup_signal_handlers()
196
+ self.logger.info("Signal handlers configured")
177
197
 
178
198
  # Start the server (this will run until shutdown)
199
+ self.logger.info("Starting server in subprocess mode...")
179
200
  try:
180
201
  result = self._run_server()
181
202
  if not result:
182
203
  self.logger.error("Failed to start server in subprocess mode")
204
+ return result
205
+ self.logger.info("Server started successfully in subprocess mode")
183
206
  return result
184
207
  except Exception as e:
185
- self.logger.error(f"Server startup exception in subprocess: {e}")
208
+ self.logger.error(
209
+ f"Server startup exception in subprocess: {e}", exc_info=True
210
+ )
186
211
  raise
187
212
  else:
188
213
  # Legacy fork approach (kept for compatibility but not used by default)
@@ -490,7 +515,9 @@ class UnifiedMonitorDaemon:
490
515
 
491
516
  # Recreate the server and health monitor after stop() sets them to None
492
517
  self.logger.info(f"Recreating server components for {self.host}:{self.port}")
493
- self.server = UnifiedMonitorServer(host=self.host, port=self.port)
518
+ self.server = UnifiedMonitorServer(
519
+ host=self.host, port=self.port, enable_hot_reload=self.enable_hot_reload
520
+ )
494
521
  self.health_monitor = HealthMonitor(port=self.port)
495
522
 
496
523
  # Reset the shutdown event for the new run
@@ -34,6 +34,11 @@ from typing import Optional, Tuple
34
34
  from ...core.enums import OperationResult
35
35
  from ...core.logging_config import get_logger
36
36
 
37
+ # Exit code constants for signal handling
38
+ EXIT_NORMAL = 0
39
+ EXIT_SIGKILL = 137 # 128 + SIGKILL(9) - forced termination
40
+ EXIT_SIGTERM = 143 # 128 + SIGTERM(15) - graceful shutdown
41
+
37
42
 
38
43
  class DaemonManager:
39
44
  """Centralized manager for all daemon lifecycle operations.
@@ -80,18 +85,20 @@ class DaemonManager:
80
85
  self.startup_status_file = None
81
86
 
82
87
  def _get_default_pid_file(self) -> Path:
83
- """Get default PID file path."""
88
+ """Get default PID file path with port number to support multiple daemons."""
84
89
  project_root = Path.cwd()
85
90
  claude_mpm_dir = project_root / ".claude-mpm"
86
91
  claude_mpm_dir.mkdir(exist_ok=True)
87
- return claude_mpm_dir / "monitor-daemon.pid"
92
+ # Include port in filename to support multiple daemon instances
93
+ return claude_mpm_dir / f"monitor-daemon-{self.port}.pid"
88
94
 
89
95
  def _get_default_log_file(self) -> Path:
90
- """Get default log file path."""
96
+ """Get default log file path with port number to support multiple daemons."""
91
97
  project_root = Path.cwd()
92
98
  claude_mpm_dir = project_root / ".claude-mpm"
93
99
  claude_mpm_dir.mkdir(exist_ok=True)
94
- return claude_mpm_dir / "monitor-daemon.log"
100
+ # Include port in filename to support multiple daemon instances
101
+ return claude_mpm_dir / f"monitor-daemon-{self.port}.log"
95
102
 
96
103
  def cleanup_port_conflicts(self, max_retries: int = 3) -> bool:
97
104
  """Clean up any processes using the daemon port.
@@ -471,6 +478,55 @@ class DaemonManager:
471
478
 
472
479
  return None
473
480
 
481
+ def _verify_daemon_health(self, max_attempts: int = 3) -> bool:
482
+ """Verify daemon is healthy by checking HTTP health endpoint.
483
+
484
+ Args:
485
+ max_attempts: Maximum number of connection attempts
486
+
487
+ Returns:
488
+ True if health check passes, False otherwise
489
+ """
490
+ try:
491
+ import requests
492
+
493
+ for attempt in range(max_attempts):
494
+ try:
495
+ # Try to connect to health endpoint
496
+ response = requests.get(
497
+ f"http://{self.host}:{self.port}/health", timeout=2
498
+ )
499
+
500
+ if response.status_code == 200:
501
+ self.logger.debug(
502
+ f"Health check passed on attempt {attempt + 1}/{max_attempts}"
503
+ )
504
+ return True
505
+
506
+ self.logger.debug(
507
+ f"Health check returned status {response.status_code} on attempt {attempt + 1}/{max_attempts}"
508
+ )
509
+
510
+ except requests.exceptions.RequestException as e:
511
+ self.logger.debug(
512
+ f"Health check attempt {attempt + 1}/{max_attempts} failed: {e}"
513
+ )
514
+
515
+ # Wait before retry (except on last attempt)
516
+ if attempt < max_attempts - 1:
517
+ time.sleep(1)
518
+
519
+ self.logger.debug(f"Health check failed after {max_attempts} attempts")
520
+ return False
521
+
522
+ except ImportError:
523
+ # requests not available, skip health check
524
+ self.logger.debug("requests library not available, skipping health check")
525
+ return True
526
+ except Exception as e:
527
+ self.logger.debug(f"Health check error: {e}")
528
+ return False
529
+
474
530
  def start_daemon(self, force_restart: bool = False) -> bool:
475
531
  """Start the daemon with automatic cleanup and retry.
476
532
 
@@ -505,7 +561,7 @@ class DaemonManager:
505
561
  # Use subprocess for clean daemon startup (v4.2.40)
506
562
  # This avoids fork() issues with Python threading
507
563
  if self.use_subprocess_daemon():
508
- return self.start_daemon_subprocess()
564
+ return self.start_daemon_subprocess(force_restart=force_restart)
509
565
  # Fallback to traditional fork (kept for compatibility)
510
566
  return self.daemonize()
511
567
 
@@ -523,12 +579,15 @@ class DaemonManager:
523
579
  # Otherwise, use subprocess for monitor daemon to avoid threading issues
524
580
  return True
525
581
 
526
- def start_daemon_subprocess(self) -> bool:
582
+ def start_daemon_subprocess(self, force_restart: bool = False) -> bool:
527
583
  """Start daemon using subprocess.Popen for clean process isolation.
528
584
 
529
585
  This avoids all the fork() + threading issues by starting the monitor
530
586
  in a completely fresh process with no inherited threads or locks.
531
587
 
588
+ Args:
589
+ force_restart: Whether this is a force restart (helps interpret exit codes)
590
+
532
591
  Returns:
533
592
  True if daemon started successfully
534
593
  """
@@ -588,36 +647,90 @@ class DaemonManager:
588
647
  pid = process.pid
589
648
  self.logger.info(f"Monitor subprocess started with PID {pid}")
590
649
 
591
- # Wait for the subprocess to write its PID file
650
+ # Wait for the subprocess to write its PID file and bind to port
592
651
  # The subprocess will write the PID file after it starts successfully
593
652
  max_wait = 10 # seconds
594
653
  start_time = time.time()
654
+ pid_file_found = False
655
+ port_bound = False
656
+
657
+ self.logger.debug(f"Waiting up to {max_wait}s for daemon to start...")
595
658
 
596
659
  while time.time() - start_time < max_wait:
597
660
  # Check if process is still running
598
- if process.poll() is not None:
599
- # Process exited
661
+ returncode = process.poll()
662
+ if returncode is not None:
663
+ # Process exited - interpret exit code with context
664
+ # Exit codes 137 (SIGKILL) and 143 (SIGTERM) are common during daemon replacement
665
+ if returncode == EXIT_SIGKILL:
666
+ # SIGKILL - process was forcefully terminated
667
+ if force_restart:
668
+ # This is expected during force restart - old daemon was killed
669
+ self.logger.info(
670
+ f"Previous monitor instance replaced (exit {EXIT_SIGKILL}: SIGKILL during force restart)"
671
+ )
672
+ else:
673
+ # Unexpected SIGKILL - something else killed our new daemon
674
+ self.logger.warning(
675
+ f"Monitor subprocess terminated unexpectedly (exit {EXIT_SIGKILL}: SIGKILL). "
676
+ f"Check {self.log_file} for details."
677
+ )
678
+ return False
679
+ if returncode == EXIT_SIGTERM:
680
+ # SIGTERM - graceful shutdown requested
681
+ self.logger.info(
682
+ f"Monitor subprocess cleanly terminated (exit {EXIT_SIGTERM}: SIGTERM, graceful shutdown)"
683
+ )
684
+ return False
685
+ if returncode == EXIT_NORMAL:
686
+ # Normal exit
687
+ self.logger.info(
688
+ f"Monitor subprocess exited normally (exit code {EXIT_NORMAL})"
689
+ )
690
+ return False
691
+ # Unexpected exit code - this IS an error
692
+ self.logger.error(
693
+ f"Monitor daemon subprocess exited prematurely with code {returncode}"
694
+ )
600
695
  self.logger.error(
601
- f"Monitor daemon exited with code {process.returncode}"
696
+ f"Port {self.port} daemon failed to start. Check {self.log_file} for details."
602
697
  )
603
698
  return False
604
699
 
605
700
  # Check if PID file was written
606
- if self.pid_file.exists():
701
+ if not pid_file_found and self.pid_file.exists():
607
702
  try:
608
703
  with self.pid_file.open() as f:
609
704
  written_pid = int(f.read().strip())
610
705
  if written_pid == pid:
611
- # PID file written correctly, check port
612
- if (
613
- not self._is_port_available()
614
- ): # Port NOT available means it's in use (good!)
615
- self.logger.info(
616
- f"Monitor daemon successfully started on port {self.port}"
617
- )
618
- return True
619
- except Exception:
620
- pass # PID file not ready yet
706
+ pid_file_found = True
707
+ self.logger.debug(
708
+ f"PID file found with correct PID {pid}"
709
+ )
710
+ except Exception as e:
711
+ self.logger.debug(f"Error reading PID file: {e}")
712
+
713
+ # Check if port is bound (health check)
714
+ if not port_bound and not self._is_port_available():
715
+ # Port NOT available means it's in use (good!)
716
+ port_bound = True
717
+ self.logger.debug(f"Port {self.port} is now bound")
718
+
719
+ # Success criteria: both PID file exists and port is bound
720
+ if pid_file_found and port_bound:
721
+ self.logger.info(
722
+ f"Monitor daemon successfully started on port {self.port} (PID: {pid})"
723
+ )
724
+ # Additional health check: verify we can connect
725
+ if self._verify_daemon_health(max_attempts=3):
726
+ self.logger.info("Daemon health check passed")
727
+ return True
728
+ self.logger.warning(
729
+ "Daemon started but health check failed - may still be initializing"
730
+ )
731
+ return (
732
+ True # Return success anyway if PID file and port are good
733
+ )
621
734
 
622
735
  time.sleep(0.5)
623
736