claude-mpm 3.4.27__py3-none-any.whl → 3.5.1__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +182 -299
- claude_mpm/agents/agent_loader.py +283 -57
- claude_mpm/agents/agent_loader_integration.py +6 -9
- claude_mpm/agents/base_agent.json +2 -1
- claude_mpm/agents/base_agent_loader.py +1 -1
- claude_mpm/cli/__init__.py +5 -7
- claude_mpm/cli/commands/__init__.py +0 -2
- claude_mpm/cli/commands/agents.py +1 -1
- claude_mpm/cli/commands/memory.py +1 -1
- claude_mpm/cli/commands/run.py +12 -0
- claude_mpm/cli/parser.py +0 -13
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/config/__init__.py +44 -2
- claude_mpm/config/agent_config.py +348 -0
- claude_mpm/config/paths.py +322 -0
- claude_mpm/constants.py +0 -1
- claude_mpm/core/__init__.py +2 -5
- claude_mpm/core/agent_registry.py +63 -17
- claude_mpm/core/claude_runner.py +354 -43
- claude_mpm/core/config.py +7 -1
- claude_mpm/core/config_aliases.py +4 -3
- claude_mpm/core/config_paths.py +151 -0
- claude_mpm/core/factories.py +4 -50
- claude_mpm/core/logger.py +11 -13
- claude_mpm/core/service_registry.py +2 -2
- claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
- claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/init.py +37 -6
- claude_mpm/scripts/socketio_daemon.py +6 -2
- claude_mpm/services/__init__.py +71 -3
- claude_mpm/services/agents/__init__.py +85 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
- claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
- claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
- claude_mpm/services/agents/memory/__init__.py +21 -0
- claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
- claude_mpm/services/agents/registry/__init__.py +29 -0
- claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
- claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
- claude_mpm/services/async_session_logger.py +584 -0
- claude_mpm/services/claude_session_logger.py +299 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
- claude_mpm/services/framework_claude_md_generator.py +4 -2
- claude_mpm/services/memory/__init__.py +17 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
- claude_mpm/services/memory/cache/simple_cache.py +317 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
- claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
- claude_mpm/services/optimized_hook_service.py +542 -0
- claude_mpm/services/project_registry.py +14 -8
- claude_mpm/services/response_tracker.py +237 -0
- claude_mpm/services/ticketing_service_original.py +4 -2
- claude_mpm/services/version_control/branch_strategy.py +3 -1
- claude_mpm/utils/paths.py +12 -10
- claude_mpm/utils/session_logging.py +114 -0
- claude_mpm/validation/agent_validator.py +2 -1
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/METADATA +28 -20
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/RECORD +83 -106
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
- /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
- /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimized Hook Service with Caching and Async Processing
|
|
3
|
+
|
|
4
|
+
High-performance hook service that minimizes overhead through:
|
|
5
|
+
- Hook configuration caching at startup
|
|
6
|
+
- Lazy loading of hook implementations
|
|
7
|
+
- Singleton pattern for efficient memory usage
|
|
8
|
+
- Async hook processing for non-blocking execution
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import time
|
|
13
|
+
import importlib
|
|
14
|
+
import inspect
|
|
15
|
+
from typing import List, Optional, Dict, Any, Type, Callable, Set
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from threading import Lock
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
from claude_mpm.core.config import Config
|
|
23
|
+
from claude_mpm.core.logger import get_logger
|
|
24
|
+
from claude_mpm.hooks.base_hook import (
|
|
25
|
+
BaseHook,
|
|
26
|
+
PreDelegationHook,
|
|
27
|
+
PostDelegationHook,
|
|
28
|
+
HookContext,
|
|
29
|
+
HookResult,
|
|
30
|
+
HookType
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class HookConfig:
|
|
36
|
+
"""Cached hook configuration."""
|
|
37
|
+
name: str
|
|
38
|
+
module_path: str
|
|
39
|
+
class_name: str
|
|
40
|
+
priority: int = 50
|
|
41
|
+
enabled: bool = True
|
|
42
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
loaded_instance: Optional[BaseHook] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class HookExecutionMetrics:
|
|
48
|
+
"""Metrics for hook execution performance."""
|
|
49
|
+
execution_count: int = 0
|
|
50
|
+
total_time_ms: float = 0.0
|
|
51
|
+
avg_time_ms: float = 0.0
|
|
52
|
+
max_time_ms: float = 0.0
|
|
53
|
+
min_time_ms: float = float('inf')
|
|
54
|
+
error_count: int = 0
|
|
55
|
+
last_execution: Optional[float] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OptimizedHookService:
|
|
59
|
+
"""
|
|
60
|
+
Optimized hook service with caching and async execution.
|
|
61
|
+
|
|
62
|
+
Features:
|
|
63
|
+
- Caches hook configurations at startup
|
|
64
|
+
- Lazy loads hook implementations on first use
|
|
65
|
+
- Singleton pattern for memory efficiency
|
|
66
|
+
- Async/parallel hook execution support
|
|
67
|
+
- Detailed performance metrics
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
_instance: Optional['OptimizedHookService'] = None
|
|
71
|
+
_lock = Lock()
|
|
72
|
+
|
|
73
|
+
def __new__(cls, *args, **kwargs):
|
|
74
|
+
"""Singleton pattern implementation."""
|
|
75
|
+
with cls._lock:
|
|
76
|
+
if cls._instance is None:
|
|
77
|
+
cls._instance = super().__new__(cls)
|
|
78
|
+
return cls._instance
|
|
79
|
+
|
|
80
|
+
def __init__(self, config: Optional[Config] = None):
|
|
81
|
+
"""
|
|
82
|
+
Initialize the optimized hook service.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
config: Optional configuration object
|
|
86
|
+
"""
|
|
87
|
+
# Skip re-initialization for singleton
|
|
88
|
+
if hasattr(self, '_initialized'):
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
self.config = config or Config()
|
|
92
|
+
self.logger = get_logger("optimized_hook_service")
|
|
93
|
+
|
|
94
|
+
# Hook storage with lazy loading
|
|
95
|
+
self._hook_configs: Dict[str, HookConfig] = {}
|
|
96
|
+
self._pre_hooks_cache: List[HookConfig] = []
|
|
97
|
+
self._post_hooks_cache: List[HookConfig] = []
|
|
98
|
+
|
|
99
|
+
# Performance metrics
|
|
100
|
+
self._metrics: Dict[str, HookExecutionMetrics] = {}
|
|
101
|
+
|
|
102
|
+
# Async execution support
|
|
103
|
+
self._executor = ThreadPoolExecutor(
|
|
104
|
+
max_workers=self.config.get("hooks.max_workers", 4),
|
|
105
|
+
thread_name_prefix="HookWorker"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Load and cache hook configurations
|
|
109
|
+
self._load_hook_configs()
|
|
110
|
+
|
|
111
|
+
# Mark as initialized
|
|
112
|
+
self._initialized = True
|
|
113
|
+
|
|
114
|
+
self.logger.info(
|
|
115
|
+
f"Initialized optimized hook service with "
|
|
116
|
+
f"{len(self._pre_hooks_cache)} pre-hooks and "
|
|
117
|
+
f"{len(self._post_hooks_cache)} post-hooks"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _load_hook_configs(self):
|
|
121
|
+
"""Load and cache all hook configurations at startup."""
|
|
122
|
+
# Load from configuration file
|
|
123
|
+
hooks_config = self.config.get("hooks.registered", {})
|
|
124
|
+
|
|
125
|
+
for hook_name, hook_data in hooks_config.items():
|
|
126
|
+
if not hook_data.get("enabled", True):
|
|
127
|
+
self.logger.debug(f"Skipping disabled hook: {hook_name}")
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
config = HookConfig(
|
|
131
|
+
name=hook_name,
|
|
132
|
+
module_path=hook_data.get("module"),
|
|
133
|
+
class_name=hook_data.get("class"),
|
|
134
|
+
priority=hook_data.get("priority", 50),
|
|
135
|
+
enabled=True,
|
|
136
|
+
params=hook_data.get("params", {})
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self._hook_configs[hook_name] = config
|
|
140
|
+
|
|
141
|
+
# Categorize by type (inferred from name or config)
|
|
142
|
+
hook_type = hook_data.get("type", "")
|
|
143
|
+
if "pre" in hook_type.lower() or "before" in hook_name.lower():
|
|
144
|
+
self._pre_hooks_cache.append(config)
|
|
145
|
+
elif "post" in hook_type.lower() or "after" in hook_name.lower():
|
|
146
|
+
self._post_hooks_cache.append(config)
|
|
147
|
+
|
|
148
|
+
# Sort by priority (lower number = higher priority)
|
|
149
|
+
self._pre_hooks_cache.sort(key=lambda h: h.priority)
|
|
150
|
+
self._post_hooks_cache.sort(key=lambda h: h.priority)
|
|
151
|
+
|
|
152
|
+
# Also scan for hooks in the hooks directory
|
|
153
|
+
self._scan_hook_directory()
|
|
154
|
+
|
|
155
|
+
def _scan_hook_directory(self):
|
|
156
|
+
"""Scan hooks directory for available hooks."""
|
|
157
|
+
hooks_dir = Path(__file__).parent.parent / "hooks"
|
|
158
|
+
|
|
159
|
+
if not hooks_dir.exists():
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Look for Python files in hooks directory
|
|
163
|
+
for hook_file in hooks_dir.glob("**/*.py"):
|
|
164
|
+
if hook_file.name.startswith("_"):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Convert file path to module path
|
|
168
|
+
relative_path = hook_file.relative_to(hooks_dir.parent.parent)
|
|
169
|
+
module_path = str(relative_path.with_suffix("")).replace("/", ".")
|
|
170
|
+
|
|
171
|
+
# Skip if already configured
|
|
172
|
+
if any(h.module_path == module_path for h in self._hook_configs.values()):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Try to load and inspect the module
|
|
176
|
+
try:
|
|
177
|
+
module = importlib.import_module(module_path)
|
|
178
|
+
|
|
179
|
+
# Find hook classes in the module
|
|
180
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
181
|
+
if issubclass(obj, BaseHook) and obj != BaseHook:
|
|
182
|
+
# Auto-register discovered hook
|
|
183
|
+
hook_name = f"auto_{hook_file.stem}_{name}"
|
|
184
|
+
|
|
185
|
+
config = HookConfig(
|
|
186
|
+
name=hook_name,
|
|
187
|
+
module_path=module_path,
|
|
188
|
+
class_name=name,
|
|
189
|
+
priority=getattr(obj, 'priority', 50),
|
|
190
|
+
enabled=False # Disabled by default for auto-discovered
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self._hook_configs[hook_name] = config
|
|
194
|
+
self.logger.debug(f"Discovered hook: {hook_name} (disabled)")
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.logger.debug(f"Failed to scan {hook_file}: {e}")
|
|
198
|
+
|
|
199
|
+
def _lazy_load_hook(self, config: HookConfig) -> Optional[BaseHook]:
|
|
200
|
+
"""
|
|
201
|
+
Lazy load a hook instance when needed.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
config: Hook configuration
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Loaded hook instance or None if failed
|
|
208
|
+
"""
|
|
209
|
+
if config.loaded_instance:
|
|
210
|
+
return config.loaded_instance
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# Import the module
|
|
214
|
+
module = importlib.import_module(config.module_path)
|
|
215
|
+
|
|
216
|
+
# Get the hook class
|
|
217
|
+
hook_class = getattr(module, config.class_name)
|
|
218
|
+
|
|
219
|
+
# Instantiate with parameters
|
|
220
|
+
hook_instance = hook_class(**config.params)
|
|
221
|
+
|
|
222
|
+
# Cache the instance
|
|
223
|
+
config.loaded_instance = hook_instance
|
|
224
|
+
|
|
225
|
+
# Initialize metrics
|
|
226
|
+
self._metrics[config.name] = HookExecutionMetrics()
|
|
227
|
+
|
|
228
|
+
self.logger.info(f"Lazy loaded hook: {config.name}")
|
|
229
|
+
return hook_instance
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.logger.error(f"Failed to load hook {config.name}: {e}")
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def register_hook(self, hook: BaseHook) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Register a hook instance directly.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
hook: The hook to register
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if successfully registered
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
# Create config for the hook
|
|
247
|
+
config = HookConfig(
|
|
248
|
+
name=hook.name,
|
|
249
|
+
module_path=hook.__module__,
|
|
250
|
+
class_name=hook.__class__.__name__,
|
|
251
|
+
priority=hook.priority,
|
|
252
|
+
enabled=hook.enabled,
|
|
253
|
+
loaded_instance=hook
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self._hook_configs[hook.name] = config
|
|
257
|
+
|
|
258
|
+
# Add to appropriate cache
|
|
259
|
+
if isinstance(hook, PreDelegationHook):
|
|
260
|
+
self._pre_hooks_cache.append(config)
|
|
261
|
+
self._pre_hooks_cache.sort(key=lambda h: h.priority)
|
|
262
|
+
elif isinstance(hook, PostDelegationHook):
|
|
263
|
+
self._post_hooks_cache.append(config)
|
|
264
|
+
self._post_hooks_cache.sort(key=lambda h: h.priority)
|
|
265
|
+
|
|
266
|
+
# Initialize metrics
|
|
267
|
+
self._metrics[hook.name] = HookExecutionMetrics()
|
|
268
|
+
|
|
269
|
+
self.logger.info(f"Registered hook: {hook.name}")
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
self.logger.error(f"Failed to register hook: {e}")
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
async def execute_pre_delegation_hooks_async(
|
|
277
|
+
self,
|
|
278
|
+
context: HookContext
|
|
279
|
+
) -> HookResult:
|
|
280
|
+
"""
|
|
281
|
+
Execute pre-delegation hooks asynchronously.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
context: The hook context
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
HookResult with processed data
|
|
288
|
+
"""
|
|
289
|
+
if not self._are_hooks_enabled("pre_delegation"):
|
|
290
|
+
return HookResult(success=True, data=context.data, modified=False)
|
|
291
|
+
|
|
292
|
+
working_data = context.data.copy()
|
|
293
|
+
has_modifications = False
|
|
294
|
+
|
|
295
|
+
# Execute hooks in parallel where possible
|
|
296
|
+
tasks = []
|
|
297
|
+
|
|
298
|
+
for config in self._pre_hooks_cache:
|
|
299
|
+
if not config.enabled:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
hook = self._lazy_load_hook(config)
|
|
303
|
+
if not hook:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Check if hook can run in parallel (no data dependencies)
|
|
307
|
+
if getattr(hook, 'parallel_safe', False):
|
|
308
|
+
task = self._execute_hook_async(hook, context, config.name)
|
|
309
|
+
tasks.append(task)
|
|
310
|
+
else:
|
|
311
|
+
# Execute sequentially for non-parallel-safe hooks
|
|
312
|
+
result = await self._execute_hook_async(hook, context, config.name)
|
|
313
|
+
if result.modified and result.data:
|
|
314
|
+
working_data.update(result.data)
|
|
315
|
+
has_modifications = True
|
|
316
|
+
|
|
317
|
+
# Wait for parallel tasks
|
|
318
|
+
if tasks:
|
|
319
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
320
|
+
for result in results:
|
|
321
|
+
if isinstance(result, HookResult):
|
|
322
|
+
if result.modified and result.data:
|
|
323
|
+
working_data.update(result.data)
|
|
324
|
+
has_modifications = True
|
|
325
|
+
elif isinstance(result, Exception):
|
|
326
|
+
self.logger.error(f"Hook execution error: {result}")
|
|
327
|
+
|
|
328
|
+
return HookResult(
|
|
329
|
+
success=True,
|
|
330
|
+
data=working_data,
|
|
331
|
+
modified=has_modifications
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
async def _execute_hook_async(
|
|
335
|
+
self,
|
|
336
|
+
hook: BaseHook,
|
|
337
|
+
context: HookContext,
|
|
338
|
+
hook_name: str
|
|
339
|
+
) -> HookResult:
|
|
340
|
+
"""Execute a single hook asynchronously with metrics."""
|
|
341
|
+
try:
|
|
342
|
+
# Validate hook
|
|
343
|
+
if not hook.validate(context):
|
|
344
|
+
return HookResult(success=False, data={}, modified=False)
|
|
345
|
+
|
|
346
|
+
# Time execution
|
|
347
|
+
start_time = time.perf_counter()
|
|
348
|
+
|
|
349
|
+
# Execute hook (wrap synchronous hooks)
|
|
350
|
+
if asyncio.iscoroutinefunction(hook.execute):
|
|
351
|
+
result = await hook.execute(context)
|
|
352
|
+
else:
|
|
353
|
+
# Run synchronous hook in executor
|
|
354
|
+
loop = asyncio.get_event_loop()
|
|
355
|
+
result = await loop.run_in_executor(
|
|
356
|
+
self._executor,
|
|
357
|
+
hook.execute,
|
|
358
|
+
context
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Update metrics
|
|
362
|
+
execution_time = (time.perf_counter() - start_time) * 1000
|
|
363
|
+
self._update_metrics(hook_name, execution_time, success=True)
|
|
364
|
+
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.logger.error(f"Hook {hook_name} failed: {e}")
|
|
369
|
+
self._update_metrics(hook_name, 0, success=False)
|
|
370
|
+
return HookResult(success=False, error=str(e))
|
|
371
|
+
|
|
372
|
+
def execute_pre_delegation_hooks(self, context: HookContext) -> HookResult:
|
|
373
|
+
"""
|
|
374
|
+
Execute pre-delegation hooks synchronously (backward compatible).
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
context: The hook context
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
HookResult with processed data
|
|
381
|
+
"""
|
|
382
|
+
# Run async version in new event loop
|
|
383
|
+
try:
|
|
384
|
+
loop = asyncio.new_event_loop()
|
|
385
|
+
result = loop.run_until_complete(
|
|
386
|
+
self.execute_pre_delegation_hooks_async(context)
|
|
387
|
+
)
|
|
388
|
+
loop.close()
|
|
389
|
+
return result
|
|
390
|
+
except Exception as e:
|
|
391
|
+
self.logger.error(f"Failed to execute hooks: {e}")
|
|
392
|
+
return HookResult(success=False, error=str(e))
|
|
393
|
+
|
|
394
|
+
def execute_post_delegation_hooks(self, context: HookContext) -> HookResult:
|
|
395
|
+
"""
|
|
396
|
+
Execute post-delegation hooks synchronously (backward compatible).
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
context: The hook context
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
HookResult with processed data
|
|
403
|
+
"""
|
|
404
|
+
if not self._are_hooks_enabled("post_delegation"):
|
|
405
|
+
return HookResult(success=True, data=context.data, modified=False)
|
|
406
|
+
|
|
407
|
+
working_data = context.data.copy()
|
|
408
|
+
has_modifications = False
|
|
409
|
+
|
|
410
|
+
for config in self._post_hooks_cache:
|
|
411
|
+
if not config.enabled:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
hook = self._lazy_load_hook(config)
|
|
415
|
+
if not hook:
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
if not hook.validate(context):
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# Time execution
|
|
423
|
+
start_time = time.perf_counter()
|
|
424
|
+
|
|
425
|
+
# Execute hook
|
|
426
|
+
hook_context = HookContext(
|
|
427
|
+
hook_type=context.hook_type,
|
|
428
|
+
data=working_data,
|
|
429
|
+
metadata=context.metadata,
|
|
430
|
+
timestamp=context.timestamp,
|
|
431
|
+
session_id=context.session_id,
|
|
432
|
+
user_id=context.user_id
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
result = hook.execute(hook_context)
|
|
436
|
+
|
|
437
|
+
# Update metrics
|
|
438
|
+
execution_time = (time.perf_counter() - start_time) * 1000
|
|
439
|
+
self._update_metrics(config.name, execution_time, success=result.success)
|
|
440
|
+
|
|
441
|
+
if result.success and result.modified and result.data:
|
|
442
|
+
working_data.update(result.data)
|
|
443
|
+
has_modifications = True
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
self.logger.error(f"Hook {config.name} failed: {e}")
|
|
447
|
+
self._update_metrics(config.name, 0, success=False)
|
|
448
|
+
|
|
449
|
+
return HookResult(
|
|
450
|
+
success=True,
|
|
451
|
+
data=working_data,
|
|
452
|
+
modified=has_modifications
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def _update_metrics(self, hook_name: str, execution_time_ms: float, success: bool):
|
|
456
|
+
"""Update execution metrics for a hook."""
|
|
457
|
+
if hook_name not in self._metrics:
|
|
458
|
+
self._metrics[hook_name] = HookExecutionMetrics()
|
|
459
|
+
|
|
460
|
+
metrics = self._metrics[hook_name]
|
|
461
|
+
metrics.execution_count += 1
|
|
462
|
+
metrics.total_time_ms += execution_time_ms
|
|
463
|
+
metrics.avg_time_ms = metrics.total_time_ms / metrics.execution_count
|
|
464
|
+
metrics.max_time_ms = max(metrics.max_time_ms, execution_time_ms)
|
|
465
|
+
metrics.min_time_ms = min(metrics.min_time_ms, execution_time_ms)
|
|
466
|
+
metrics.last_execution = time.time()
|
|
467
|
+
|
|
468
|
+
if not success:
|
|
469
|
+
metrics.error_count += 1
|
|
470
|
+
|
|
471
|
+
def _are_hooks_enabled(self, hook_type: str) -> bool:
|
|
472
|
+
"""Check if hooks are enabled."""
|
|
473
|
+
if not self.config.get("hooks.enabled", True):
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
if not self.config.get(f"hooks.{hook_type}.enabled", True):
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
def get_metrics(self) -> Dict[str, Dict[str, Any]]:
|
|
482
|
+
"""Get performance metrics for all hooks."""
|
|
483
|
+
return {
|
|
484
|
+
name: {
|
|
485
|
+
"execution_count": m.execution_count,
|
|
486
|
+
"avg_time_ms": round(m.avg_time_ms, 2),
|
|
487
|
+
"max_time_ms": round(m.max_time_ms, 2),
|
|
488
|
+
"min_time_ms": round(m.min_time_ms, 2) if m.min_time_ms != float('inf') else 0,
|
|
489
|
+
"error_count": m.error_count,
|
|
490
|
+
"error_rate": round(m.error_count / m.execution_count * 100, 2) if m.execution_count > 0 else 0
|
|
491
|
+
}
|
|
492
|
+
for name, m in self._metrics.items()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
def list_hooks(self) -> Dict[str, List[str]]:
|
|
496
|
+
"""List all registered hooks."""
|
|
497
|
+
return {
|
|
498
|
+
"pre_delegation": [h.name for h in self._pre_hooks_cache],
|
|
499
|
+
"post_delegation": [h.name for h in self._post_hooks_cache],
|
|
500
|
+
"available": list(self._hook_configs.keys())
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
def enable_hook(self, hook_name: str) -> bool:
|
|
504
|
+
"""Enable a hook by name."""
|
|
505
|
+
if hook_name in self._hook_configs:
|
|
506
|
+
self._hook_configs[hook_name].enabled = True
|
|
507
|
+
return True
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def disable_hook(self, hook_name: str) -> bool:
|
|
511
|
+
"""Disable a hook by name."""
|
|
512
|
+
if hook_name in self._hook_configs:
|
|
513
|
+
self._hook_configs[hook_name].enabled = False
|
|
514
|
+
return True
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
def shutdown(self):
|
|
518
|
+
"""Shutdown the hook service and cleanup resources."""
|
|
519
|
+
self.logger.info("Shutting down hook service")
|
|
520
|
+
self._executor.shutdown(wait=True)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# Global singleton instance
|
|
524
|
+
_hook_service: Optional[OptimizedHookService] = None
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def get_optimized_hook_service(config: Optional[Config] = None) -> OptimizedHookService:
|
|
528
|
+
"""
|
|
529
|
+
Get the singleton optimized hook service instance.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
config: Optional configuration
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
The shared OptimizedHookService instance
|
|
536
|
+
"""
|
|
537
|
+
global _hook_service
|
|
538
|
+
|
|
539
|
+
if _hook_service is None:
|
|
540
|
+
_hook_service = OptimizedHookService(config)
|
|
541
|
+
|
|
542
|
+
return _hook_service
|
|
@@ -6,9 +6,10 @@ project identification, tracking, and metadata management. The registry stores
|
|
|
6
6
|
comprehensive project information including git status, environment details,
|
|
7
7
|
runtime information, and project characteristics.
|
|
8
8
|
|
|
9
|
-
DESIGN DECISION: Uses YAML for human-readable registry files stored in the
|
|
10
|
-
home directory (~/.claude-mpm/registry/). Each project
|
|
11
|
-
registry file to avoid conflicts and enable easy project
|
|
9
|
+
DESIGN DECISION: Uses YAML for human-readable registry files stored in the
|
|
10
|
+
user's home directory (~/.claude-mpm/registry/). Each project
|
|
11
|
+
gets a unique UUID-based registry file to avoid conflicts and enable easy project
|
|
12
|
+
identification. Registry is user-specific for better isolation and persistence.
|
|
12
13
|
|
|
13
14
|
The registry captures both static project information (paths, git info) and
|
|
14
15
|
dynamic runtime information (startup times, process IDs, command line args)
|
|
@@ -27,6 +28,7 @@ from typing import Dict, Any, Optional, List
|
|
|
27
28
|
import yaml
|
|
28
29
|
|
|
29
30
|
from claude_mpm.core.logger import get_logger
|
|
31
|
+
from claude_mpm.deployment_paths import get_project_root
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class ProjectRegistryError(Exception):
|
|
@@ -43,9 +45,10 @@ class ProjectRegistry:
|
|
|
43
45
|
This is crucial for multi-project environments where users switch between
|
|
44
46
|
different codebases.
|
|
45
47
|
|
|
46
|
-
DESIGN DECISION: Registry files are stored in ~/.claude-mpm/registry/
|
|
47
|
-
UUID-based filenames to ensure uniqueness and avoid conflicts. The registry
|
|
48
|
+
DESIGN DECISION: Registry files are stored in ~/.claude-mpm/registry/
|
|
49
|
+
with UUID-based filenames to ensure uniqueness and avoid conflicts. The registry
|
|
48
50
|
uses YAML for human readability and ease of manual inspection/editing.
|
|
51
|
+
Registry is stored in the user's home directory for persistence across projects.
|
|
49
52
|
"""
|
|
50
53
|
|
|
51
54
|
def __init__(self):
|
|
@@ -53,11 +56,14 @@ class ProjectRegistry:
|
|
|
53
56
|
Initialize the project registry.
|
|
54
57
|
|
|
55
58
|
WHY: Sets up the registry directory and logger. The registry directory
|
|
56
|
-
is created in the user's home directory to
|
|
57
|
-
|
|
59
|
+
is created in the user's home directory to keep registry data user-specific
|
|
60
|
+
and persistent across different projects and sessions.
|
|
58
61
|
"""
|
|
59
62
|
self.logger = get_logger("project_registry")
|
|
60
|
-
|
|
63
|
+
# Use user's home directory for registry to avoid project-specific contamination
|
|
64
|
+
# This ensures registry persists across all projects for the user
|
|
65
|
+
user_home = Path.home()
|
|
66
|
+
self.registry_dir = user_home / ".claude-mpm" / "registry"
|
|
61
67
|
self.current_project_path = Path.cwd().resolve()
|
|
62
68
|
|
|
63
69
|
# Ensure registry directory exists
|