claude-mpm 5.1.9__py3-none-any.whl → 5.4.22__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 (176) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  7. claude_mpm/cli/__main__.py +4 -0
  8. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  9. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  10. claude_mpm/cli/commands/agents.py +0 -31
  11. claude_mpm/cli/commands/auto_configure.py +210 -25
  12. claude_mpm/cli/commands/config.py +88 -2
  13. claude_mpm/cli/commands/configure.py +1097 -158
  14. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  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/skills.py +214 -189
  19. claude_mpm/cli/commands/summarize.py +413 -0
  20. claude_mpm/cli/executor.py +11 -3
  21. claude_mpm/cli/parsers/agents_parser.py +0 -9
  22. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/config_parser.py +153 -83
  25. claude_mpm/cli/parsers/skills_parser.py +3 -2
  26. claude_mpm/cli/startup.py +550 -94
  27. claude_mpm/commands/mpm-config.md +265 -0
  28. claude_mpm/commands/mpm-help.md +14 -95
  29. claude_mpm/commands/mpm-organize.md +500 -0
  30. claude_mpm/config/agent_sources.py +27 -0
  31. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  32. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  33. claude_mpm/core/framework_loader.py +4 -2
  34. claude_mpm/core/logger.py +13 -0
  35. claude_mpm/core/socketio_pool.py +3 -3
  36. claude_mpm/core/unified_agent_registry.py +5 -15
  37. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  38. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  39. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  40. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  41. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  42. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  43. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  44. claude_mpm/hooks/memory_integration_hook.py +46 -1
  45. claude_mpm/init.py +0 -19
  46. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  47. claude_mpm/scripts/launch_monitor.py +93 -13
  48. claude_mpm/scripts/start_activity_logging.py +0 -0
  49. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  50. claude_mpm/services/agents/agent_review_service.py +280 -0
  51. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  52. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  53. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  54. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  55. claude_mpm/services/agents/git_source_manager.py +34 -0
  56. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  57. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  58. claude_mpm/services/agents/toolchain_detector.py +10 -6
  59. claude_mpm/services/analysis/__init__.py +11 -1
  60. claude_mpm/services/analysis/clone_detector.py +1030 -0
  61. claude_mpm/services/command_deployment_service.py +81 -10
  62. claude_mpm/services/event_bus/config.py +3 -1
  63. claude_mpm/services/git/git_operations_service.py +93 -8
  64. claude_mpm/services/monitor/daemon.py +9 -2
  65. claude_mpm/services/monitor/daemon_manager.py +39 -3
  66. claude_mpm/services/monitor/server.py +225 -19
  67. claude_mpm/services/self_upgrade_service.py +120 -12
  68. claude_mpm/services/skills/__init__.py +3 -0
  69. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  70. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  71. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  72. claude_mpm/services/skills_deployer.py +126 -9
  73. claude_mpm/services/socketio/event_normalizer.py +15 -1
  74. claude_mpm/services/socketio/server/core.py +160 -21
  75. claude_mpm/services/version_control/git_operations.py +103 -0
  76. claude_mpm/utils/agent_filters.py +17 -44
  77. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  78. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +82 -161
  79. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  80. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  81. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  82. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  83. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  84. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  85. claude_mpm/agents/BASE_OPS.md +0 -219
  86. claude_mpm/agents/BASE_PM.md +0 -480
  87. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  88. claude_mpm/agents/BASE_QA.md +0 -167
  89. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  90. claude_mpm/agents/base_agent.json +0 -31
  91. claude_mpm/agents/base_agent_loader.py +0 -601
  92. claude_mpm/cli/commands/agents_detect.py +0 -380
  93. claude_mpm/cli/commands/agents_recommend.py +0 -309
  94. claude_mpm/cli/ticket_cli.py +0 -35
  95. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  96. claude_mpm/commands/mpm-agents-detect.md +0 -177
  97. claude_mpm/commands/mpm-agents-list.md +0 -131
  98. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  99. claude_mpm/commands/mpm-config-view.md +0 -150
  100. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  101. claude_mpm/dashboard/analysis_runner.py +0 -455
  102. claude_mpm/dashboard/index.html +0 -13
  103. claude_mpm/dashboard/open_dashboard.py +0 -66
  104. claude_mpm/dashboard/static/css/activity.css +0 -1958
  105. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  106. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  107. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  108. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  109. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  110. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  111. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  112. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  113. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  114. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  115. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  116. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  117. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  118. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  119. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  120. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  121. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  122. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  123. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  124. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  125. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  126. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  127. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  128. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  129. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  130. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  131. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  132. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  133. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  134. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  135. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  136. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  137. claude_mpm/dashboard/templates/code_simple.html +0 -153
  138. claude_mpm/dashboard/templates/index.html +0 -606
  139. claude_mpm/dashboard/test_dashboard.html +0 -372
  140. claude_mpm/scripts/mcp_server.py +0 -75
  141. claude_mpm/scripts/mcp_wrapper.py +0 -39
  142. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  143. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  144. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  145. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  146. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  147. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  148. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  149. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  150. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  151. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  152. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  153. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  154. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  155. claude_mpm/services/mcp_gateway/main.py +0 -589
  156. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  157. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  158. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  159. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  160. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  161. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  162. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  163. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  164. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  165. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  166. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  167. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  168. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  169. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  170. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  171. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  172. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  173. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  174. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  175. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  176. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
@@ -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