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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_state_manager.py +67 -23
- claude_mpm/cli/commands/agents.py +446 -25
- claude_mpm/cli/commands/auto_configure.py +535 -233
- claude_mpm/cli/commands/configure.py +1500 -147
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +1 -39
- claude_mpm/cli/commands/skills.py +322 -19
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/interactive/agent_wizard.py +302 -195
- claude_mpm/cli/parsers/agents_parser.py +137 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +9 -0
- claude_mpm/cli/parsers/skills_parser.py +7 -0
- claude_mpm/cli/startup.py +133 -85
- claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
- claude_mpm/commands/mpm-agents-list.md +2 -2
- claude_mpm/commands/mpm-config-view.md +2 -2
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +2 -2
- claude_mpm/commands/mpm-ticket-view.md +2 -2
- claude_mpm/config/agent_presets.py +312 -82
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -25
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/interactive_session.py +19 -5
- claude_mpm/core/oneshot_session.py +16 -4
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +35 -0
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/command_deployment_service.py +106 -5
- claude_mpm/services/core/base.py +7 -2
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/monitor/daemon.py +37 -10
- claude_mpm/services/monitor/daemon_manager.py +134 -21
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/runner_configuration_service.py +16 -3
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +261 -0
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +5 -1
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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":
|
|
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
|
-
|
|
595
|
-
|
|
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
|
claude_mpm/utils/gitignore.py
CHANGED
|
@@ -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)
|