claude-mpm 3.7.4__py3-none-any.whl → 3.8.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.
Files changed (117) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -78
  4. claude_mpm/agents/MEMORY.md +88 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/schema/agent_schema.json +1 -1
  7. claude_mpm/agents/templates/code_analyzer.json +26 -11
  8. claude_mpm/agents/templates/data_engineer.json +4 -7
  9. claude_mpm/agents/templates/documentation.json +2 -2
  10. claude_mpm/agents/templates/engineer.json +2 -2
  11. claude_mpm/agents/templates/ops.json +3 -8
  12. claude_mpm/agents/templates/qa.json +2 -3
  13. claude_mpm/agents/templates/research.json +2 -3
  14. claude_mpm/agents/templates/security.json +3 -6
  15. claude_mpm/agents/templates/ticketing.json +4 -9
  16. claude_mpm/agents/templates/version_control.json +3 -3
  17. claude_mpm/agents/templates/web_qa.json +4 -4
  18. claude_mpm/agents/templates/web_ui.json +4 -4
  19. claude_mpm/cli/__init__.py +2 -2
  20. claude_mpm/cli/commands/__init__.py +2 -1
  21. claude_mpm/cli/commands/agents.py +118 -1
  22. claude_mpm/cli/commands/tickets.py +596 -19
  23. claude_mpm/cli/parser.py +228 -5
  24. claude_mpm/config/__init__.py +30 -39
  25. claude_mpm/config/socketio_config.py +8 -5
  26. claude_mpm/constants.py +13 -0
  27. claude_mpm/core/__init__.py +8 -18
  28. claude_mpm/core/cache.py +596 -0
  29. claude_mpm/core/claude_runner.py +166 -622
  30. claude_mpm/core/config.py +5 -1
  31. claude_mpm/core/constants.py +339 -0
  32. claude_mpm/core/container.py +461 -22
  33. claude_mpm/core/exceptions.py +392 -0
  34. claude_mpm/core/framework_loader.py +208 -93
  35. claude_mpm/core/interactive_session.py +432 -0
  36. claude_mpm/core/interfaces.py +424 -0
  37. claude_mpm/core/lazy.py +467 -0
  38. claude_mpm/core/logging_config.py +444 -0
  39. claude_mpm/core/oneshot_session.py +465 -0
  40. claude_mpm/core/optimized_agent_loader.py +485 -0
  41. claude_mpm/core/optimized_startup.py +490 -0
  42. claude_mpm/core/service_registry.py +52 -26
  43. claude_mpm/core/socketio_pool.py +162 -5
  44. claude_mpm/core/types.py +292 -0
  45. claude_mpm/core/typing_utils.py +477 -0
  46. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +46 -2
  47. claude_mpm/dashboard/templates/index.html +5 -5
  48. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  49. claude_mpm/init.py +2 -1
  50. claude_mpm/services/__init__.py +78 -14
  51. claude_mpm/services/agent/__init__.py +24 -0
  52. claude_mpm/services/agent/deployment.py +2548 -0
  53. claude_mpm/services/agent/management.py +598 -0
  54. claude_mpm/services/agent/registry.py +813 -0
  55. claude_mpm/services/agents/deployment/agent_deployment.py +592 -269
  56. claude_mpm/services/agents/deployment/async_agent_deployment.py +5 -1
  57. claude_mpm/services/agents/management/agent_capabilities_generator.py +21 -11
  58. claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
  59. claude_mpm/services/async_session_logger.py +8 -3
  60. claude_mpm/services/communication/__init__.py +21 -0
  61. claude_mpm/services/communication/socketio.py +1933 -0
  62. claude_mpm/services/communication/websocket.py +479 -0
  63. claude_mpm/services/core/__init__.py +123 -0
  64. claude_mpm/services/core/base.py +247 -0
  65. claude_mpm/services/core/interfaces.py +951 -0
  66. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  67. claude_mpm/services/framework_claude_md_generator.py +3 -2
  68. claude_mpm/services/health_monitor.py +4 -3
  69. claude_mpm/services/hook_service.py +64 -4
  70. claude_mpm/services/infrastructure/__init__.py +21 -0
  71. claude_mpm/services/infrastructure/logging.py +202 -0
  72. claude_mpm/services/infrastructure/monitoring.py +893 -0
  73. claude_mpm/services/memory/indexed_memory.py +648 -0
  74. claude_mpm/services/project/__init__.py +21 -0
  75. claude_mpm/services/project/analyzer.py +864 -0
  76. claude_mpm/services/project/registry.py +608 -0
  77. claude_mpm/services/project_analyzer.py +95 -2
  78. claude_mpm/services/recovery_manager.py +15 -9
  79. claude_mpm/services/socketio/__init__.py +25 -0
  80. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  81. claude_mpm/services/socketio/handlers/base.py +121 -0
  82. claude_mpm/services/socketio/handlers/connection.py +198 -0
  83. claude_mpm/services/socketio/handlers/file.py +213 -0
  84. claude_mpm/services/socketio/handlers/git.py +723 -0
  85. claude_mpm/services/socketio/handlers/memory.py +27 -0
  86. claude_mpm/services/socketio/handlers/project.py +25 -0
  87. claude_mpm/services/socketio/handlers/registry.py +145 -0
  88. claude_mpm/services/socketio_client_manager.py +12 -7
  89. claude_mpm/services/socketio_server.py +156 -30
  90. claude_mpm/services/ticket_manager.py +377 -51
  91. claude_mpm/utils/agent_dependency_loader.py +66 -15
  92. claude_mpm/utils/error_handler.py +1 -1
  93. claude_mpm/utils/robust_installer.py +587 -0
  94. claude_mpm/validation/agent_validator.py +27 -14
  95. claude_mpm/validation/frontmatter_validator.py +231 -0
  96. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +74 -41
  97. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +101 -76
  98. claude_mpm/.claude-mpm/logs/hooks_20250728.log +0 -10
  99. claude_mpm/agents/agent-template.yaml +0 -83
  100. claude_mpm/cli/README.md +0 -108
  101. claude_mpm/cli_module/refactoring_guide.md +0 -253
  102. claude_mpm/config/async_logging_config.yaml +0 -145
  103. claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +0 -34
  104. claude_mpm/dashboard/.claude-mpm/memories/README.md +0 -36
  105. claude_mpm/dashboard/README.md +0 -121
  106. claude_mpm/dashboard/static/js/dashboard.js.backup +0 -1973
  107. claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +0 -36
  108. claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +0 -39
  109. claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +0 -38
  110. claude_mpm/hooks/README.md +0 -96
  111. claude_mpm/schemas/agent_schema.json +0 -435
  112. claude_mpm/services/framework_claude_md_generator/README.md +0 -92
  113. claude_mpm/services/version_control/VERSION +0 -1
  114. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
  115. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
  116. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
  117. {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,587 @@
1
+ """
2
+ Robust dependency installer with retry logic and fallback strategies.
3
+
4
+ WHY: Network issues and temporary unavailability can cause dependency installation
5
+ to fail. This module provides resilient installation with automatic retries,
6
+ fallback strategies, and clear error reporting.
7
+
8
+ DESIGN DECISION: We implement exponential backoff for retries and provide
9
+ multiple installation strategies (pip, conda, source) to maximize success rate.
10
+ """
11
+
12
+ import subprocess
13
+ import sys
14
+ import time
15
+ import json
16
+ import re
17
+ from pathlib import Path
18
+ from typing import List, Tuple, Optional, Dict, Any
19
+ from enum import Enum
20
+ from dataclasses import dataclass
21
+
22
+ from ..core.logger import get_logger
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class InstallStrategy(Enum):
28
+ """Available installation strategies."""
29
+ PIP = "pip"
30
+ PIP_NO_DEPS = "pip_no_deps"
31
+ PIP_UPGRADE = "pip_upgrade"
32
+ PIP_INDEX_URL = "pip_index_url"
33
+ SOURCE = "source"
34
+
35
+
36
+ @dataclass
37
+ class InstallAttempt:
38
+ """Record of an installation attempt."""
39
+ strategy: InstallStrategy
40
+ package: str
41
+ success: bool
42
+ error: Optional[str]
43
+ duration: float
44
+ retry_count: int
45
+
46
+
47
+ class RobustPackageInstaller:
48
+ """
49
+ Robust package installer with retry logic and multiple strategies.
50
+
51
+ WHY: This class handles the complexity of package installation in various
52
+ environments, network conditions, and Python versions. It ensures maximum
53
+ success rate while providing clear feedback on failures.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ max_retries: int = 3,
59
+ retry_delay: float = 2.0,
60
+ timeout: int = 300,
61
+ use_cache: bool = True
62
+ ):
63
+ """
64
+ Initialize robust installer.
65
+
66
+ Args:
67
+ max_retries: Maximum number of retry attempts per package
68
+ retry_delay: Initial delay between retries (uses exponential backoff)
69
+ timeout: Maximum time for each installation attempt in seconds
70
+ use_cache: Whether to use pip cache
71
+ """
72
+ self.max_retries = max_retries
73
+ self.retry_delay = retry_delay
74
+ self.timeout = timeout
75
+ self.use_cache = use_cache
76
+ self.attempts: List[InstallAttempt] = []
77
+ self.success_cache: Dict[str, bool] = {}
78
+
79
+ def install_package(
80
+ self,
81
+ package_spec: str,
82
+ strategies: Optional[List[InstallStrategy]] = None
83
+ ) -> Tuple[bool, Optional[str]]:
84
+ """
85
+ Install a package using robust retry logic and multiple strategies.
86
+
87
+ WHY: Single installation attempts often fail due to transient issues.
88
+ This method tries multiple strategies with retries to maximize success.
89
+
90
+ Args:
91
+ package_spec: Package specification (e.g., "pandas>=2.0.0")
92
+ strategies: List of strategies to try (defaults to sensible order)
93
+
94
+ Returns:
95
+ Tuple of (success, error_message)
96
+ """
97
+ # Check success cache first
98
+ if package_spec in self.success_cache:
99
+ if self.success_cache[package_spec]:
100
+ logger.debug(f"Package {package_spec} already successfully installed")
101
+ return True, None
102
+
103
+ # Default strategy order
104
+ if strategies is None:
105
+ strategies = [
106
+ InstallStrategy.PIP,
107
+ InstallStrategy.PIP_UPGRADE,
108
+ InstallStrategy.PIP_NO_DEPS,
109
+ InstallStrategy.PIP_INDEX_URL,
110
+ ]
111
+
112
+ # Extract package name for special handling
113
+ package_name = self._extract_package_name(package_spec)
114
+
115
+ # Special handling for known problematic packages
116
+ if self._needs_special_handling(package_name):
117
+ strategies = self._get_special_strategies(package_name)
118
+
119
+ # Try each strategy with retries
120
+ for strategy in strategies:
121
+ for retry in range(self.max_retries):
122
+ start_time = time.time()
123
+
124
+ # Calculate delay with exponential backoff
125
+ if retry > 0:
126
+ delay = self.retry_delay * (2 ** (retry - 1))
127
+ logger.info(f"Retry {retry}/{self.max_retries} after {delay:.1f}s delay...")
128
+ time.sleep(delay)
129
+
130
+ # Attempt installation
131
+ success, error = self._attempt_install(package_spec, strategy)
132
+ duration = time.time() - start_time
133
+
134
+ # Record attempt
135
+ self.attempts.append(InstallAttempt(
136
+ strategy=strategy,
137
+ package=package_spec,
138
+ success=success,
139
+ error=error,
140
+ duration=duration,
141
+ retry_count=retry
142
+ ))
143
+
144
+ if success:
145
+ logger.info(f"Successfully installed {package_spec} using {strategy.value}")
146
+ self.success_cache[package_spec] = True
147
+ return True, None
148
+
149
+ # Check if error is retryable
150
+ if not self._is_retryable_error(error):
151
+ logger.warning(f"Non-retryable error for {package_spec}: {error}")
152
+ break
153
+
154
+ # All attempts failed
155
+ self.success_cache[package_spec] = False
156
+ final_error = self._get_consolidated_error(package_spec)
157
+ return False, final_error
158
+
159
+ def _attempt_install(
160
+ self,
161
+ package_spec: str,
162
+ strategy: InstallStrategy
163
+ ) -> Tuple[bool, Optional[str]]:
164
+ """
165
+ Attempt to install a package using a specific strategy.
166
+
167
+ Args:
168
+ package_spec: Package specification
169
+ strategy: Installation strategy to use
170
+
171
+ Returns:
172
+ Tuple of (success, error_message)
173
+ """
174
+ try:
175
+ cmd = self._build_install_command(package_spec, strategy)
176
+ logger.debug(f"Running: {' '.join(cmd)}")
177
+
178
+ result = subprocess.run(
179
+ cmd,
180
+ capture_output=True,
181
+ text=True,
182
+ timeout=self.timeout
183
+ )
184
+
185
+ if result.returncode == 0:
186
+ # Verify installation
187
+ if self._verify_installation(package_spec):
188
+ return True, None
189
+ else:
190
+ return False, "Package installed but verification failed"
191
+ else:
192
+ error_msg = self._extract_error_message(result.stderr)
193
+ logger.debug(f"Installation failed: {error_msg}")
194
+ return False, error_msg
195
+
196
+ except subprocess.TimeoutExpired:
197
+ return False, f"Installation timed out after {self.timeout}s"
198
+ except Exception as e:
199
+ return False, f"Unexpected error: {str(e)}"
200
+
201
+ def _build_install_command(
202
+ self,
203
+ package_spec: str,
204
+ strategy: InstallStrategy
205
+ ) -> List[str]:
206
+ """
207
+ Build the installation command for a given strategy.
208
+
209
+ Args:
210
+ package_spec: Package specification
211
+ strategy: Installation strategy
212
+
213
+ Returns:
214
+ Command as list of arguments
215
+ """
216
+ base_cmd = [sys.executable, "-m", "pip", "install"]
217
+
218
+ # Add cache control
219
+ if not self.use_cache:
220
+ base_cmd.append("--no-cache-dir")
221
+
222
+ if strategy == InstallStrategy.PIP:
223
+ return base_cmd + [package_spec]
224
+
225
+ elif strategy == InstallStrategy.PIP_NO_DEPS:
226
+ return base_cmd + ["--no-deps", package_spec]
227
+
228
+ elif strategy == InstallStrategy.PIP_UPGRADE:
229
+ return base_cmd + ["--upgrade", package_spec]
230
+
231
+ elif strategy == InstallStrategy.PIP_INDEX_URL:
232
+ # Try alternative index (PyPI mirror)
233
+ return base_cmd + [
234
+ "--index-url", "https://pypi.org/simple",
235
+ "--extra-index-url", "https://pypi.python.org/simple",
236
+ package_spec
237
+ ]
238
+
239
+ else:
240
+ return base_cmd + [package_spec]
241
+
242
+ def _extract_package_name(self, package_spec: str) -> str:
243
+ """
244
+ Extract package name from specification.
245
+
246
+ Args:
247
+ package_spec: Package specification (e.g., "pandas>=2.0.0")
248
+
249
+ Returns:
250
+ Package name (e.g., "pandas")
251
+ """
252
+ # Remove version specifiers
253
+ match = re.match(r'^([a-zA-Z0-9_-]+)', package_spec)
254
+ if match:
255
+ return match.group(1)
256
+ return package_spec
257
+
258
+ def _needs_special_handling(self, package_name: str) -> bool:
259
+ """
260
+ Check if package needs special installation handling.
261
+
262
+ Args:
263
+ package_name: Name of the package
264
+
265
+ Returns:
266
+ True if package needs special handling
267
+ """
268
+ # Known problematic packages
269
+ special_packages = {
270
+ 'tree-sitter-ruby',
271
+ 'tree-sitter-php',
272
+ 'tree-sitter-javascript',
273
+ 'tree-sitter-typescript',
274
+ 'tree-sitter-go',
275
+ 'tree-sitter-rust',
276
+ 'tree-sitter-java',
277
+ 'tree-sitter-cpp',
278
+ 'tree-sitter-c',
279
+ }
280
+
281
+ return package_name.lower() in special_packages
282
+
283
+ def _get_special_strategies(self, package_name: str) -> List[InstallStrategy]:
284
+ """
285
+ Get special installation strategies for problematic packages.
286
+
287
+ Args:
288
+ package_name: Name of the package
289
+
290
+ Returns:
291
+ List of strategies to try
292
+ """
293
+ # For tree-sitter packages, try upgrade first (often fixes version conflicts)
294
+ if package_name.startswith('tree-sitter-'):
295
+ return [
296
+ InstallStrategy.PIP_UPGRADE,
297
+ InstallStrategy.PIP,
298
+ InstallStrategy.PIP_INDEX_URL,
299
+ InstallStrategy.PIP_NO_DEPS,
300
+ ]
301
+
302
+ return [InstallStrategy.PIP, InstallStrategy.PIP_UPGRADE]
303
+
304
+ def _verify_installation(self, package_spec: str) -> bool:
305
+ """
306
+ Verify that a package was successfully installed.
307
+
308
+ Args:
309
+ package_spec: Package specification
310
+
311
+ Returns:
312
+ True if package is installed and importable
313
+ """
314
+ package_name = self._extract_package_name(package_spec)
315
+
316
+ # Convert package name to import name (e.g., tree-sitter-ruby -> tree_sitter_ruby)
317
+ import_name = package_name.replace('-', '_')
318
+
319
+ try:
320
+ # Check if package is installed
321
+ import importlib.metadata
322
+ try:
323
+ version = importlib.metadata.version(package_name)
324
+ logger.debug(f"Package {package_name} version {version} is installed")
325
+
326
+ # For tree-sitter packages, don't try to import (they have C extensions)
327
+ if package_name.startswith('tree-sitter-'):
328
+ return True
329
+
330
+ # Try to import the package
331
+ try:
332
+ __import__(import_name)
333
+ return True
334
+ except ImportError:
335
+ # Some packages have different import names, that's OK
336
+ return True
337
+
338
+ except importlib.metadata.PackageNotFoundError:
339
+ return False
340
+
341
+ except ImportError:
342
+ # Fallback for older Python versions
343
+ try:
344
+ import pkg_resources
345
+ pkg_resources.get_distribution(package_name)
346
+ return True
347
+ except pkg_resources.DistributionNotFound:
348
+ return False
349
+
350
+ def _is_retryable_error(self, error: Optional[str]) -> bool:
351
+ """
352
+ Determine if an error is worth retrying.
353
+
354
+ Args:
355
+ error: Error message
356
+
357
+ Returns:
358
+ True if error is retryable
359
+ """
360
+ if not error:
361
+ return False
362
+
363
+ # Retryable error patterns
364
+ retryable_patterns = [
365
+ 'connection', 'timeout', 'temporary failure',
366
+ 'network', 'unreachable', 'could not find',
367
+ 'no matching distribution', 'httperror',
368
+ 'http error', 'ssl', 'certificate',
369
+ 'readtimeout', 'connectionerror'
370
+ ]
371
+
372
+ error_lower = error.lower()
373
+ return any(pattern in error_lower for pattern in retryable_patterns)
374
+
375
+ def _extract_error_message(self, stderr: str) -> str:
376
+ """
377
+ Extract meaningful error message from pip stderr.
378
+
379
+ Args:
380
+ stderr: Standard error output from pip
381
+
382
+ Returns:
383
+ Extracted error message
384
+ """
385
+ if not stderr:
386
+ return "Unknown error"
387
+
388
+ # Look for ERROR: lines
389
+ error_lines = []
390
+ for line in stderr.splitlines():
391
+ if 'ERROR:' in line:
392
+ error_lines.append(line.split('ERROR:', 1)[1].strip())
393
+
394
+ if error_lines:
395
+ return ' | '.join(error_lines)
396
+
397
+ # Fall back to last non-empty line
398
+ lines = [l.strip() for l in stderr.splitlines() if l.strip()]
399
+ if lines:
400
+ return lines[-1]
401
+
402
+ return "Installation failed"
403
+
404
+ def _get_consolidated_error(self, package_spec: str) -> str:
405
+ """
406
+ Get a consolidated error message from all attempts.
407
+
408
+ Args:
409
+ package_spec: Package specification that failed
410
+
411
+ Returns:
412
+ Consolidated error message
413
+ """
414
+ # Get unique error messages from attempts
415
+ errors = set()
416
+ for attempt in self.attempts:
417
+ if attempt.package == package_spec and attempt.error:
418
+ errors.add(attempt.error)
419
+
420
+ if not errors:
421
+ return f"Failed to install {package_spec} after {len(self.attempts)} attempts"
422
+
423
+ # Format error message
424
+ if len(errors) == 1:
425
+ return list(errors)[0]
426
+ else:
427
+ return f"Multiple errors: {' | '.join(errors)}"
428
+
429
+ def install_packages(
430
+ self,
431
+ packages: List[str],
432
+ parallel: bool = False
433
+ ) -> Tuple[List[str], List[str], Dict[str, str]]:
434
+ """
435
+ Install multiple packages with robust error handling.
436
+
437
+ Args:
438
+ packages: List of package specifications
439
+ parallel: Whether to attempt parallel installation
440
+
441
+ Returns:
442
+ Tuple of (successful_packages, failed_packages, error_map)
443
+ """
444
+ successful = []
445
+ failed = []
446
+ errors = {}
447
+
448
+ # Group packages that can be installed together
449
+ if parallel and len(packages) > 1:
450
+ # Try to install all at once first
451
+ logger.info(f"Attempting batch installation of {len(packages)} packages...")
452
+ success, error = self._attempt_batch_install(packages)
453
+
454
+ if success:
455
+ logger.info("Batch installation successful")
456
+ return packages, [], {}
457
+ else:
458
+ logger.warning(f"Batch installation failed: {error}")
459
+ logger.info("Falling back to individual installation...")
460
+
461
+ # Install packages individually
462
+ for i, package in enumerate(packages, 1):
463
+ logger.info(f"Installing package {i}/{len(packages)}: {package}")
464
+
465
+ success, error = self.install_package(package)
466
+
467
+ if success:
468
+ successful.append(package)
469
+ else:
470
+ failed.append(package)
471
+ errors[package] = error or "Unknown error"
472
+
473
+ return successful, failed, errors
474
+
475
+ def _attempt_batch_install(self, packages: List[str]) -> Tuple[bool, Optional[str]]:
476
+ """
477
+ Attempt to install multiple packages in a single pip command.
478
+
479
+ Args:
480
+ packages: List of package specifications
481
+
482
+ Returns:
483
+ Tuple of (success, error_message)
484
+ """
485
+ try:
486
+ cmd = [sys.executable, "-m", "pip", "install"] + packages
487
+
488
+ result = subprocess.run(
489
+ cmd,
490
+ capture_output=True,
491
+ text=True,
492
+ timeout=self.timeout * 2 # Longer timeout for batch
493
+ )
494
+
495
+ if result.returncode == 0:
496
+ # Verify all packages
497
+ all_verified = all(
498
+ self._verify_installation(pkg) for pkg in packages
499
+ )
500
+ if all_verified:
501
+ return True, None
502
+ else:
503
+ return False, "Some packages failed verification"
504
+ else:
505
+ error_msg = self._extract_error_message(result.stderr)
506
+ return False, error_msg
507
+
508
+ except subprocess.TimeoutExpired:
509
+ return False, f"Batch installation timed out"
510
+ except Exception as e:
511
+ return False, f"Batch installation error: {str(e)}"
512
+
513
+ def get_report(self) -> str:
514
+ """
515
+ Generate a report of installation attempts.
516
+
517
+ Returns:
518
+ Formatted report string
519
+ """
520
+ lines = []
521
+ lines.append("=" * 60)
522
+ lines.append("INSTALLATION REPORT")
523
+ lines.append("=" * 60)
524
+
525
+ # Summary
526
+ total_attempts = len(self.attempts)
527
+ successful = sum(1 for a in self.attempts if a.success)
528
+ failed = total_attempts - successful
529
+
530
+ lines.append(f"Total attempts: {total_attempts}")
531
+ lines.append(f"Successful: {successful}")
532
+ lines.append(f"Failed: {failed}")
533
+ lines.append("")
534
+
535
+ # Details by package
536
+ packages = {}
537
+ for attempt in self.attempts:
538
+ if attempt.package not in packages:
539
+ packages[attempt.package] = []
540
+ packages[attempt.package].append(attempt)
541
+
542
+ for package, attempts in packages.items():
543
+ success = any(a.success for a in attempts)
544
+ status = "✓" if success else "✗"
545
+ lines.append(f"{status} {package}:")
546
+
547
+ for attempt in attempts:
548
+ retry_str = f" (retry {attempt.retry_count})" if attempt.retry_count > 0 else ""
549
+ result = "success" if attempt.success else f"failed: {attempt.error}"
550
+ lines.append(f" - {attempt.strategy.value}{retry_str}: {result}")
551
+
552
+ lines.append("=" * 60)
553
+ return "\n".join(lines)
554
+
555
+
556
+ def install_with_retry(
557
+ packages: List[str],
558
+ max_retries: int = 3,
559
+ verbose: bool = False
560
+ ) -> Tuple[bool, str]:
561
+ """
562
+ Convenience function to install packages with retry logic.
563
+
564
+ Args:
565
+ packages: List of package specifications
566
+ max_retries: Maximum retry attempts
567
+ verbose: Whether to print verbose output
568
+
569
+ Returns:
570
+ Tuple of (all_success, error_message)
571
+ """
572
+ if verbose:
573
+ import logging
574
+ logging.getLogger().setLevel(logging.DEBUG)
575
+
576
+ installer = RobustPackageInstaller(max_retries=max_retries)
577
+ successful, failed, errors = installer.install_packages(packages)
578
+
579
+ if verbose:
580
+ print(installer.get_report())
581
+
582
+ if failed:
583
+ error_msg = f"Failed to install {len(failed)} packages: "
584
+ error_msg += ", ".join(f"{pkg} ({errors[pkg]})" for pkg in failed)
585
+ return False, error_msg
586
+
587
+ return True, ""
@@ -28,6 +28,14 @@ from datetime import datetime
28
28
  import jsonschema
29
29
  from jsonschema import validate, ValidationError, Draft7Validator
30
30
  from claude_mpm.config.paths import paths
31
+ from claude_mpm.core.constants import (
32
+ SystemLimits,
33
+ ResourceLimits,
34
+ TimeoutConfig,
35
+ ComplexityMetrics,
36
+ ErrorMessages,
37
+ ValidationRules
38
+ )
31
39
 
32
40
  logger = logging.getLogger(__name__)
33
41
 
@@ -198,8 +206,11 @@ class AgentValidator:
198
206
  # SECURITY: Validate instruction length to prevent memory exhaustion
199
207
  # Double-check even though schema enforces this - defense in depth
200
208
  instructions = agent_data.get("instructions", "")
201
- if len(instructions) > 8000:
202
- result.errors.append(f"Instructions exceed 8000 character limit: {len(instructions)} characters")
209
+ if len(instructions) > SystemLimits.MAX_INSTRUCTION_LENGTH:
210
+ result.errors.append(ErrorMessages.INSTRUCTION_TOO_LONG.format(
211
+ limit=SystemLimits.MAX_INSTRUCTION_LENGTH,
212
+ actual=len(instructions)
213
+ ))
203
214
  result.is_valid = False
204
215
 
205
216
  # Validate model compatibility with tools
@@ -237,19 +248,19 @@ class AgentValidator:
237
248
  """
238
249
  tier_limits = {
239
250
  "intensive": {
240
- "memory_limit": (4096, 8192),
241
- "cpu_limit": (60, 100),
242
- "timeout": (600, 3600)
251
+ "memory_limit": ResourceLimits.INTENSIVE_MEMORY_RANGE,
252
+ "cpu_limit": ResourceLimits.INTENSIVE_CPU_RANGE,
253
+ "timeout": TimeoutConfig.INTENSIVE_TIMEOUT_RANGE
243
254
  },
244
255
  "standard": {
245
- "memory_limit": (2048, 4096),
246
- "cpu_limit": (30, 60),
247
- "timeout": (300, 1200)
256
+ "memory_limit": ResourceLimits.STANDARD_MEMORY_RANGE,
257
+ "cpu_limit": ResourceLimits.STANDARD_CPU_RANGE,
258
+ "timeout": TimeoutConfig.STANDARD_TIMEOUT_RANGE
248
259
  },
249
260
  "lightweight": {
250
- "memory_limit": (512, 2048),
251
- "cpu_limit": (10, 30),
252
- "timeout": (30, 600)
261
+ "memory_limit": ResourceLimits.LIGHTWEIGHT_MEMORY_RANGE,
262
+ "cpu_limit": ResourceLimits.LIGHTWEIGHT_CPU_RANGE,
263
+ "timeout": TimeoutConfig.LIGHTWEIGHT_TIMEOUT_RANGE
253
264
  }
254
265
  }
255
266
 
@@ -345,9 +356,11 @@ class AgentValidator:
345
356
 
346
357
  # SECURITY: Check file size to prevent memory exhaustion
347
358
  file_size = file_path.stat().st_size
348
- max_size = 1024 * 1024 # 1MB limit for agent configs
359
+ max_size = SystemLimits.MAX_AGENT_CONFIG_SIZE
349
360
  if file_size > max_size:
350
- raise ValueError(f"File too large: {file_size} bytes (max {max_size} bytes)")
361
+ raise ValueError(ErrorMessages.FILE_TOO_LARGE.format(
362
+ limit=max_size
363
+ ))
351
364
  with open(file_path, 'r') as f:
352
365
  agent_data = json.load(f)
353
366
 
@@ -381,7 +394,7 @@ class AgentValidator:
381
394
  raise ValueError(f"Path is not a directory: {directory}")
382
395
 
383
396
  # SECURITY: Limit number of files to prevent DoS
384
- max_files = 100
397
+ max_files = SystemLimits.MAX_FILES_TO_VALIDATE
385
398
  file_count = 0
386
399
 
387
400
  for json_file in directory.glob("*.json"):