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

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

Potentially problematic release.


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

Files changed (184) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
  4. claude_mpm/agents/agent_loader.py +10 -17
  5. claude_mpm/agents/base_agent_loader.py +10 -35
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  8. claude_mpm/cli/__init__.py +0 -1
  9. claude_mpm/cli/commands/__init__.py +2 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +67 -23
  11. claude_mpm/cli/commands/agents.py +446 -25
  12. claude_mpm/cli/commands/auto_configure.py +535 -233
  13. claude_mpm/cli/commands/configure.py +1500 -147
  14. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/postmortem.py +401 -0
  19. claude_mpm/cli/commands/run.py +1 -39
  20. claude_mpm/cli/commands/skills.py +322 -19
  21. claude_mpm/cli/commands/summarize.py +413 -0
  22. claude_mpm/cli/executor.py +8 -0
  23. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  24. claude_mpm/cli/parsers/agents_parser.py +137 -0
  25. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  26. claude_mpm/cli/parsers/base_parser.py +9 -0
  27. claude_mpm/cli/parsers/skills_parser.py +7 -0
  28. claude_mpm/cli/startup.py +133 -85
  29. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  30. claude_mpm/commands/mpm-agents-list.md +2 -2
  31. claude_mpm/commands/mpm-config-view.md +2 -2
  32. claude_mpm/commands/mpm-help.md +3 -0
  33. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  34. claude_mpm/commands/mpm-postmortem.md +123 -0
  35. claude_mpm/commands/mpm-session-resume.md +2 -2
  36. claude_mpm/commands/mpm-ticket-view.md +2 -2
  37. claude_mpm/config/agent_presets.py +312 -82
  38. claude_mpm/config/agent_sources.py +27 -0
  39. claude_mpm/config/skill_presets.py +392 -0
  40. claude_mpm/constants.py +1 -0
  41. claude_mpm/core/claude_runner.py +2 -25
  42. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  43. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  44. claude_mpm/core/interactive_session.py +19 -5
  45. claude_mpm/core/oneshot_session.py +16 -4
  46. claude_mpm/core/output_style_manager.py +173 -43
  47. claude_mpm/core/protocols/__init__.py +23 -0
  48. claude_mpm/core/protocols/runner_protocol.py +103 -0
  49. claude_mpm/core/protocols/session_protocol.py +131 -0
  50. claude_mpm/core/shared/singleton_manager.py +11 -4
  51. claude_mpm/core/socketio_pool.py +3 -3
  52. claude_mpm/core/system_context.py +38 -0
  53. claude_mpm/core/unified_agent_registry.py +134 -16
  54. claude_mpm/core/unified_config.py +22 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  63. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  64. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  65. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  72. claude_mpm/models/agent_definition.py +7 -0
  73. claude_mpm/scripts/launch_monitor.py +93 -13
  74. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  75. claude_mpm/services/agents/cache_git_manager.py +621 -0
  76. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  77. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  78. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
  79. claude_mpm/services/agents/git_source_manager.py +20 -0
  80. claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
  81. claude_mpm/services/agents/toolchain_detector.py +6 -5
  82. claude_mpm/services/analysis/__init__.py +35 -0
  83. claude_mpm/services/analysis/clone_detector.py +1030 -0
  84. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  85. claude_mpm/services/analysis/postmortem_service.py +765 -0
  86. claude_mpm/services/command_deployment_service.py +106 -5
  87. claude_mpm/services/core/base.py +7 -2
  88. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  89. claude_mpm/services/event_bus/config.py +3 -1
  90. claude_mpm/services/git/git_operations_service.py +8 -8
  91. claude_mpm/services/mcp_config_manager.py +75 -145
  92. claude_mpm/services/mcp_service_verifier.py +6 -3
  93. claude_mpm/services/monitor/daemon.py +37 -10
  94. claude_mpm/services/monitor/daemon_manager.py +134 -21
  95. claude_mpm/services/monitor/server.py +225 -19
  96. claude_mpm/services/project/project_organizer.py +4 -0
  97. claude_mpm/services/runner_configuration_service.py +16 -3
  98. claude_mpm/services/session_management_service.py +16 -4
  99. claude_mpm/services/socketio/event_normalizer.py +15 -1
  100. claude_mpm/services/socketio/server/core.py +160 -21
  101. claude_mpm/services/version_control/git_operations.py +103 -0
  102. claude_mpm/utils/agent_filters.py +261 -0
  103. claude_mpm/utils/gitignore.py +3 -0
  104. claude_mpm/utils/migration.py +372 -0
  105. claude_mpm/utils/progress.py +5 -1
  106. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
  107. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
  108. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  109. claude_mpm/dashboard/analysis_runner.py +0 -455
  110. claude_mpm/dashboard/index.html +0 -13
  111. claude_mpm/dashboard/open_dashboard.py +0 -66
  112. claude_mpm/dashboard/static/css/activity.css +0 -1958
  113. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  114. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  115. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  116. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  117. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  118. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  119. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  120. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  121. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  122. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  123. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  124. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  125. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  126. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  127. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  128. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  129. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  130. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  131. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  132. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  133. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  134. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  135. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  136. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  137. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  138. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  139. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  140. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  141. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  142. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  143. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  144. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  145. claude_mpm/dashboard/templates/code_simple.html +0 -153
  146. claude_mpm/dashboard/templates/index.html +0 -606
  147. claude_mpm/dashboard/test_dashboard.html +0 -372
  148. claude_mpm/scripts/mcp_server.py +0 -75
  149. claude_mpm/scripts/mcp_wrapper.py +0 -39
  150. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  151. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  152. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  153. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  154. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  155. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  156. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  157. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  158. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  159. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  160. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
  161. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  162. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  163. claude_mpm/services/mcp_gateway/main.py +0 -589
  164. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  165. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  166. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  167. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  168. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  169. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  170. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  171. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  172. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  173. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  174. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  175. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  176. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  177. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  178. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  179. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  180. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  181. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  182. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  183. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  184. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -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"]
@@ -0,0 +1,261 @@
1
+ """
2
+ Agent filtering utilities for claude-mpm.
3
+
4
+ WHY: This module provides centralized filtering logic to remove non-deployable
5
+ agents (BASE_AGENT) and already-deployed agents from user-facing displays.
6
+
7
+ ARCHITECTURE:
8
+ - SOURCE: ~/.claude-mpm/cache/remote-agents/ (git repository cache)
9
+ - DEPLOYMENT: .claude/agents/ (project-level deployment location)
10
+
11
+ DESIGN DECISIONS:
12
+ - BASE_AGENT is a build tool, not a deployable agent - filter everywhere
13
+ - Deployed agent detection checks .claude/agents/ for all deployed agents
14
+ - Supports both virtual (.mpm_deployment_state) and physical (.md files) detection
15
+ - Case-insensitive BASE_AGENT detection for robustness
16
+ - Pure functions for easy testing and reuse
17
+
18
+ IMPLEMENTATION NOTES:
19
+ - Related to ticket 1M-502 Phase 1: UX improvements for agent filtering
20
+ - Addresses user confusion from seeing BASE_AGENT and deployed agents in lists
21
+ """
22
+
23
+ from pathlib import Path
24
+ from typing import Dict, List, Optional, Set
25
+
26
+
27
+ def is_base_agent(agent_id: str) -> bool:
28
+ """Check if agent is BASE_AGENT (build tool, not deployable).
29
+
30
+ BASE_AGENT is an internal build tool used to construct other agents.
31
+ It should never appear in user-facing agent lists or deployment menus.
32
+
33
+ Args:
34
+ agent_id: Agent identifier to check (may include path like "qa/BASE-AGENT")
35
+
36
+ Returns:
37
+ True if agent is BASE_AGENT (case-insensitive), False otherwise
38
+
39
+ Examples:
40
+ >>> is_base_agent("BASE_AGENT")
41
+ True
42
+ >>> is_base_agent("base-agent")
43
+ True
44
+ >>> is_base_agent("qa/BASE-AGENT")
45
+ True
46
+ >>> is_base_agent("ENGINEER")
47
+ False
48
+ """
49
+ if not agent_id:
50
+ return False
51
+
52
+ # Extract filename from path (handle cases like "qa/BASE-AGENT")
53
+ # 1M-502: Remote agents may have path prefixes like "qa/", "pm/", etc.
54
+ agent_name = agent_id.split("/")[-1]
55
+
56
+ normalized_id = agent_name.lower().replace("-", "").replace("_", "")
57
+ return normalized_id == "baseagent"
58
+
59
+
60
+ def filter_base_agents(agents: List[Dict]) -> List[Dict]:
61
+ """Remove BASE_AGENT from agent list.
62
+
63
+ Filters out any agent with agent_id matching BASE_AGENT (case-insensitive).
64
+ This prevents users from seeing or selecting the internal build tool.
65
+
66
+ Args:
67
+ agents: List of agent dictionaries, each containing at least 'agent_id' key
68
+
69
+ Returns:
70
+ Filtered list with BASE_AGENT removed
71
+
72
+ Examples:
73
+ >>> agents = [
74
+ ... {"agent_id": "ENGINEER", "name": "Engineer"},
75
+ ... {"agent_id": "BASE_AGENT", "name": "Base Agent"},
76
+ ... {"agent_id": "PM", "name": "PM"}
77
+ ... ]
78
+ >>> filtered = filter_base_agents(agents)
79
+ >>> len(filtered)
80
+ 2
81
+ >>> "BASE_AGENT" in [a["agent_id"] for a in filtered]
82
+ False
83
+ """
84
+ return [a for a in agents if not is_base_agent(a.get("agent_id", ""))]
85
+
86
+
87
+ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
88
+ """Get set of currently deployed agent IDs.
89
+
90
+ Checks virtual deployment state (.mpm_deployment_state) first, then falls back
91
+ to physical .md files for backward compatibility. This ensures agents are detected
92
+ whether deployed virtually or as physical files.
93
+
94
+ Args:
95
+ project_dir: Project directory to check, defaults to current working directory
96
+
97
+ Returns:
98
+ Set of deployed agent IDs (leaf names like "python-engineer", "qa")
99
+
100
+ Examples:
101
+ >>> deployed = get_deployed_agent_ids()
102
+ >>> "python-engineer" in deployed # If agent exists in deployment state
103
+ True
104
+ >>> "ENGINEER" in deployed # If ENGINEER.md exists
105
+ True
106
+
107
+ Design Rationale:
108
+ - Primary detection: Virtual deployment state (.mpm_deployment_state)
109
+ - Fallback detection: Physical .md files in .claude/agents/
110
+ - Returns leaf names for consistent comparison with agent_id formats
111
+ - Combines both detection methods for complete coverage
112
+ - Graceful error handling for malformed or missing state files
113
+ - Only checks project-level deployment (simplified architecture)
114
+
115
+ Related:
116
+ - Fixes checkbox interface showing all agents as "○ [Available]" instead of "● [Installed]"
117
+ - Matches detection logic from _is_agent_deployed() in agent_state_manager.py
118
+ - Related to ticket 1M-502: Virtual deployment state detection
119
+ """
120
+ deployed = set()
121
+
122
+ # Track if project_dir was explicitly provided
123
+
124
+ if project_dir is None:
125
+ project_dir = Path.cwd()
126
+
127
+ # NEW: Check virtual deployment state (primary method)
128
+ # This is the current deployment model used by Claude Code
129
+ # Only checking project-level deployment in simplified architecture
130
+ deployment_state_paths = [
131
+ project_dir / ".claude" / "agents" / ".mpm_deployment_state",
132
+ ]
133
+
134
+ for state_path in deployment_state_paths:
135
+ if state_path.exists():
136
+ try:
137
+ import json
138
+
139
+ with state_path.open() as f:
140
+ state = json.load(f)
141
+
142
+ # Extract agent IDs from deployment state
143
+ # Agent IDs are leaf names (e.g., "python-engineer", "qa")
144
+ agents = state.get("last_check_results", {}).get("agents", {})
145
+ deployed.update(agents.keys())
146
+
147
+ except (json.JSONDecodeError, KeyError) as e:
148
+ # Log error but continue - don't break if state file is malformed
149
+ import logging
150
+
151
+ logger = logging.getLogger(__name__)
152
+ logger.debug(f"Failed to read deployment state from {state_path}: {e}")
153
+ continue
154
+ except Exception as e:
155
+ # Catch unexpected errors - fail gracefully
156
+ import logging
157
+
158
+ logger = logging.getLogger(__name__)
159
+ logger.debug(f"Unexpected error reading deployment state: {e}")
160
+ continue
161
+
162
+ # EXISTING: Check physical .md files (fallback for backward compatibility)
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"):
167
+ if file.stem not in {"BASE-AGENT", ".DS_Store"}:
168
+ deployed.add(file.stem)
169
+
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)
174
+
175
+ return deployed
176
+
177
+
178
+ def filter_deployed_agents(
179
+ agents: List[Dict], project_dir: Optional[Path] = None
180
+ ) -> List[Dict]:
181
+ """Remove already-deployed agents from list.
182
+
183
+ Filters agent list to show only agents that are not currently deployed.
184
+ This prevents users from attempting to re-deploy existing agents and
185
+ reduces confusion in deployment menus.
186
+
187
+ Args:
188
+ agents: List of agent dictionaries, each containing at least 'agent_id' key
189
+ project_dir: Project directory to check, defaults to current working directory
190
+
191
+ Returns:
192
+ Filtered list containing only non-deployed agents
193
+
194
+ Examples:
195
+ >>> agents = [
196
+ ... {"agent_id": "ENGINEER", "name": "Engineer"},
197
+ ... {"agent_id": "PM", "name": "PM"},
198
+ ... {"agent_id": "QA", "name": "QA"}
199
+ ... ]
200
+ >>> # Assuming ENGINEER is deployed
201
+ >>> filtered = filter_deployed_agents(agents)
202
+ >>> "ENGINEER" not in [a["agent_id"] for a in filtered]
203
+ True
204
+
205
+ Design Rationale:
206
+ - Checks filesystem for actual deployed files (source of truth)
207
+ - Supports both new and legacy agent directory structures
208
+ - Preserves agent order for consistent UX
209
+ """
210
+ deployed_ids = get_deployed_agent_ids(project_dir)
211
+ return [a for a in agents if a.get("agent_id") not in deployed_ids]
212
+
213
+
214
+ def apply_all_filters(
215
+ agents: List[Dict],
216
+ project_dir: Optional[Path] = None,
217
+ filter_base: bool = True,
218
+ filter_deployed: bool = False,
219
+ ) -> List[Dict]:
220
+ """Apply multiple filters to agent list in correct order.
221
+
222
+ Convenience function to apply common filtering combinations. Filters are
223
+ applied in this order:
224
+ 1. BASE_AGENT filtering (if enabled)
225
+ 2. Deployed agent filtering (if enabled)
226
+
227
+ Args:
228
+ agents: List of agent dictionaries to filter
229
+ project_dir: Project directory for deployment checks
230
+ filter_base: Remove BASE_AGENT from list (default: True)
231
+ filter_deployed: Remove deployed agents from list (default: False)
232
+
233
+ Returns:
234
+ Filtered agent list
235
+
236
+ Examples:
237
+ >>> agents = get_all_agents()
238
+ >>> # For display/info purposes - remove only BASE_AGENT
239
+ >>> filtered = apply_all_filters(
240
+ ... agents, filter_base=True, filter_deployed=False
241
+ ... )
242
+ >>> # For deployment menus - remove BASE_AGENT and deployed agents
243
+ >>> deployable = apply_all_filters(
244
+ ... agents, filter_base=True, filter_deployed=True
245
+ ... )
246
+
247
+ Usage Guidelines:
248
+ - Use filter_base=True (default) for all user-facing displays
249
+ - Use filter_deployed=True when showing deployment options
250
+ - Use filter_deployed=False when showing all available agents
251
+ (info/list commands)
252
+ """
253
+ result = agents
254
+
255
+ if filter_base:
256
+ result = filter_base_agents(result)
257
+
258
+ if filter_deployed:
259
+ result = filter_deployed_agents(result, project_dir)
260
+
261
+ return result
@@ -212,6 +212,9 @@ def ensure_claude_mpm_gitignore(project_dir: str = ".") -> dict:
212
212
  entries_to_add = [
213
213
  ".claude-mpm/",
214
214
  ".claude/agents/",
215
+ ".mcp.json",
216
+ ".claude.json",
217
+ ".claude/",
215
218
  ]
216
219
 
217
220
  added, existing = manager.ensure_entries(entries_to_add)