claude-mpm 3.5.6__py3-none-any.whl → 3.6.0__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.
Files changed (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,376 @@
1
+ """
2
+ Dependency caching system for smart dependency checking.
3
+
4
+ This module provides caching for dependency check results to avoid
5
+ redundant checks and improve startup performance.
6
+ """
7
+
8
+ import json
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Dict, Optional, Tuple
12
+ import hashlib
13
+ import logging
14
+
15
+ from ..core.logger import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class DependencyCache:
21
+ """
22
+ Manages caching of dependency check results.
23
+
24
+ WHY: Dependency checking can be slow, especially when checking many packages.
25
+ By caching results keyed by deployment hash, we can skip checks when nothing
26
+ has changed, significantly improving startup time.
27
+
28
+ DESIGN DECISION: We use a file-based cache in .claude/agents/.dependency_cache
29
+ - Simple JSON format for easy debugging
30
+ - TTL-based expiration (24 hours default)
31
+ - Automatic invalidation when agent deployment changes
32
+ """
33
+
34
+ DEFAULT_TTL_SECONDS = 86400 # 24 hours
35
+
36
+ def __init__(self, cache_dir: Optional[Path] = None, ttl_seconds: int = DEFAULT_TTL_SECONDS):
37
+ """
38
+ Initialize the dependency cache.
39
+
40
+ Args:
41
+ cache_dir: Directory for cache files (default: .claude/agents/)
42
+ ttl_seconds: Time-to-live for cache entries in seconds
43
+ """
44
+ if cache_dir is None:
45
+ cache_dir = Path.cwd() / ".claude" / "agents"
46
+
47
+ self.cache_dir = cache_dir
48
+ self.cache_file = self.cache_dir / ".dependency_cache"
49
+ self.ttl_seconds = ttl_seconds
50
+ self._cache_data: Optional[Dict] = None
51
+
52
+ def _load_cache(self) -> Dict:
53
+ """
54
+ Load cache data from disk.
55
+
56
+ Returns:
57
+ Cache data dictionary or empty dict if not found.
58
+ """
59
+ if self._cache_data is not None:
60
+ return self._cache_data
61
+
62
+ if not self.cache_file.exists():
63
+ self._cache_data = {}
64
+ return self._cache_data
65
+
66
+ try:
67
+ with open(self.cache_file, 'r') as f:
68
+ self._cache_data = json.load(f)
69
+ return self._cache_data
70
+ except Exception as e:
71
+ logger.debug(f"Could not load dependency cache: {e}")
72
+ self._cache_data = {}
73
+ return self._cache_data
74
+
75
+ def _save_cache(self, cache_data: Dict) -> None:
76
+ """
77
+ Save cache data to disk.
78
+
79
+ Args:
80
+ cache_data: Cache data to save.
81
+ """
82
+ try:
83
+ # Ensure directory exists
84
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
85
+
86
+ with open(self.cache_file, 'w') as f:
87
+ json.dump(cache_data, f, indent=2)
88
+
89
+ self._cache_data = cache_data
90
+ except Exception as e:
91
+ logger.debug(f"Could not save dependency cache: {e}")
92
+
93
+ def _generate_cache_key(self, deployment_hash: str, context: Dict) -> str:
94
+ """
95
+ Generate a cache key for dependency results.
96
+
97
+ Args:
98
+ deployment_hash: Hash of the current agent deployment
99
+ context: Additional context (e.g., Python version, platform)
100
+
101
+ Returns:
102
+ Cache key string.
103
+
104
+ WHY: We include context like Python version because dependency
105
+ availability can vary between Python versions.
106
+ """
107
+ # Include important context in the cache key
108
+ import sys
109
+ import platform
110
+
111
+ key_parts = [
112
+ deployment_hash,
113
+ f"py{sys.version_info.major}.{sys.version_info.minor}",
114
+ platform.system().lower(),
115
+ platform.machine().lower()
116
+ ]
117
+
118
+ # Add any additional context
119
+ for key, value in sorted(context.items()):
120
+ key_parts.append(f"{key}:{value}")
121
+
122
+ # Create a hash of all parts for a compact key
123
+ key_string = "|".join(key_parts)
124
+ return hashlib.md5(key_string.encode()).hexdigest()
125
+
126
+ def get(
127
+ self,
128
+ deployment_hash: str,
129
+ context: Optional[Dict] = None
130
+ ) -> Optional[Dict]:
131
+ """
132
+ Get cached dependency check results.
133
+
134
+ Args:
135
+ deployment_hash: Hash of the current agent deployment
136
+ context: Additional context for cache key
137
+
138
+ Returns:
139
+ Cached results or None if not found/expired.
140
+ """
141
+ if context is None:
142
+ context = {}
143
+
144
+ cache_data = self._load_cache()
145
+ cache_key = self._generate_cache_key(deployment_hash, context)
146
+
147
+ if cache_key not in cache_data:
148
+ logger.debug(f"Cache miss for key {cache_key}")
149
+ return None
150
+
151
+ entry = cache_data[cache_key]
152
+
153
+ # Check if entry has expired
154
+ current_time = time.time()
155
+ if current_time - entry.get('timestamp', 0) > self.ttl_seconds:
156
+ logger.debug(f"Cache entry expired for key {cache_key}")
157
+ # Remove expired entry
158
+ del cache_data[cache_key]
159
+ self._save_cache(cache_data)
160
+ return None
161
+
162
+ logger.debug(f"Cache hit for key {cache_key}")
163
+ return entry.get('results')
164
+
165
+ def set(
166
+ self,
167
+ deployment_hash: str,
168
+ results: Dict,
169
+ context: Optional[Dict] = None
170
+ ) -> None:
171
+ """
172
+ Cache dependency check results.
173
+
174
+ Args:
175
+ deployment_hash: Hash of the current agent deployment
176
+ results: Dependency check results to cache
177
+ context: Additional context for cache key
178
+ """
179
+ if context is None:
180
+ context = {}
181
+
182
+ cache_data = self._load_cache()
183
+ cache_key = self._generate_cache_key(deployment_hash, context)
184
+
185
+ # Store with timestamp
186
+ cache_data[cache_key] = {
187
+ 'timestamp': time.time(),
188
+ 'results': results,
189
+ 'deployment_hash': deployment_hash,
190
+ 'context': context
191
+ }
192
+
193
+ # Clean up old entries while we're at it
194
+ self._cleanup_expired_entries(cache_data)
195
+
196
+ self._save_cache(cache_data)
197
+ logger.debug(f"Cached results for key {cache_key}")
198
+
199
+ def invalidate(self, deployment_hash: Optional[str] = None) -> None:
200
+ """
201
+ Invalidate cache entries.
202
+
203
+ Args:
204
+ deployment_hash: If provided, only invalidate entries for this hash.
205
+ If None, invalidate all entries.
206
+ """
207
+ if deployment_hash is None:
208
+ # Clear entire cache
209
+ self._cache_data = {}
210
+ self._save_cache({})
211
+ logger.info("Cleared entire dependency cache")
212
+ else:
213
+ # Clear entries for specific deployment
214
+ cache_data = self._load_cache()
215
+ keys_to_remove = []
216
+
217
+ for key, entry in cache_data.items():
218
+ if entry.get('deployment_hash') == deployment_hash:
219
+ keys_to_remove.append(key)
220
+
221
+ for key in keys_to_remove:
222
+ del cache_data[key]
223
+
224
+ if keys_to_remove:
225
+ self._save_cache(cache_data)
226
+ logger.info(f"Invalidated {len(keys_to_remove)} cache entries for deployment {deployment_hash[:8]}...")
227
+
228
+ def _cleanup_expired_entries(self, cache_data: Dict) -> None:
229
+ """
230
+ Remove expired entries from cache data.
231
+
232
+ Args:
233
+ cache_data: Cache data dictionary to clean.
234
+
235
+ WHY: We clean up expired entries periodically to prevent the cache
236
+ from growing indefinitely.
237
+ """
238
+ current_time = time.time()
239
+ keys_to_remove = []
240
+
241
+ for key, entry in cache_data.items():
242
+ if current_time - entry.get('timestamp', 0) > self.ttl_seconds:
243
+ keys_to_remove.append(key)
244
+
245
+ for key in keys_to_remove:
246
+ del cache_data[key]
247
+
248
+ if keys_to_remove:
249
+ logger.debug(f"Cleaned up {len(keys_to_remove)} expired cache entries")
250
+
251
+ def get_cache_stats(self) -> Dict:
252
+ """
253
+ Get statistics about the cache.
254
+
255
+ Returns:
256
+ Dictionary with cache statistics.
257
+ """
258
+ cache_data = self._load_cache()
259
+ current_time = time.time()
260
+
261
+ total_entries = len(cache_data)
262
+ expired_entries = 0
263
+ valid_entries = 0
264
+
265
+ for entry in cache_data.values():
266
+ if current_time - entry.get('timestamp', 0) > self.ttl_seconds:
267
+ expired_entries += 1
268
+ else:
269
+ valid_entries += 1
270
+
271
+ return {
272
+ 'total_entries': total_entries,
273
+ 'valid_entries': valid_entries,
274
+ 'expired_entries': expired_entries,
275
+ 'cache_file': str(self.cache_file),
276
+ 'ttl_seconds': self.ttl_seconds
277
+ }
278
+
279
+ def clear(self) -> None:
280
+ """
281
+ Clear all cache entries.
282
+ """
283
+ self.invalidate(None)
284
+
285
+
286
+ class SmartDependencyChecker:
287
+ """
288
+ Combines caching with agent dependency checking for smart, efficient checks.
289
+
290
+ WHY: This class orchestrates the smart dependency checking by combining:
291
+ - Change detection (only check when agents change)
292
+ - Caching (reuse results when nothing has changed)
293
+ - Environment awareness (only prompt in appropriate contexts)
294
+ """
295
+
296
+ def __init__(self, cache_ttl_seconds: int = DependencyCache.DEFAULT_TTL_SECONDS):
297
+ """
298
+ Initialize the smart dependency checker.
299
+
300
+ Args:
301
+ cache_ttl_seconds: TTL for cache entries.
302
+ """
303
+ self.cache = DependencyCache(ttl_seconds=cache_ttl_seconds)
304
+ self._last_check_time = 0
305
+ self._min_check_interval = 60 # Don't check more than once per minute
306
+
307
+ def should_check_dependencies(
308
+ self,
309
+ force_check: bool = False,
310
+ deployment_hash: Optional[str] = None
311
+ ) -> Tuple[bool, str]:
312
+ """
313
+ Determine if dependency checking should be performed.
314
+
315
+ Args:
316
+ force_check: Force checking regardless of cache/changes
317
+ deployment_hash: Current deployment hash
318
+
319
+ Returns:
320
+ Tuple of (should_check, reason)
321
+ """
322
+ if force_check:
323
+ return True, "Forced check requested"
324
+
325
+ # Rate limiting - don't check too frequently
326
+ current_time = time.time()
327
+ if current_time - self._last_check_time < self._min_check_interval:
328
+ return False, f"Checked recently (within {self._min_check_interval}s)"
329
+
330
+ # Check if we have valid cached results
331
+ if deployment_hash:
332
+ cached_results = self.cache.get(deployment_hash)
333
+ if cached_results:
334
+ return False, "Valid cached results available"
335
+
336
+ return True, "No valid cache, checking needed"
337
+
338
+ def get_or_check_dependencies(
339
+ self,
340
+ loader, # AgentDependencyLoader instance
341
+ force_check: bool = False
342
+ ) -> Tuple[Dict, bool]:
343
+ """
344
+ Get dependency results from cache or perform check.
345
+
346
+ Args:
347
+ loader: AgentDependencyLoader instance
348
+ force_check: Force checking even if cached
349
+
350
+ Returns:
351
+ Tuple of (results, was_cached)
352
+ """
353
+ # Check if agents have changed
354
+ has_changed, deployment_hash = loader.has_agents_changed()
355
+
356
+ # Try to get cached results first
357
+ if not force_check and not has_changed:
358
+ cached_results = self.cache.get(deployment_hash)
359
+ if cached_results:
360
+ logger.info("Using cached dependency check results")
361
+ return cached_results, True
362
+
363
+ # Perform actual dependency check
364
+ logger.info("Performing dependency check...")
365
+ results = loader.load_and_check()
366
+
367
+ # Cache the results
368
+ self.cache.set(deployment_hash, results)
369
+
370
+ # Mark deployment as checked
371
+ loader.mark_deployment_checked(deployment_hash, results)
372
+
373
+ # Update last check time
374
+ self._last_check_time = time.time()
375
+
376
+ return results, False