claude-mpm 5.1.9__py3-none-any.whl → 5.4.14__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 (162) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -0
  4. claude_mpm/agents/agent_loader.py +13 -44
  5. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  6. claude_mpm/cli/__main__.py +4 -0
  7. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  8. claude_mpm/cli/commands/auto_configure.py +210 -25
  9. claude_mpm/cli/commands/config.py +88 -2
  10. claude_mpm/cli/commands/configure.py +1097 -158
  11. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  12. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  13. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  14. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  15. claude_mpm/cli/commands/skills.py +21 -2
  16. claude_mpm/cli/commands/summarize.py +413 -0
  17. claude_mpm/cli/executor.py +11 -3
  18. claude_mpm/cli/parsers/base_parser.py +5 -0
  19. claude_mpm/cli/parsers/config_parser.py +153 -83
  20. claude_mpm/cli/parsers/skills_parser.py +3 -2
  21. claude_mpm/cli/startup.py +333 -89
  22. claude_mpm/commands/mpm-config.md +266 -0
  23. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  24. claude_mpm/config/agent_sources.py +27 -0
  25. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  26. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  27. claude_mpm/core/framework_loader.py +4 -2
  28. claude_mpm/core/logger.py +13 -0
  29. claude_mpm/core/socketio_pool.py +3 -3
  30. claude_mpm/core/unified_agent_registry.py +5 -15
  31. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  32. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -78
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  34. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  35. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  36. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  37. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  38. claude_mpm/hooks/memory_integration_hook.py +46 -1
  39. claude_mpm/init.py +0 -19
  40. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  41. claude_mpm/scripts/launch_monitor.py +93 -13
  42. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  43. claude_mpm/services/agents/agent_review_service.py +280 -0
  44. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  45. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  46. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  47. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  48. claude_mpm/services/agents/git_source_manager.py +34 -0
  49. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  50. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  51. claude_mpm/services/agents/toolchain_detector.py +10 -6
  52. claude_mpm/services/analysis/__init__.py +11 -1
  53. claude_mpm/services/analysis/clone_detector.py +1030 -0
  54. claude_mpm/services/command_deployment_service.py +71 -10
  55. claude_mpm/services/event_bus/config.py +3 -1
  56. claude_mpm/services/git/git_operations_service.py +93 -8
  57. claude_mpm/services/monitor/daemon.py +9 -2
  58. claude_mpm/services/monitor/daemon_manager.py +39 -3
  59. claude_mpm/services/monitor/server.py +225 -19
  60. claude_mpm/services/self_upgrade_service.py +120 -12
  61. claude_mpm/services/skills/__init__.py +3 -0
  62. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  63. claude_mpm/services/skills/selective_skill_deployer.py +230 -0
  64. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  65. claude_mpm/services/skills_deployer.py +64 -3
  66. claude_mpm/services/socketio/event_normalizer.py +15 -1
  67. claude_mpm/services/socketio/server/core.py +160 -21
  68. claude_mpm/services/version_control/git_operations.py +103 -0
  69. claude_mpm/utils/agent_filters.py +17 -44
  70. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -84
  71. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +76 -150
  72. claude_mpm-5.4.14.dist-info/entry_points.txt +5 -0
  73. claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
  74. claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
  75. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  76. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  77. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  78. claude_mpm/agents/BASE_OPS.md +0 -219
  79. claude_mpm/agents/BASE_PM.md +0 -480
  80. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  81. claude_mpm/agents/BASE_QA.md +0 -167
  82. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  83. claude_mpm/agents/base_agent.json +0 -31
  84. claude_mpm/agents/base_agent_loader.py +0 -601
  85. claude_mpm/cli/ticket_cli.py +0 -35
  86. claude_mpm/commands/mpm-config-view.md +0 -150
  87. claude_mpm/dashboard/analysis_runner.py +0 -455
  88. claude_mpm/dashboard/index.html +0 -13
  89. claude_mpm/dashboard/open_dashboard.py +0 -66
  90. claude_mpm/dashboard/static/css/activity.css +0 -1958
  91. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  92. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  93. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  94. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  95. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  96. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  97. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  98. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  99. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  100. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  101. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  102. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  103. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  104. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  105. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  106. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  107. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  108. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  109. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  110. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  111. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  112. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  113. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  114. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  115. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  116. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  117. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  118. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  119. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  120. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  121. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  122. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  123. claude_mpm/dashboard/templates/code_simple.html +0 -153
  124. claude_mpm/dashboard/templates/index.html +0 -606
  125. claude_mpm/dashboard/test_dashboard.html +0 -372
  126. claude_mpm/scripts/mcp_server.py +0 -75
  127. claude_mpm/scripts/mcp_wrapper.py +0 -39
  128. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  129. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  130. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  131. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  132. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  133. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  134. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  135. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  136. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  137. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  138. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  139. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  140. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  141. claude_mpm/services/mcp_gateway/main.py +0 -589
  142. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  143. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  144. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  145. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  146. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  147. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  148. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  149. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  150. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  151. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  152. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  153. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  154. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  155. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  156. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  157. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  158. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  159. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  160. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  161. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
  162. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/top_level.txt +0 -0
@@ -82,6 +82,8 @@ class SkillsDeployerService(LoggerMixin):
82
82
  toolchain: Optional[List[str]] = None,
83
83
  categories: Optional[List[str]] = None,
84
84
  force: bool = False,
85
+ selective: bool = True,
86
+ project_root: Optional[Path] = None,
85
87
  ) -> Dict:
86
88
  """Deploy skills from GitHub repository.
87
89
 
@@ -89,14 +91,17 @@ class SkillsDeployerService(LoggerMixin):
89
91
  1. Downloads skills from GitHub collection
90
92
  2. Parses manifest for metadata
91
93
  3. Filters by toolchain and categories
92
- 4. Deploys to ~/.claude/skills/
93
- 5. Warns about Claude Code restart
94
+ 4. (If selective=True) Filters to only agent-referenced skills
95
+ 5. Deploys to ~/.claude/skills/
96
+ 6. Warns about Claude Code restart
94
97
 
95
98
  Args:
96
99
  collection: Collection name to deploy from (default: uses default collection)
97
100
  toolchain: Filter by toolchain (e.g., ['python', 'javascript'])
98
101
  categories: Filter by categories (e.g., ['testing', 'debugging'])
99
102
  force: Overwrite existing skills
103
+ selective: If True, only deploy skills referenced by agents (default)
104
+ project_root: Project root directory (for finding agents, auto-detected if None)
100
105
 
101
106
  Returns:
102
107
  Dict containing:
@@ -107,10 +112,14 @@ class SkillsDeployerService(LoggerMixin):
107
112
  - restart_required: True if Claude Code needs restart
108
113
  - restart_instructions: Message about restarting
109
114
  - collection: Collection name used for deployment
115
+ - selective_mode: True if selective deployment was used
116
+ - total_available: Total skills available before filtering
110
117
 
111
118
  Example:
112
119
  >>> result = deployer.deploy_skills(collection="obra-superpowers")
113
120
  >>> result = deployer.deploy_skills(toolchain=['python']) # Uses default
121
+ >>> # Deploy all skills (not just agent-referenced)
122
+ >>> result = deployer.deploy_skills(selective=False)
114
123
  >>> if result['restart_required']:
115
124
  >>> print(result['restart_instructions'])
116
125
  """
@@ -152,7 +161,7 @@ class SkillsDeployerService(LoggerMixin):
152
161
 
153
162
  self.logger.info(f"Found {len(skills)} skills in repository")
154
163
 
155
- # Step 3: Filter skills
164
+ # Step 3: Filter skills by toolchain and categories
156
165
  filtered_skills = self._filter_skills(skills, toolchain, categories)
157
166
 
158
167
  self.logger.info(
@@ -160,6 +169,56 @@ class SkillsDeployerService(LoggerMixin):
160
169
  f" (toolchain={toolchain}, categories={categories})"
161
170
  )
162
171
 
172
+ # Step 3.5: Apply selective filtering (only agent-referenced skills)
173
+ total_available = len(filtered_skills)
174
+ if selective:
175
+ # Auto-detect project root if not provided
176
+ if project_root is None:
177
+ # Try to find project root by looking for .claude directory
178
+ # Start from current directory and walk up
179
+ current = Path.cwd()
180
+ while current != current.parent:
181
+ if (current / ".claude").exists():
182
+ project_root = current
183
+ break
184
+ current = current.parent
185
+
186
+ if project_root:
187
+ agents_dir = Path(project_root) / ".claude" / "agents"
188
+ else:
189
+ # Fallback to current directory's .claude/agents
190
+ agents_dir = Path.cwd() / ".claude" / "agents"
191
+
192
+ from claude_mpm.services.skills.selective_skill_deployer import (
193
+ get_required_skills_from_agents,
194
+ )
195
+
196
+ required_skill_names = get_required_skills_from_agents(agents_dir)
197
+
198
+ if required_skill_names:
199
+ # Filter to only required skills
200
+ # Match on either 'name' or 'skill_id' field
201
+ filtered_skills = [
202
+ s
203
+ for s in filtered_skills
204
+ if s.get("name") in required_skill_names
205
+ or s.get("skill_id") in required_skill_names
206
+ ]
207
+
208
+ self.logger.info(
209
+ f"Selective deployment: {len(filtered_skills)}/{total_available} skills "
210
+ f"(agent-referenced only)"
211
+ )
212
+ else:
213
+ self.logger.warning(
214
+ f"No skills found in agent frontmatter at {agents_dir}. "
215
+ f"Deploying all {total_available} skills."
216
+ )
217
+ else:
218
+ self.logger.info(
219
+ f"Selective mode disabled: deploying all {total_available} skills"
220
+ )
221
+
163
222
  # Step 4: Deploy skills
164
223
  deployed = []
165
224
  skipped = []
@@ -228,6 +287,8 @@ class SkillsDeployerService(LoggerMixin):
228
287
  "restart_required": restart_required,
229
288
  "restart_instructions": restart_instructions,
230
289
  "collection": collection_name,
290
+ "selective_mode": selective,
291
+ "total_available": total_available,
231
292
  }
232
293
 
233
294
  def list_available_skills(self, collection: Optional[str] = None) -> Dict:
@@ -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]]:
@@ -32,6 +32,7 @@ except ImportError:
32
32
  # Import VersionService for dynamic version retrieval
33
33
  import contextlib
34
34
 
35
+ import claude_mpm
35
36
  from claude_mpm.services.version_service import VersionService
36
37
 
37
38
  from ....core.constants import SystemLimits, TimeoutConfig
@@ -271,7 +272,15 @@ class SocketIOServerCore:
271
272
  """Handle POST /api/events from hook handlers."""
272
273
  try:
273
274
  # Parse JSON payload
274
- event_data = await request.json()
275
+ payload = await request.json()
276
+
277
+ # Extract event data from payload (handles both direct and wrapped formats)
278
+ # ConnectionManagerService sends: {"namespace": "...", "event": "...", "data": {...}}
279
+ # Direct hook events may send data directly
280
+ if "data" in payload and isinstance(payload.get("data"), dict):
281
+ event_data = payload["data"]
282
+ else:
283
+ event_data = payload
275
284
 
276
285
  # Log receipt with more detail
277
286
  event_type = (
@@ -292,38 +301,96 @@ class SocketIOServerCore:
292
301
 
293
302
  normalizer = EventNormalizer()
294
303
 
304
+ # Map hook event names to dashboard subtypes
305
+ # Comprehensive mapping of all known Claude Code hook event types
306
+ subtype_map = {
307
+ # User interaction events
308
+ "UserPromptSubmit": "user_prompt_submit",
309
+ "UserPromptCancel": "user_prompt_cancel",
310
+ # Tool execution events
311
+ "PreToolUse": "pre_tool_use",
312
+ "PostToolUse": "post_tool_use",
313
+ "ToolStart": "tool_start",
314
+ "ToolUse": "tool_use",
315
+ # Assistant events
316
+ "AssistantResponse": "assistant_response",
317
+ # Session lifecycle events
318
+ "Start": "start",
319
+ "Stop": "stop",
320
+ "SessionStart": "session_start",
321
+ # Subagent events
322
+ "SubagentStart": "subagent_start",
323
+ "SubagentStop": "subagent_stop",
324
+ "SubagentEvent": "subagent_event",
325
+ # Task events
326
+ "Task": "task",
327
+ "TaskStart": "task_start",
328
+ "TaskComplete": "task_complete",
329
+ # File operation events
330
+ "FileWrite": "file_write",
331
+ "Write": "write",
332
+ # System events
333
+ "Notification": "notification",
334
+ }
335
+
336
+ # Helper function to convert PascalCase to snake_case
337
+ def to_snake_case(name: str) -> str:
338
+ """Convert PascalCase event names to snake_case.
339
+
340
+ Examples:
341
+ UserPromptSubmit → user_prompt_submit
342
+ PreToolUse → pre_tool_use
343
+ TaskComplete → task_complete
344
+ """
345
+ import re
346
+
347
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
348
+
349
+ # Get hook event name and map to subtype
350
+ hook_event_name = event_data.get("hook_event_name", "unknown")
351
+ subtype = subtype_map.get(
352
+ hook_event_name, to_snake_case(hook_event_name)
353
+ )
354
+
355
+ # Debug log for unmapped events to discover new event types
356
+ if (
357
+ hook_event_name not in subtype_map
358
+ and hook_event_name != "unknown"
359
+ ):
360
+ self.logger.debug(
361
+ f"Unmapped hook event: {hook_event_name} → {subtype}"
362
+ )
363
+
295
364
  # Create the format expected by normalizer
296
365
  raw_event = {
297
366
  "type": "hook",
298
- "subtype": event_data.get("hook_event_name", "unknown")
299
- .lower()
300
- .replace("submit", "")
301
- .replace("use", "_use"),
367
+ "subtype": subtype,
302
368
  "timestamp": event_data.get("timestamp"),
303
369
  "data": event_data.get("hook_input_data", {}),
304
370
  "source": "claude_hooks",
305
371
  "session_id": event_data.get("session_id"),
306
372
  }
307
373
 
308
- # Map hook event names to dashboard subtypes
309
- subtype_map = {
310
- "UserPromptSubmit": "user_prompt",
311
- "PreToolUse": "pre_tool",
312
- "PostToolUse": "post_tool",
313
- "Stop": "stop",
314
- "SubagentStop": "subagent_stop",
315
- "AssistantResponse": "assistant_response",
316
- }
317
- raw_event["subtype"] = subtype_map.get(
318
- event_data.get("hook_event_name"), "unknown"
319
- )
320
-
321
374
  normalized = normalizer.normalize(raw_event, source="hook")
322
375
  event_data = normalized.to_dict()
323
376
  self.logger.debug(
324
377
  f"Normalized event: type={event_data.get('type')}, subtype={event_data.get('subtype')}"
325
378
  )
326
379
 
380
+ # Publish to EventBus for cross-component communication
381
+ # WHY: This allows other parts of the system to react to hook events
382
+ # without coupling to Socket.IO directly
383
+ try:
384
+ from claude_mpm.services.event_bus import EventBus
385
+
386
+ event_bus = EventBus.get_instance()
387
+ event_type = f"hook.{event_data.get('subtype', 'unknown')}"
388
+ event_bus.publish(event_type, event_data)
389
+ self.logger.debug(f"Published to EventBus: {event_type}")
390
+ except Exception as e:
391
+ # Non-fatal: EventBus publication failure shouldn't break event flow
392
+ self.logger.warning(f"Failed to publish to EventBus: {e}")
393
+
327
394
  # Broadcast to all connected dashboard clients via SocketIO
328
395
  if self.sio:
329
396
  # CRITICAL: Use the main server's broadcaster for proper event handling
@@ -390,6 +457,47 @@ class SocketIOServerCore:
390
457
  self.app.router.add_post("/api/events", api_events_handler)
391
458
  self.logger.info("✅ HTTP API endpoint registered at /api/events")
392
459
 
460
+ # Add health check endpoint
461
+ async def health_handler(request):
462
+ """Handle GET /api/health for health checks."""
463
+ try:
464
+ # Get server status
465
+ uptime_seconds = 0
466
+ if self.stats.get("start_time"):
467
+ uptime_seconds = int(
468
+ (
469
+ datetime.now(timezone.utc) - self.stats["start_time"]
470
+ ).total_seconds()
471
+ )
472
+
473
+ health_data = {
474
+ "status": "healthy",
475
+ "service": "claude-mpm-socketio",
476
+ "timestamp": datetime.now(timezone.utc).isoformat(),
477
+ "uptime_seconds": uptime_seconds,
478
+ "connected_clients": len(self.connected_clients),
479
+ "total_events": self.stats.get("events_sent", 0),
480
+ "buffered_events": self.stats.get("events_buffered", 0),
481
+ }
482
+
483
+ return web.json_response(health_data)
484
+ except Exception as e:
485
+ self.logger.error(f"Error in health check: {e}")
486
+ return web.json_response(
487
+ {
488
+ "status": "unhealthy",
489
+ "service": "claude-mpm-socketio",
490
+ "error": str(e),
491
+ },
492
+ status=503,
493
+ )
494
+
495
+ self.app.router.add_get("/api/health", health_handler)
496
+ self.app.router.add_get("/health", health_handler) # Alias for convenience
497
+ self.logger.info(
498
+ "✅ Health check endpoints registered at /api/health and /health"
499
+ )
500
+
393
501
  # Add working directory endpoint
394
502
  async def working_directory_handler(request):
395
503
  """Handle GET /api/working-directory to provide current working directory."""
@@ -591,9 +699,9 @@ class SocketIOServerCore:
591
699
  self.app.router.add_get("/version.json", version_handler)
592
700
 
593
701
  # Serve static assets (CSS, JS) from the dashboard static directory
594
- dashboard_static_path = (
595
- get_project_root() / "src" / "claude_mpm" / "dashboard" / "static"
596
- )
702
+ # Use package-relative path (works for both dev and installed package)
703
+ package_root = Path(claude_mpm.__file__).parent
704
+ dashboard_static_path = package_root / "dashboard" / "static"
597
705
  if dashboard_static_path.exists():
598
706
  self.app.router.add_static(
599
707
  "/static/", dashboard_static_path, name="dashboard_static"
@@ -606,6 +714,37 @@ class SocketIOServerCore:
606
714
  f"⚠️ Static assets directory not found at: {dashboard_static_path}"
607
715
  )
608
716
 
717
+ # Serve Svelte dashboard build
718
+ svelte_build_path = (
719
+ package_root / "dashboard" / "static" / "svelte-build"
720
+ )
721
+ if svelte_build_path.exists():
722
+ # Serve Svelte dashboard at /svelte route
723
+ async def svelte_handler(request):
724
+ svelte_index = svelte_build_path / "index.html"
725
+ if svelte_index.exists():
726
+ self.logger.debug(
727
+ f"Serving Svelte dashboard from: {svelte_index}"
728
+ )
729
+ return web.FileResponse(svelte_index)
730
+ return web.Response(
731
+ text="Svelte dashboard not available", status=404
732
+ )
733
+
734
+ self.app.router.add_get("/svelte", svelte_handler)
735
+
736
+ # Serve Svelte app assets at /_app/ (needed for SvelteKit builds)
737
+ svelte_app_path = svelte_build_path / "_app"
738
+ if svelte_app_path.exists():
739
+ self.app.router.add_static(
740
+ "/_app/", svelte_app_path, name="svelte_app"
741
+ )
742
+ self.logger.info(
743
+ f"✅ Svelte dashboard available at /svelte (build: {svelte_build_path})"
744
+ )
745
+ else:
746
+ self.logger.debug(f"Svelte build not found at: {svelte_build_path}")
747
+
609
748
  else:
610
749
  self.logger.warning("⚠️ No dashboard found, serving fallback response")
611
750
 
@@ -18,6 +18,11 @@ from dataclasses import dataclass, field
18
18
  from datetime import datetime, timezone
19
19
  from typing import Any, Dict, List, Optional
20
20
 
21
+ # Privileged users who can push directly to main branch
22
+ # All other users must use feature branches and PRs
23
+ PRIVILEGED_GIT_USERS = ["bobmatnyc@users.noreply.github.com"]
24
+ PROTECTED_BRANCHES = ["main", "master"]
25
+
21
26
 
22
27
  @dataclass
23
28
  class GitBranchInfo:
@@ -101,6 +106,94 @@ class GitOperationsManager:
101
106
  if not self._is_git_repository():
102
107
  raise GitOperationError(f"Not a Git repository: {project_root}")
103
108
 
109
+ def _get_current_git_user(self) -> str:
110
+ """
111
+ Get the current Git user email.
112
+
113
+ Returns:
114
+ Git user email configured in repository or globally
115
+
116
+ Raises:
117
+ GitOperationError: If git user.email is not configured
118
+ """
119
+ try:
120
+ result = self._run_git_command(["config", "user.email"])
121
+ email = result.stdout.strip()
122
+ if not email:
123
+ raise GitOperationError(
124
+ "Git user.email is not configured. "
125
+ "Please configure it with: git config user.email 'your@email.com'"
126
+ )
127
+ return email
128
+ except GitOperationError as e:
129
+ raise GitOperationError(
130
+ "Git user.email is not configured. "
131
+ "Please configure it with: git config user.email 'your@email.com'"
132
+ ) from e
133
+
134
+ def _is_privileged_user(self) -> bool:
135
+ """
136
+ Check if the current Git user is privileged to push to protected branches.
137
+
138
+ Returns:
139
+ True if user email is in PRIVILEGED_GIT_USERS, False otherwise
140
+ """
141
+ try:
142
+ current_user = self._get_current_git_user()
143
+ return current_user in PRIVILEGED_GIT_USERS
144
+ except GitOperationError:
145
+ # If we can't determine user, assume not privileged
146
+ return False
147
+
148
+ def _enforce_branch_protection(
149
+ self, target_branch: str, operation: str
150
+ ) -> Optional[GitOperationResult]:
151
+ """
152
+ Enforce branch protection rules for protected branches.
153
+
154
+ Args:
155
+ target_branch: Branch being operated on
156
+ operation: Operation being performed (e.g., "push", "merge")
157
+
158
+ Returns:
159
+ GitOperationResult with error if protection violated, None if allowed
160
+ """
161
+ # Check if target branch is protected
162
+ if target_branch not in PROTECTED_BRANCHES:
163
+ return None
164
+
165
+ # Check if user is privileged
166
+ if self._is_privileged_user():
167
+ return None
168
+
169
+ # Get current user for error message
170
+ try:
171
+ current_user = self._get_current_git_user()
172
+ except GitOperationError:
173
+ current_user = "unknown"
174
+
175
+ # Build helpful error message
176
+ error_message = (
177
+ f"Direct {operation} to '{target_branch}' branch is restricted.\n"
178
+ f"Only {', '.join(PRIVILEGED_GIT_USERS)} can {operation} directly to protected branches.\n"
179
+ f"Current user: {current_user}\n\n"
180
+ f"Please use the feature branch workflow:\n"
181
+ f" 1. git checkout -b feature/your-feature-name\n"
182
+ f" 2. Make your changes and commit\n"
183
+ f" 3. git push -u origin feature/your-feature-name\n"
184
+ f" 4. Create a Pull Request on GitHub for review"
185
+ )
186
+
187
+ return GitOperationResult(
188
+ success=False,
189
+ operation=f"{operation}_branch_protection",
190
+ message=f"Branch protection: {operation} to '{target_branch}' denied",
191
+ error=error_message,
192
+ branch_before=self.get_current_branch(),
193
+ branch_after=self.get_current_branch(),
194
+ execution_time=0.0,
195
+ )
196
+
104
197
  def _is_git_repository(self) -> bool:
105
198
  """Check if the directory is a Git repository."""
106
199
  return self.git_dir.exists() and self.git_dir.is_dir()
@@ -503,6 +596,11 @@ class GitOperationsManager:
503
596
  start_time = datetime.now(timezone.utc)
504
597
  current_branch = self.get_current_branch()
505
598
 
599
+ # Enforce branch protection for target branch
600
+ protection_result = self._enforce_branch_protection(target_branch, "merge")
601
+ if protection_result:
602
+ return protection_result
603
+
506
604
  try:
507
605
  # Switch to target branch
508
606
  if current_branch != target_branch:
@@ -659,6 +757,11 @@ class GitOperationsManager:
659
757
  if not branch_name:
660
758
  branch_name = current_branch
661
759
 
760
+ # Enforce branch protection
761
+ protection_result = self._enforce_branch_protection(branch_name, "push")
762
+ if protection_result:
763
+ return protection_result
764
+
662
765
  try:
663
766
  # Build push command
664
767
  push_args = ["push"]
@@ -4,10 +4,14 @@ Agent filtering utilities for claude-mpm.
4
4
  WHY: This module provides centralized filtering logic to remove non-deployable
5
5
  agents (BASE_AGENT) and already-deployed agents from user-facing displays.
6
6
 
7
+ ARCHITECTURE:
8
+ - SOURCE: ~/.claude-mpm/cache/remote-agents/ (git repository cache)
9
+ - DEPLOYMENT: .claude/agents/ (project-level deployment location)
10
+
7
11
  DESIGN DECISIONS:
8
12
  - BASE_AGENT is a build tool, not a deployable agent - filter everywhere
9
- - Deployed agent detection supports both new (.claude-mpm/agents/) and
10
- legacy (.claude/agents/)
13
+ - Deployed agent detection checks .claude/agents/ for all deployed agents
14
+ - Supports both virtual (.mpm_deployment_state) and physical (.md files) detection
11
15
  - Case-insensitive BASE_AGENT detection for robustness
12
16
  - Pure functions for easy testing and reuse
13
17
 
@@ -102,10 +106,11 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
102
106
 
103
107
  Design Rationale:
104
108
  - Primary detection: Virtual deployment state (.mpm_deployment_state)
105
- - Fallback detection: Physical .md files (.claude-mpm/agents/, .claude/agents/)
109
+ - Fallback detection: Physical .md files in .claude/agents/
106
110
  - Returns leaf names for consistent comparison with agent_id formats
107
111
  - Combines both detection methods for complete coverage
108
112
  - Graceful error handling for malformed or missing state files
113
+ - Only checks project-level deployment (simplified architecture)
109
114
 
110
115
  Related:
111
116
  - Fixes checkbox interface showing all agents as "○ [Available]" instead of "● [Installed]"
@@ -115,24 +120,17 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
115
120
  deployed = set()
116
121
 
117
122
  # Track if project_dir was explicitly provided
118
- explicit_project_dir = project_dir is not None
119
123
 
120
124
  if project_dir is None:
121
125
  project_dir = Path.cwd()
122
126
 
123
127
  # NEW: Check virtual deployment state (primary method)
124
128
  # This is the current deployment model used by Claude Code
129
+ # Only checking project-level deployment in simplified architecture
125
130
  deployment_state_paths = [
126
131
  project_dir / ".claude" / "agents" / ".mpm_deployment_state",
127
132
  ]
128
133
 
129
- # Only check user-level state if using default project directory
130
- # This prevents test isolation issues when explicit project_dir is provided
131
- if not explicit_project_dir:
132
- deployment_state_paths.append(
133
- Path.home() / ".claude" / "agents" / ".mpm_deployment_state"
134
- )
135
-
136
134
  for state_path in deployment_state_paths:
137
135
  if state_path.exists():
138
136
  try:
@@ -162,42 +160,17 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
162
160
  continue
163
161
 
164
162
  # EXISTING: Check physical .md files (fallback for backward compatibility)
165
- # Check new architecture
166
- new_agents_dir = project_dir / ".claude-mpm" / "agents"
167
- if new_agents_dir.exists():
168
- for file in new_agents_dir.glob("*.md"):
169
- if file.stem not in {"BASE-AGENT", ".DS_Store"}:
170
- deployed.add(file.stem)
171
-
172
- # Check legacy architecture
173
- legacy_agents_dir = project_dir / ".claude" / "agents"
174
- if legacy_agents_dir.exists():
175
- for file in legacy_agents_dir.glob("*.md"):
163
+ # Check project deployment location (.claude/agents/)
164
+ agents_dir = project_dir / ".claude" / "agents"
165
+ if agents_dir.exists():
166
+ for file in agents_dir.glob("*.md"):
176
167
  if file.stem not in {"BASE-AGENT", ".DS_Store"}:
177
168
  deployed.add(file.stem)
178
169
 
179
- # Check .claude/templates/ directory (where agents are actually deployed)
180
- templates_dir = project_dir / ".claude" / "templates"
181
- if templates_dir.exists():
182
- for file in templates_dir.glob("*.md"):
183
- if file.stem not in {
184
- "BASE-AGENT",
185
- ".DS_Store",
186
- "README",
187
- "circuit-breakers",
188
- }:
189
- # Skip template/example files
190
- if not any(x in file.stem for x in ["example", "template", "pm-"]):
191
- deployed.add(file.stem)
192
-
193
- # Check user-level directory only if using default project directory
194
- # This prevents test isolation issues when explicit project_dir is provided
195
- if not explicit_project_dir:
196
- user_agents_dir = Path.home() / ".claude" / "agents"
197
- if user_agents_dir.exists():
198
- for file in user_agents_dir.glob("*.md"):
199
- if file.stem not in {"BASE-AGENT", ".DS_Store"}:
200
- deployed.add(file.stem)
170
+ # NOTE: .claude/templates/ contains PM instruction templates, NOT deployed agents
171
+ # It should NOT be checked here. Agents are deployed to:
172
+ # - .mpm_deployment_state (virtual deployment)
173
+ # - .claude/agents/*.md (project deployment)
201
174
 
202
175
  return deployed
203
176