claude-mpm 5.1.9__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
  3. claude_mpm/agents/agent_loader.py +10 -17
  4. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  5. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  6. claude_mpm/cli/commands/configure.py +1046 -149
  7. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  8. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  9. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  10. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  11. claude_mpm/cli/commands/summarize.py +413 -0
  12. claude_mpm/cli/executor.py +8 -0
  13. claude_mpm/cli/parsers/base_parser.py +5 -0
  14. claude_mpm/cli/startup.py +60 -53
  15. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  16. claude_mpm/config/agent_sources.py +27 -0
  17. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  18. claude_mpm/core/socketio_pool.py +3 -3
  19. claude_mpm/core/unified_agent_registry.py +5 -15
  20. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  36. claude_mpm/scripts/launch_monitor.py +93 -13
  37. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  39. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
  40. claude_mpm/services/agents/git_source_manager.py +20 -0
  41. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  42. claude_mpm/services/agents/toolchain_detector.py +6 -5
  43. claude_mpm/services/analysis/__init__.py +11 -1
  44. claude_mpm/services/analysis/clone_detector.py +1030 -0
  45. claude_mpm/services/command_deployment_service.py +0 -2
  46. claude_mpm/services/event_bus/config.py +3 -1
  47. claude_mpm/services/monitor/daemon.py +9 -2
  48. claude_mpm/services/monitor/daemon_manager.py +39 -3
  49. claude_mpm/services/monitor/server.py +225 -19
  50. claude_mpm/services/socketio/event_normalizer.py +15 -1
  51. claude_mpm/services/socketio/server/core.py +160 -21
  52. claude_mpm/services/version_control/git_operations.py +103 -0
  53. claude_mpm/utils/agent_filters.py +17 -44
  54. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
  55. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
  56. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  57. claude_mpm/dashboard/analysis_runner.py +0 -455
  58. claude_mpm/dashboard/index.html +0 -13
  59. claude_mpm/dashboard/open_dashboard.py +0 -66
  60. claude_mpm/dashboard/static/css/activity.css +0 -1958
  61. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  62. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  63. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  64. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  65. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  66. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  67. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  68. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  69. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  70. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  71. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  72. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  73. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  74. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  75. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  76. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  77. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  78. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  79. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  80. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  81. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  82. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  83. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  84. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  85. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  86. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  87. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  88. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  89. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  90. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  91. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  92. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  93. claude_mpm/dashboard/templates/code_simple.html +0 -153
  94. claude_mpm/dashboard/templates/index.html +0 -606
  95. claude_mpm/dashboard/test_dashboard.html +0 -372
  96. claude_mpm/scripts/mcp_server.py +0 -75
  97. claude_mpm/scripts/mcp_wrapper.py +0 -39
  98. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  99. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  100. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  101. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  102. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  103. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  104. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  105. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  106. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  107. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  108. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  109. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  110. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  111. claude_mpm/services/mcp_gateway/main.py +0 -589
  112. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  113. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  114. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  115. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  116. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  117. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  118. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  119. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  120. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  121. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  122. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  123. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  124. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  125. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  126. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  127. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  128. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  129. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  130. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -24,7 +24,6 @@ class CommandDeploymentService(BaseService):
24
24
  "mpm-agents.md", # Replaced by mpm-agents-list.md
25
25
  "mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
26
26
  "mpm-config.md", # Replaced by mpm-config-view.md
27
- "mpm-organize.md", # Replaced by mpm-ticket-organize.md
28
27
  "mpm-resume.md", # Replaced by mpm-session-resume.md
29
28
  "mpm-ticket.md", # Replaced by mpm-ticket-view.md
30
29
  ]
@@ -316,7 +315,6 @@ class CommandDeploymentService(BaseService):
316
315
  "mpm-agents.md": "mpm-agents-list.md",
317
316
  "mpm-auto-configure.md": "mpm-agents-auto-configure.md",
318
317
  "mpm-config.md": "mpm-config-view.md",
319
- "mpm-organize.md": "mpm-ticket-organize.md",
320
318
  "mpm-resume.md": "mpm-session-resume.md",
321
319
  "mpm-ticket.md": "mpm-ticket-view.md",
322
320
  }
@@ -52,9 +52,11 @@ class EventBusConfig:
52
52
  )
53
53
 
54
54
  # Relay configuration
55
+ # DirectSocketIORelay disabled by default - events already emit via direct sio.emit()
56
+ # Enable with CLAUDE_MPM_RELAY_ENABLED=true if needed for external consumers
55
57
  relay_enabled: bool = field(
56
58
  default_factory=lambda: os.environ.get(
57
- "CLAUDE_MPM_RELAY_ENABLED", "true"
59
+ "CLAUDE_MPM_RELAY_ENABLED", "false"
58
60
  ).lower()
59
61
  == "true"
60
62
  )
@@ -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)
@@ -510,7 +515,9 @@ class UnifiedMonitorDaemon:
510
515
 
511
516
  # Recreate the server and health monitor after stop() sets them to None
512
517
  self.logger.info(f"Recreating server components for {self.host}:{self.port}")
513
- 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
+ )
514
521
  self.health_monitor = HealthMonitor(port=self.port)
515
522
 
516
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.
@@ -556,7 +561,7 @@ class DaemonManager:
556
561
  # Use subprocess for clean daemon startup (v4.2.40)
557
562
  # This avoids fork() issues with Python threading
558
563
  if self.use_subprocess_daemon():
559
- return self.start_daemon_subprocess()
564
+ return self.start_daemon_subprocess(force_restart=force_restart)
560
565
  # Fallback to traditional fork (kept for compatibility)
561
566
  return self.daemonize()
562
567
 
@@ -574,12 +579,15 @@ class DaemonManager:
574
579
  # Otherwise, use subprocess for monitor daemon to avoid threading issues
575
580
  return True
576
581
 
577
- def start_daemon_subprocess(self) -> bool:
582
+ def start_daemon_subprocess(self, force_restart: bool = False) -> bool:
578
583
  """Start daemon using subprocess.Popen for clean process isolation.
579
584
 
580
585
  This avoids all the fork() + threading issues by starting the monitor
581
586
  in a completely fresh process with no inherited threads or locks.
582
587
 
588
+ Args:
589
+ force_restart: Whether this is a force restart (helps interpret exit codes)
590
+
583
591
  Returns:
584
592
  True if daemon started successfully
585
593
  """
@@ -652,7 +660,35 @@ class DaemonManager:
652
660
  # Check if process is still running
653
661
  returncode = process.poll()
654
662
  if returncode is not None:
655
- # Process exited - this is the bug we're fixing!
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
656
692
  self.logger.error(
657
693
  f"Monitor daemon subprocess exited prematurely with code {returncode}"
658
694
  )
@@ -15,7 +15,6 @@ DESIGN DECISIONS:
15
15
  """
16
16
 
17
17
  import asyncio
18
- import contextlib
19
18
  import os
20
19
  import threading
21
20
  import time
@@ -25,6 +24,8 @@ from typing import Dict, Optional
25
24
 
26
25
  import socketio
27
26
  from aiohttp import web
27
+ from watchdog.events import FileSystemEventHandler
28
+ from watchdog.observers import Observer
28
29
 
29
30
  from ...core.enums import ServiceState
30
31
  from ...core.logging_config import get_logger
@@ -45,6 +46,91 @@ except ImportError:
45
46
  EVENTBUS_AVAILABLE = False
46
47
 
47
48
 
49
+ class SvelteBuildWatcher(FileSystemEventHandler):
50
+ """File watcher for Svelte build directory changes.
51
+
52
+ Watches for file changes in svelte-build directory and triggers
53
+ hot reload via Socket.IO event emission.
54
+
55
+ STABILITY FIX: Added thread lock and stop() method to prevent timer leaks.
56
+ """
57
+
58
+ def __init__(
59
+ self, sio: socketio.AsyncServer, loop: asyncio.AbstractEventLoop, logger
60
+ ):
61
+ """Initialize the file watcher.
62
+
63
+ Args:
64
+ sio: Socket.IO server instance for emitting events
65
+ loop: Event loop for async operations
66
+ logger: Logger instance
67
+ """
68
+ super().__init__()
69
+ self.sio = sio
70
+ self.loop = loop
71
+ self.logger = logger
72
+ self.debounce_timer = None
73
+ self.debounce_delay = 0.5 # Wait 500ms after last change
74
+ self._timer_lock = threading.Lock() # STABILITY FIX: Prevent race condition
75
+
76
+ def stop(self):
77
+ """Stop the watcher and cancel any pending timers.
78
+
79
+ STABILITY FIX: Ensures timer is cancelled on shutdown.
80
+ """
81
+ with self._timer_lock:
82
+ if self.debounce_timer:
83
+ self.debounce_timer.cancel()
84
+ self.debounce_timer = None
85
+
86
+ def on_any_event(self, event):
87
+ """Handle any file system event.
88
+
89
+ Args:
90
+ event: File system event from watchdog
91
+ """
92
+ # Ignore directory events and temporary files
93
+ if event.is_directory or event.src_path.endswith((".tmp", ".swp", "~")):
94
+ return
95
+
96
+ self.logger.debug(
97
+ f"File change detected: {event.event_type} - {event.src_path}"
98
+ )
99
+
100
+ # STABILITY FIX: Use lock to prevent timer race condition
101
+ with self._timer_lock:
102
+ # Cancel existing timer
103
+ if self.debounce_timer:
104
+ self.debounce_timer.cancel()
105
+
106
+ # Schedule reload after debounce delay
107
+ self.debounce_timer = threading.Timer(
108
+ self.debounce_delay, self._trigger_reload
109
+ )
110
+ self.debounce_timer.start()
111
+
112
+ def _trigger_reload(self):
113
+ """Trigger hot reload by emitting Socket.IO event."""
114
+ try:
115
+ # Schedule the async emit in the event loop
116
+ asyncio.run_coroutine_threadsafe(self._emit_reload_event(), self.loop)
117
+ self.logger.info("Hot reload triggered - Svelte build changed")
118
+ except Exception as e:
119
+ self.logger.error(f"Error triggering reload: {e}")
120
+
121
+ async def _emit_reload_event(self):
122
+ """Emit the reload event to all connected clients."""
123
+ if self.sio:
124
+ await self.sio.emit(
125
+ "reload",
126
+ {
127
+ "type": "reload",
128
+ "timestamp": datetime.now(timezone.utc).isoformat() + "Z",
129
+ "reason": "svelte-build-updated",
130
+ },
131
+ )
132
+
133
+
48
134
  class UnifiedMonitorServer:
49
135
  """Unified server that combines HTTP dashboard and Socket.IO functionality.
50
136
 
@@ -52,15 +138,19 @@ class UnifiedMonitorServer:
52
138
  Replaces multiple competing server implementations with one stable solution.
53
139
  """
54
140
 
55
- def __init__(self, host: str = "localhost", port: int = 8765):
141
+ def __init__(
142
+ self, host: str = "localhost", port: int = 8765, enable_hot_reload: bool = False
143
+ ):
56
144
  """Initialize the unified monitor server.
57
145
 
58
146
  Args:
59
147
  host: Host to bind to
60
148
  port: Port to bind to
149
+ enable_hot_reload: Enable file watching and hot reload for development
61
150
  """
62
151
  self.host = host
63
152
  self.port = port
153
+ self.enable_hot_reload = enable_hot_reload
64
154
  self.logger = get_logger(__name__)
65
155
 
66
156
  # Core components
@@ -78,6 +168,10 @@ class UnifiedMonitorServer:
78
168
  # High-performance event emitter
79
169
  self.event_emitter = None
80
170
 
171
+ # File watching (optional for dev mode)
172
+ self.file_observer: Optional[Observer] = None
173
+ self.file_watcher: Optional[SvelteBuildWatcher] = None
174
+
81
175
  # State
82
176
  self.running = False
83
177
  self.loop = None
@@ -184,6 +278,9 @@ class UnifiedMonitorServer:
184
278
 
185
279
  time.sleep(0.1)
186
280
 
281
+ # STABILITY FIX: Give tasks more time to clean up before closing
282
+ time.sleep(0.5)
283
+
187
284
  # Clear the event loop from the thread BEFORE closing
188
285
  # This prevents other code from accidentally using it
189
286
  asyncio.set_event_loop(None)
@@ -229,6 +326,10 @@ class UnifiedMonitorServer:
229
326
  self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
230
327
  self.logger.info("Heartbeat task started (3-minute interval)")
231
328
 
329
+ # Setup file watching for hot reload (if enabled)
330
+ if self.enable_hot_reload:
331
+ self._setup_file_watcher()
332
+
232
333
  # Setup HTTP routes
233
334
  self._setup_http_routes()
234
335
 
@@ -304,20 +405,64 @@ class UnifiedMonitorServer:
304
405
  self.logger.error(f"Error setting up event emitter: {e}")
305
406
  raise
306
407
 
408
+ def _setup_file_watcher(self):
409
+ """Setup file watcher for Svelte build directory.
410
+
411
+ Watches for changes in svelte-build and triggers hot reload.
412
+ Only enabled when enable_hot_reload is True.
413
+ """
414
+ try:
415
+ dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
416
+ svelte_build_dir = dashboard_dir / "static" / "svelte-build"
417
+
418
+ if not svelte_build_dir.exists():
419
+ self.logger.warning(
420
+ f"Svelte build directory not found: {svelte_build_dir}. "
421
+ "Hot reload disabled."
422
+ )
423
+ return
424
+
425
+ # Create file watcher with Socket.IO reference
426
+ self.file_watcher = SvelteBuildWatcher(
427
+ sio=self.sio, loop=self.loop, logger=self.logger
428
+ )
429
+
430
+ # Create observer and schedule watching
431
+ self.file_observer = Observer()
432
+ self.file_observer.schedule(
433
+ self.file_watcher, str(svelte_build_dir), recursive=True
434
+ )
435
+ self.file_observer.start()
436
+
437
+ self.logger.info(f"🔥 Hot reload enabled - watching {svelte_build_dir}")
438
+
439
+ except Exception as e:
440
+ self.logger.error(f"Error setting up file watcher: {e}")
441
+ # Don't raise - hot reload is optional
442
+
307
443
  def _setup_http_routes(self):
308
444
  """Setup HTTP routes for the dashboard."""
309
445
  try:
310
- # Dashboard static files
311
- dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
446
+ # Dashboard static files - use .resolve() for absolute path
447
+ dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
448
+ static_dir = dashboard_dir / "static"
312
449
 
313
- # Main dashboard route
450
+ # Main dashboard route - serve Svelte dashboard
314
451
  async def dashboard_index(request):
315
- template_path = dashboard_dir / "templates" / "index.html"
316
- if template_path.exists():
317
- with template_path.open() as f:
452
+ svelte_index = static_dir / "svelte-build" / "index.html"
453
+ if svelte_index.exists():
454
+ with svelte_index.open(encoding="utf-8") as f:
318
455
  content = f.read()
319
456
  return web.Response(text=content, content_type="text/html")
320
- return web.Response(text="Dashboard not found", status=404)
457
+
458
+ # Log error with path details for debugging
459
+ self.logger.error(
460
+ f"Dashboard index.html not found at: {svelte_index.resolve()}"
461
+ )
462
+ return web.Response(
463
+ text=f"Dashboard not found. Expected location: {svelte_index.resolve()}",
464
+ status=404,
465
+ )
321
466
 
322
467
  # Health check
323
468
  async def health_check(request):
@@ -325,7 +470,8 @@ class UnifiedMonitorServer:
325
470
  version = "1.0.0"
326
471
  try:
327
472
  version_file = (
328
- Path(__file__).parent.parent.parent.parent.parent / "VERSION"
473
+ Path(__file__).resolve().parent.parent.parent.parent.parent
474
+ / "VERSION"
329
475
  )
330
476
  if version_file.exists():
331
477
  version = version_file.read_text().strip()
@@ -546,12 +692,43 @@ class UnifiedMonitorServer:
546
692
  "/monitor/events", lambda r: monitor_page_handler(r)
547
693
  )
548
694
 
549
- # Static files with cache busting headers for development
550
- static_dir = dashboard_dir / "static"
695
+ # Serve Svelte _app assets (compiled JS/CSS)
696
+ svelte_build_dir = static_dir / "svelte-build"
697
+ if svelte_build_dir.exists():
698
+ svelte_app_dir = svelte_build_dir / "_app"
699
+ if svelte_app_dir.exists():
700
+ # Serve _app assets with proper caching
701
+ async def app_assets_handler(request):
702
+ """Serve Svelte _app assets."""
703
+ from aiohttp.web_fileresponse import FileResponse
704
+
705
+ rel_path = request.match_info["filepath"]
706
+ file_path = svelte_app_dir / rel_path
707
+
708
+ if not file_path.exists() or not file_path.is_file():
709
+ raise web.HTTPNotFound()
710
+
711
+ response = FileResponse(file_path)
712
+
713
+ # Add cache headers for immutable assets
714
+ if "/immutable/" in str(rel_path):
715
+ response.headers["Cache-Control"] = (
716
+ "public, max-age=31536000, immutable"
717
+ )
718
+ else:
719
+ response.headers["Cache-Control"] = (
720
+ "no-cache, no-store, must-revalidate"
721
+ )
722
+
723
+ return response
724
+
725
+ self.app.router.add_get("/_app/{filepath:.*}", app_assets_handler)
726
+
727
+ # Legacy static files (for backward compatibility)
551
728
  if static_dir.exists():
552
729
 
553
730
  async def static_handler(request):
554
- """Serve static files with cache-control headers for development."""
731
+ """Serve legacy static files with cache-control headers for development."""
555
732
 
556
733
  from aiohttp.web_fileresponse import FileResponse
557
734
 
@@ -576,10 +753,13 @@ class UnifiedMonitorServer:
576
753
 
577
754
  self.app.router.add_get("/static/{filepath:.*}", static_handler)
578
755
 
579
- # Templates
580
- templates_dir = dashboard_dir / "templates"
581
- if templates_dir.exists():
582
- self.app.router.add_static("/templates/", templates_dir)
756
+ # Log dashboard availability
757
+ if svelte_build_dir.exists():
758
+ self.logger.info(
759
+ f"✅ Svelte dashboard available at / (root) (build: {svelte_build_dir})"
760
+ )
761
+ else:
762
+ self.logger.warning(f"Svelte build not found at: {svelte_build_dir}")
583
763
 
584
764
  self.logger.info("HTTP routes registered successfully")
585
765
 
@@ -691,11 +871,37 @@ class UnifiedMonitorServer:
691
871
  async def _cleanup_async(self):
692
872
  """Cleanup async resources."""
693
873
  try:
874
+ # Stop file observer if running
875
+ # STABILITY FIX: Ensure watcher is stopped and verify observer termination
876
+ if self.file_observer:
877
+ try:
878
+ # Stop the watcher first to cancel pending timers
879
+ if self.file_watcher:
880
+ self.file_watcher.stop()
881
+
882
+ # Stop the observer
883
+ self.file_observer.stop()
884
+ self.file_observer.join(timeout=2)
885
+
886
+ # Verify observer actually stopped
887
+ if self.file_observer.is_alive():
888
+ self.logger.warning("File observer did not stop cleanly")
889
+
890
+ self.logger.debug("File observer stopped")
891
+ except Exception as e:
892
+ self.logger.debug(f"Error stopping file observer: {e}")
893
+ finally:
894
+ self.file_observer = None
895
+ self.file_watcher = None
896
+
694
897
  # Cancel heartbeat task if running
898
+ # STABILITY FIX: Add timeout to prevent infinite wait on cancellation
695
899
  if self.heartbeat_task and not self.heartbeat_task.done():
696
900
  self.heartbeat_task.cancel()
697
- with contextlib.suppress(asyncio.CancelledError):
698
- await self.heartbeat_task
901
+ try:
902
+ await asyncio.wait_for(self.heartbeat_task, timeout=2.0)
903
+ except (asyncio.CancelledError, asyncio.TimeoutError):
904
+ pass
699
905
  self.logger.debug("Heartbeat task cancelled")
700
906
 
701
907
  # Close the Socket.IO server first to stop accepting new connections
@@ -78,10 +78,13 @@ class NormalizedEvent:
78
78
  subtype: str = "" # Specific event type
79
79
  timestamp: str = "" # ISO format timestamp
80
80
  data: Dict[str, Any] = field(default_factory=dict) # Event payload
81
+ correlation_id: Optional[str] = (
82
+ None # For correlating related events (e.g., pre_tool/post_tool)
83
+ )
81
84
 
82
85
  def to_dict(self) -> Dict[str, Any]:
83
86
  """Convert to dictionary for emission."""
84
- return {
87
+ result = {
85
88
  "event": self.event,
86
89
  "source": self.source,
87
90
  "type": self.type,
@@ -89,6 +92,10 @@ class NormalizedEvent:
89
92
  "timestamp": self.timestamp,
90
93
  "data": self.data,
91
94
  }
95
+ # Include correlation_id if present
96
+ if self.correlation_id:
97
+ result["correlation_id"] = self.correlation_id
98
+ return result
92
99
 
93
100
 
94
101
  class EventNormalizer:
@@ -218,6 +225,11 @@ class EventNormalizer:
218
225
  # Get or generate timestamp
219
226
  timestamp = self._extract_timestamp(event_data)
220
227
 
228
+ # Extract correlation_id if present
229
+ correlation_id = None
230
+ if isinstance(event_data, dict):
231
+ correlation_id = event_data.get("correlation_id")
232
+
221
233
  # Create normalized event
222
234
  normalized = NormalizedEvent(
223
235
  event="claude_event",
@@ -226,6 +238,7 @@ class EventNormalizer:
226
238
  subtype=subtype,
227
239
  timestamp=timestamp,
228
240
  data=data,
241
+ correlation_id=correlation_id,
229
242
  )
230
243
 
231
244
  self.stats["normalized"] += 1
@@ -281,6 +294,7 @@ class EventNormalizer:
281
294
  "timestamp", datetime.now(timezone.utc).isoformat()
282
295
  ),
283
296
  data=event_data.get("data", {}),
297
+ correlation_id=event_data.get("correlation_id"),
284
298
  )
285
299
 
286
300
  def _extract_event_info(self, event_data: Any) -> Tuple[str, str, Dict[str, Any]]: