claude-mpm 4.15.2__py3-none-any.whl → 4.15.6__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 (85) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  3. claude_mpm/agents/templates/api_qa.json +7 -1
  4. claude_mpm/agents/templates/clerk-ops.json +8 -1
  5. claude_mpm/agents/templates/code_analyzer.json +4 -1
  6. claude_mpm/agents/templates/dart_engineer.json +11 -1
  7. claude_mpm/agents/templates/data_engineer.json +11 -1
  8. claude_mpm/agents/templates/documentation.json +6 -1
  9. claude_mpm/agents/templates/engineer.json +13 -0
  10. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  11. claude_mpm/agents/templates/golang_engineer.json +11 -1
  12. claude_mpm/agents/templates/java_engineer.json +12 -2
  13. claude_mpm/agents/templates/local_ops_agent.json +216 -37
  14. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  15. claude_mpm/agents/templates/ops.json +8 -1
  16. claude_mpm/agents/templates/php-engineer.json +11 -1
  17. claude_mpm/agents/templates/project_organizer.json +9 -2
  18. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  19. claude_mpm/agents/templates/python_engineer.json +11 -1
  20. claude_mpm/agents/templates/qa.json +7 -1
  21. claude_mpm/agents/templates/react_engineer.json +11 -1
  22. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  23. claude_mpm/agents/templates/research.json +4 -1
  24. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  25. claude_mpm/agents/templates/rust_engineer.json +11 -1
  26. claude_mpm/agents/templates/security.json +6 -1
  27. claude_mpm/agents/templates/ticketing.json +6 -1
  28. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  29. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  30. claude_mpm/agents/templates/version_control.json +8 -1
  31. claude_mpm/agents/templates/web_qa.json +7 -1
  32. claude_mpm/agents/templates/web_ui.json +11 -1
  33. claude_mpm/cli/commands/configure.py +164 -16
  34. claude_mpm/cli/commands/configure_agent_display.py +6 -6
  35. claude_mpm/cli/commands/configure_behavior_manager.py +8 -8
  36. claude_mpm/cli/commands/configure_navigation.py +20 -18
  37. claude_mpm/cli/commands/configure_startup_manager.py +14 -14
  38. claude_mpm/cli/commands/configure_template_editor.py +8 -8
  39. claude_mpm/cli/interactive/__init__.py +3 -0
  40. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  41. claude_mpm/cli/startup.py +26 -0
  42. claude_mpm/core/enums.py +18 -0
  43. claude_mpm/core/types.py +2 -9
  44. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  45. claude_mpm/dashboard/templates/index.html +3 -41
  46. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  47. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  48. claude_mpm/services/core/models/health.py +1 -28
  49. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  50. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  51. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  52. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  53. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  54. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  55. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  56. claude_mpm/services/local_ops/__init__.py +1 -1
  57. claude_mpm/services/local_ops/crash_detector.py +1 -1
  58. claude_mpm/services/local_ops/health_checks/http_check.py +2 -1
  59. claude_mpm/services/local_ops/health_checks/process_check.py +2 -1
  60. claude_mpm/services/local_ops/health_checks/resource_check.py +2 -1
  61. claude_mpm/services/local_ops/health_manager.py +1 -1
  62. claude_mpm/services/local_ops/restart_manager.py +1 -1
  63. claude_mpm/services/shared/async_service_base.py +16 -27
  64. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  65. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  66. claude_mpm/services/socketio/handlers/hook.py +10 -0
  67. claude_mpm/services/socketio/handlers/registry.py +4 -2
  68. claude_mpm/services/socketio/server/main.py +7 -7
  69. claude_mpm/skills/__init__.py +21 -0
  70. claude_mpm/skills/bundled/__init__.py +6 -0
  71. claude_mpm/skills/registry.py +198 -0
  72. claude_mpm/skills/skill_manager.py +310 -0
  73. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/METADATA +1 -1
  74. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/RECORD +78 -80
  75. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  76. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  77. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  78. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  79. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  80. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  81. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  82. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/WHEEL +0 -0
  83. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/entry_points.txt +0 -0
  84. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/licenses/LICENSE +0 -0
  85. {claude_mpm-4.15.2.dist-info → claude_mpm-4.15.6.dist-info}/top_level.txt +0 -0
@@ -4,23 +4,12 @@ Base class for asynchronous services to reduce duplication.
4
4
 
5
5
  import asyncio
6
6
  from abc import ABC, abstractmethod
7
- from enum import Enum
8
7
  from typing import Any, Dict, Optional
9
8
 
9
+ from ...core.enums import ServiceState
10
10
  from ...core.mixins import LoggerMixin
11
11
 
12
12
 
13
- class AsyncServiceState(Enum):
14
- """Standard states for async services."""
15
-
16
- UNINITIALIZED = "uninitialized"
17
- INITIALIZING = "initializing"
18
- RUNNING = "running"
19
- STOPPING = "stopping"
20
- STOPPED = "stopped"
21
- ERROR = "error"
22
-
23
-
24
13
  class AsyncServiceBase(LoggerMixin, ABC):
25
14
  """
26
15
  Base class for asynchronous services.
@@ -45,7 +34,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
45
34
  self.config = config or {}
46
35
 
47
36
  # State management
48
- self._state = AsyncServiceState.UNINITIALIZED
37
+ self._state = ServiceState.UNINITIALIZED
49
38
  self._state_lock = asyncio.Lock()
50
39
 
51
40
  # Background tasks
@@ -57,19 +46,19 @@ class AsyncServiceBase(LoggerMixin, ABC):
57
46
  self._error_count = 0
58
47
 
59
48
  @property
60
- def state(self) -> AsyncServiceState:
49
+ def state(self) -> ServiceState:
61
50
  """Get current service state."""
62
51
  return self._state
63
52
 
64
53
  @property
65
54
  def is_running(self) -> bool:
66
55
  """Check if service is running."""
67
- return self._state == AsyncServiceState.RUNNING
56
+ return self._state == ServiceState.RUNNING
68
57
 
69
58
  @property
70
59
  def is_healthy(self) -> bool:
71
60
  """Check if service is healthy."""
72
- return self._state == AsyncServiceState.RUNNING and self._last_error is None
61
+ return self._state == ServiceState.RUNNING and self._last_error is None
73
62
 
74
63
  async def initialize(self) -> bool:
75
64
  """
@@ -79,22 +68,22 @@ class AsyncServiceBase(LoggerMixin, ABC):
79
68
  True if initialization successful
80
69
  """
81
70
  async with self._state_lock:
82
- if self._state != AsyncServiceState.UNINITIALIZED:
71
+ if self._state != ServiceState.UNINITIALIZED:
83
72
  self.logger.warning(f"Service {self.service_name} already initialized")
84
- return self._state == AsyncServiceState.RUNNING
73
+ return self._state == ServiceState.RUNNING
85
74
 
86
- self._state = AsyncServiceState.INITIALIZING
75
+ self._state = ServiceState.INITIALIZING
87
76
  self.logger.info(f"Initializing service: {self.service_name}")
88
77
 
89
78
  try:
90
79
  success = await self._do_initialize()
91
80
  if success:
92
- self._state = AsyncServiceState.RUNNING
81
+ self._state = ServiceState.RUNNING
93
82
  self.logger.info(
94
83
  f"Service {self.service_name} initialized successfully"
95
84
  )
96
85
  else:
97
- self._state = AsyncServiceState.ERROR
86
+ self._state = ServiceState.ERROR
98
87
  self.logger.error(
99
88
  f"Service {self.service_name} initialization failed"
100
89
  )
@@ -102,7 +91,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
102
91
  return success
103
92
 
104
93
  except Exception as e:
105
- self._state = AsyncServiceState.ERROR
94
+ self._state = ServiceState.ERROR
106
95
  self._last_error = e
107
96
  self._error_count += 1
108
97
  self.logger.error(
@@ -114,10 +103,10 @@ class AsyncServiceBase(LoggerMixin, ABC):
114
103
  async def shutdown(self) -> None:
115
104
  """Shutdown the service gracefully."""
116
105
  async with self._state_lock:
117
- if self._state in (AsyncServiceState.STOPPED, AsyncServiceState.STOPPING):
106
+ if self._state in (ServiceState.STOPPED, ServiceState.STOPPING):
118
107
  return
119
108
 
120
- self._state = AsyncServiceState.STOPPING
109
+ self._state = ServiceState.STOPPING
121
110
  self.logger.info(f"Shutting down service: {self.service_name}")
122
111
 
123
112
  try:
@@ -130,11 +119,11 @@ class AsyncServiceBase(LoggerMixin, ABC):
130
119
  # Service-specific shutdown
131
120
  await self._do_shutdown()
132
121
 
133
- self._state = AsyncServiceState.STOPPED
122
+ self._state = ServiceState.STOPPED
134
123
  self.logger.info(f"Service {self.service_name} shut down successfully")
135
124
 
136
125
  except Exception as e:
137
- self._state = AsyncServiceState.ERROR
126
+ self._state = ServiceState.ERROR
138
127
  self._last_error = e
139
128
  self.logger.error(
140
129
  f"Service {self.service_name} shutdown error: {e}", exc_info=True
@@ -146,7 +135,7 @@ class AsyncServiceBase(LoggerMixin, ABC):
146
135
  await self.shutdown()
147
136
 
148
137
  # Reset state for restart
149
- self._state = AsyncServiceState.UNINITIALIZED
138
+ self._state = ServiceState.UNINITIALIZED
150
139
  self._shutdown_event.clear()
151
140
  self._last_error = None
152
141
 
@@ -4,25 +4,12 @@ Base class for services with complex lifecycle management.
4
4
 
5
5
  import time
6
6
  from abc import ABC, abstractmethod
7
- from enum import Enum
8
7
  from typing import Any, Dict, List, Optional
9
8
 
9
+ from ...core.enums import ServiceState
10
10
  from ...core.mixins import LoggerMixin
11
11
 
12
12
 
13
- class ServiceState(Enum):
14
- """Standard service states."""
15
-
16
- UNINITIALIZED = "uninitialized"
17
- INITIALIZING = "initializing"
18
- INITIALIZED = "initialized"
19
- STARTING = "starting"
20
- RUNNING = "running"
21
- STOPPING = "stopping"
22
- STOPPED = "stopped"
23
- ERROR = "error"
24
-
25
-
26
13
  class LifecycleServiceBase(LoggerMixin, ABC):
27
14
  """
28
15
  Base class for services with complex lifecycle management.
@@ -7,7 +7,9 @@ and maintainability.
7
7
  """
8
8
 
9
9
  from .base import BaseEventHandler
10
- from .code_analysis import CodeAnalysisEventHandler
10
+
11
+ # DISABLED: File Tree interface removed from dashboard
12
+ # from .code_analysis import CodeAnalysisEventHandler
11
13
  from .connection import ConnectionEventHandler
12
14
  from .file import FileEventHandler
13
15
  from .git import GitEventHandler
@@ -17,7 +19,8 @@ from .registry import EventHandlerRegistry
17
19
 
18
20
  __all__ = [
19
21
  "BaseEventHandler",
20
- "CodeAnalysisEventHandler",
22
+ # DISABLED: File Tree interface removed from dashboard
23
+ # "CodeAnalysisEventHandler",
21
24
  "ConnectionEventHandler",
22
25
  "EventHandlerRegistry",
23
26
  "FileEventHandler",
@@ -61,6 +61,10 @@ class HookEventHandler(BaseEventHandler):
61
61
 
62
62
  hook_data = data.get("data", {})
63
63
 
64
+ # Log hook event processing
65
+ tool_name = hook_data.get("tool_name", "N/A")
66
+ self.logger.info(f"Processing hook event: {hook_event} - tool: {tool_name}")
67
+
64
68
  # Create properly formatted event for history
65
69
  # Note: add_to_history expects the event data directly, not wrapped
66
70
  history_event = {
@@ -77,6 +81,12 @@ class HookEventHandler(BaseEventHandler):
77
81
 
78
82
  # Broadcast the original event to all connected clients
79
83
  # (preserves all original fields)
84
+ connected_clients = (
85
+ len(self.server.clients) if hasattr(self.server, "clients") else 0
86
+ )
87
+ self.logger.info(
88
+ f"Broadcasting claude_event to {connected_clients} clients: {hook_event}"
89
+ )
80
90
  await self.broadcast_event("claude_event", data)
81
91
 
82
92
  # Track sessions based on hook events
@@ -15,7 +15,8 @@ if TYPE_CHECKING:
15
15
 
16
16
  from ..server import SocketIOServer
17
17
 
18
- from .code_analysis import CodeAnalysisEventHandler
18
+ # DISABLED: File Tree interface removed from dashboard
19
+ # from .code_analysis import CodeAnalysisEventHandler
19
20
  from .connection import ConnectionEventHandler
20
21
  from .file import FileEventHandler
21
22
  from .git import GitEventHandler
@@ -38,7 +39,8 @@ class EventHandlerRegistry:
38
39
  HookEventHandler, # Hook events for session tracking
39
40
  GitEventHandler, # Git operations
40
41
  FileEventHandler, # File operations
41
- CodeAnalysisEventHandler, # Code analysis for dashboard
42
+ # DISABLED: File Tree interface removed from dashboard
43
+ # CodeAnalysisEventHandler, # Code analysis for dashboard
42
44
  ProjectEventHandler, # Project management (future)
43
45
  MemoryEventHandler, # Memory management (future)
44
46
  ]
@@ -267,15 +267,15 @@ class SocketIOServer(SocketIOServiceInterface):
267
267
  except Exception as e:
268
268
  self.logger.error(f"Error during EventBus teardown: {e}")
269
269
 
270
- # Stop code analysis handler
270
+ # Stop event handlers
271
271
  if self.event_registry:
272
- from ..handlers import CodeAnalysisEventHandler, ConnectionEventHandler
273
-
274
- # Stop analysis runner
275
- analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
276
- if analysis_handler and hasattr(analysis_handler, "cleanup"):
277
- analysis_handler.cleanup()
272
+ from ..handlers import ConnectionEventHandler
278
273
 
274
+ # DISABLED: File Tree interface removed from dashboard
275
+ # Stop analysis runner (code analysis handler is disabled)
276
+ # analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
277
+ # if analysis_handler and hasattr(analysis_handler, "cleanup"):
278
+ # analysis_handler.cleanup()
279
279
  # Stop health monitoring in connection handler
280
280
  conn_handler = self.event_registry.get_handler(ConnectionEventHandler)
281
281
  if conn_handler and hasattr(conn_handler, "stop_health_monitoring"):
@@ -0,0 +1,21 @@
1
+ """
2
+ Claude MPM Skills Package
3
+
4
+ Skills system for sharing common capabilities across agents.
5
+ This reduces redundancy by extracting shared patterns into reusable skills.
6
+
7
+ Skills can be:
8
+ - Bundled with MPM (in skills/bundled/)
9
+ - User-installed (in ~/.claude/skills/)
10
+ - Project-specific (in .claude/skills/)
11
+ """
12
+
13
+ from .registry import Skill, SkillsRegistry, get_registry
14
+ from .skill_manager import SkillManager
15
+
16
+ __all__ = [
17
+ "Skill",
18
+ "SkillManager",
19
+ "SkillsRegistry",
20
+ "get_registry",
21
+ ]
@@ -0,0 +1,6 @@
1
+ """
2
+ Bundled skills shipped with Claude MPM.
3
+
4
+ These skills provide reusable patterns and practices that eliminate
5
+ redundancy across agent templates.
6
+ """
@@ -0,0 +1,198 @@
1
+ """Skills registry - manages bundled and discovered skills."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from claude_mpm.core.logging_utils import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class Skill:
14
+ """Represents a skill that can be used by agents."""
15
+
16
+ name: str
17
+ path: Path
18
+ content: str
19
+ source: str # 'bundled', 'user', or 'project'
20
+ description: str = ""
21
+ agent_types: List[str] = None # Which agent types can use this skill
22
+
23
+ def __post_init__(self):
24
+ """Initialize agent_types list if not provided."""
25
+ if self.agent_types is None:
26
+ self.agent_types = []
27
+
28
+
29
+ class SkillsRegistry:
30
+ """Registry for managing skills across all tiers."""
31
+
32
+ def __init__(self):
33
+ """Initialize the skills registry."""
34
+ self.skills: Dict[str, Skill] = {}
35
+ self._load_bundled_skills()
36
+ self._load_user_skills()
37
+ self._load_project_skills()
38
+
39
+ def _load_bundled_skills(self):
40
+ """Load skills bundled with MPM."""
41
+ bundled_dir = Path(__file__).parent / "bundled"
42
+ if not bundled_dir.exists():
43
+ logger.warning(f"Bundled skills directory not found: {bundled_dir}")
44
+ return
45
+
46
+ skill_count = 0
47
+ for skill_file in bundled_dir.glob("*.md"):
48
+ try:
49
+ skill_name = skill_file.stem
50
+ content = skill_file.read_text(encoding="utf-8")
51
+
52
+ # Extract description from first paragraph if available
53
+ description = self._extract_description(content)
54
+
55
+ self.skills[skill_name] = Skill(
56
+ name=skill_name,
57
+ path=skill_file,
58
+ content=content,
59
+ source="bundled",
60
+ description=description,
61
+ )
62
+ skill_count += 1
63
+ except Exception as e:
64
+ logger.error(f"Error loading bundled skill {skill_file}: {e}")
65
+
66
+ logger.info(f"Loaded {skill_count} bundled skills")
67
+
68
+ def _load_user_skills(self):
69
+ """Load user-installed skills from ~/.claude/skills/"""
70
+ user_skills_dir = Path.home() / ".claude" / "skills"
71
+ if not user_skills_dir.exists():
72
+ logger.debug("User skills directory not found, skipping")
73
+ return
74
+
75
+ skill_count = 0
76
+ for skill_file in user_skills_dir.glob("*.md"):
77
+ try:
78
+ skill_name = skill_file.stem
79
+ # User skills override bundled skills
80
+ content = skill_file.read_text(encoding="utf-8")
81
+ description = self._extract_description(content)
82
+
83
+ self.skills[skill_name] = Skill(
84
+ name=skill_name,
85
+ path=skill_file,
86
+ content=content,
87
+ source="user",
88
+ description=description,
89
+ )
90
+ skill_count += 1
91
+ logger.debug(f"User skill '{skill_name}' overrides bundled version")
92
+ except Exception as e:
93
+ logger.error(f"Error loading user skill {skill_file}: {e}")
94
+
95
+ if skill_count > 0:
96
+ logger.info(f"Loaded {skill_count} user skills")
97
+
98
+ def _load_project_skills(self):
99
+ """Load project-specific skills from .claude/skills/"""
100
+ project_skills_dir = Path.cwd() / ".claude" / "skills"
101
+ if not project_skills_dir.exists():
102
+ logger.debug("Project skills directory not found, skipping")
103
+ return
104
+
105
+ skill_count = 0
106
+ for skill_file in project_skills_dir.glob("*.md"):
107
+ try:
108
+ skill_name = skill_file.stem
109
+ # Project skills override both user and bundled skills
110
+ content = skill_file.read_text(encoding="utf-8")
111
+ description = self._extract_description(content)
112
+
113
+ self.skills[skill_name] = Skill(
114
+ name=skill_name,
115
+ path=skill_file,
116
+ content=content,
117
+ source="project",
118
+ description=description,
119
+ )
120
+ skill_count += 1
121
+ logger.debug(f"Project skill '{skill_name}' overrides other versions")
122
+ except Exception as e:
123
+ logger.error(f"Error loading project skill {skill_file}: {e}")
124
+
125
+ if skill_count > 0:
126
+ logger.info(f"Loaded {skill_count} project skills")
127
+
128
+ def _extract_description(self, content: str) -> str:
129
+ """Extract description from skill content (first paragraph or summary)."""
130
+ lines = content.strip().split("\n")
131
+ description_lines = []
132
+
133
+ # Skip title (first line starting with #)
134
+ start_idx = 0
135
+ if lines and lines[0].startswith("#"):
136
+ start_idx = 1
137
+
138
+ # Find first non-empty paragraph
139
+ for line in lines[start_idx:]:
140
+ line = line.strip()
141
+ if not line:
142
+ if description_lines:
143
+ break
144
+ continue
145
+ if line.startswith("#"):
146
+ break
147
+ description_lines.append(line)
148
+
149
+ return " ".join(description_lines)[:200] # Limit to 200 chars
150
+
151
+ def get_skill(self, name: str) -> Optional[Skill]:
152
+ """Get a skill by name."""
153
+ return self.skills.get(name)
154
+
155
+ def list_skills(self, source: Optional[str] = None) -> List[Skill]:
156
+ """List all skills, optionally filtered by source."""
157
+ if source:
158
+ return [s for s in self.skills.values() if s.source == source]
159
+ return list(self.skills.values())
160
+
161
+ def get_skills_for_agent(self, agent_type: str) -> List[Skill]:
162
+ """
163
+ Get skills mapped to a specific agent type.
164
+
165
+ Args:
166
+ agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
167
+
168
+ Returns:
169
+ List of skills applicable to this agent type
170
+ """
171
+ # Filter skills that explicitly list this agent type
172
+ # If a skill has no agent_types specified, it's available to all agents
173
+ return [
174
+ skill
175
+ for skill in self.skills.values()
176
+ if not skill.agent_types or agent_type in skill.agent_types
177
+ ]
178
+
179
+ def reload(self):
180
+ """Reload all skills from disk."""
181
+ logger.info("Reloading skills registry...")
182
+ self.skills.clear()
183
+ self._load_bundled_skills()
184
+ self._load_user_skills()
185
+ self._load_project_skills()
186
+ logger.info(f"Skills registry reloaded with {len(self.skills)} skills")
187
+
188
+
189
+ # Global registry instance (singleton pattern)
190
+ _registry: Optional[SkillsRegistry] = None
191
+
192
+
193
+ def get_registry() -> SkillsRegistry:
194
+ """Get the global skills registry (singleton)."""
195
+ global _registry
196
+ if _registry is None:
197
+ _registry = SkillsRegistry()
198
+ return _registry