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
@@ -34,13 +34,20 @@ import time
34
34
  from pathlib import Path
35
35
  from typing import Optional, List, Dict, Any
36
36
 
37
- from claude_mpm.core.logger import get_logger
37
+ from claude_mpm.core.logging_config import get_logger, log_operation, log_performance_context
38
+ from claude_mpm.core.exceptions import AgentDeploymentError
38
39
  from claude_mpm.constants import EnvironmentVars, Paths, AgentMetadata
39
40
  from claude_mpm.config.paths import paths
40
41
  from claude_mpm.core.config import Config
42
+ from claude_mpm.core.constants import (
43
+ TimeoutConfig,
44
+ SystemLimits,
45
+ ResourceLimits
46
+ )
47
+ from claude_mpm.core.interfaces import AgentDeploymentInterface
41
48
 
42
49
 
43
- class AgentDeploymentService:
50
+ class AgentDeploymentService(AgentDeploymentInterface):
44
51
  """Service for deploying Claude Code native agents.
45
52
 
46
53
  METRICS COLLECTION OPPORTUNITIES:
@@ -85,7 +92,7 @@ class AgentDeploymentService:
85
92
  - Base agent loading time
86
93
  - Initial validation overhead
87
94
  """
88
- self.logger = get_logger(self.__class__.__name__)
95
+ self.logger = get_logger(__name__)
89
96
 
90
97
  # METRICS: Initialize deployment metrics tracking
91
98
  # This data structure would be used for collecting deployment telemetry
@@ -207,125 +214,36 @@ class AgentDeploymentService:
207
214
 
208
215
  # Try async deployment for better performance if requested
209
216
  if use_async:
210
- try:
211
- from .async_agent_deployment import deploy_agents_async_wrapper
212
- self.logger.info("Using async deployment for improved performance")
213
-
214
- # Run async deployment
215
- results = deploy_agents_async_wrapper(
216
- templates_dir=self.templates_dir,
217
- base_agent_path=self.base_agent_path,
218
- working_directory=self.working_directory,
219
- target_dir=target_dir,
220
- force_rebuild=force_rebuild,
221
- config=config
222
- )
223
-
224
- # Add metrics about async vs sync
225
- if 'metrics' in results:
226
- results['metrics']['deployment_method'] = 'async'
227
- duration_ms = results['metrics'].get('duration_ms', 0)
228
- self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
229
-
230
- # Update internal metrics
231
- self._deployment_metrics['total_deployments'] += 1
232
- if not results.get('errors'):
233
- self._deployment_metrics['successful_deployments'] += 1
234
- else:
235
- self._deployment_metrics['failed_deployments'] += 1
236
-
237
- return results
238
-
239
- except ImportError:
240
- self.logger.warning("Async deployment not available, falling back to sync")
241
- except Exception as e:
242
- self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
217
+ async_results = self._try_async_deployment(
218
+ target_dir=target_dir,
219
+ force_rebuild=force_rebuild,
220
+ config=config,
221
+ deployment_start_time=deployment_start_time
222
+ )
223
+ if async_results is not None:
224
+ return async_results
243
225
 
244
226
  # Continue with synchronous deployment
245
227
  self.logger.info("Using synchronous deployment")
246
228
 
247
- # Load configuration if not provided
248
- if config is None:
249
- config = Config()
250
-
251
- # Get agent exclusion configuration
252
- excluded_agents = config.get('agent_deployment.excluded_agents', [])
253
- case_sensitive = config.get('agent_deployment.case_sensitive', False)
254
- exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
255
-
256
- # Normalize excluded agents list for comparison
257
- if not case_sensitive:
258
- excluded_agents = [agent.lower() for agent in excluded_agents]
259
-
260
- # Log exclusion configuration if agents are being excluded
261
- if excluded_agents:
262
- self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
263
- self.logger.debug(f"Case sensitive matching: {case_sensitive}")
264
- self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
229
+ # Load and process configuration
230
+ config, excluded_agents = self._load_deployment_config(config)
265
231
 
266
- if not target_dir:
267
- # Default to working directory's .claude/agents directory (not home)
268
- # This ensures we deploy to the user's project directory, not the framework directory
269
- agents_dir = self.working_directory / ".claude" / "agents"
270
- else:
271
- # If target_dir provided, use it directly (caller decides structure)
272
- # This allows for both passing a project dir or the full agents path
273
- target_dir = Path(target_dir)
274
- # Check if this is already an agents directory
275
- if target_dir.name == "agents":
276
- # Already an agents directory, use as-is
277
- agents_dir = target_dir
278
- elif target_dir.name == ".claude-mpm":
279
- # .claude-mpm directory, add agents subdirectory
280
- agents_dir = target_dir / "agents"
281
- elif target_dir.name == ".claude":
282
- # .claude directory, add agents subdirectory
283
- agents_dir = target_dir / "agents"
284
- else:
285
- # Assume it's a project directory, add .claude/agents
286
- agents_dir = target_dir / ".claude" / "agents"
232
+ # Determine target agents directory
233
+ agents_dir = self._determine_agents_directory(target_dir)
287
234
 
288
- results = {
289
- "target_dir": str(agents_dir),
290
- "deployed": [],
291
- "errors": [],
292
- "skipped": [],
293
- "updated": [],
294
- "migrated": [], # Track agents migrated from old format
295
- "converted": [], # Track YAML to MD conversions
296
- "repaired": [], # Track agents with repaired frontmatter
297
- "total": 0,
298
- # METRICS: Add detailed timing and performance data to results
299
- "metrics": {
300
- "start_time": deployment_start_time,
301
- "end_time": None,
302
- "duration_ms": None,
303
- "agent_timings": {}, # Track individual agent deployment times
304
- "validation_times": {}, # Track template validation times
305
- "resource_usage": {} # Could track memory/CPU if needed
306
- }
307
- }
235
+ # Initialize results dictionary
236
+ results = self._initialize_deployment_results(agents_dir, deployment_start_time)
308
237
 
309
238
  try:
310
239
  # Create agents directory if needed
311
240
  agents_dir.mkdir(parents=True, exist_ok=True)
312
241
 
313
242
  # STEP 0: Validate and repair broken frontmatter in existing agents
314
- # This ensures all existing agents have valid YAML frontmatter before deploying new ones
315
- repair_results = self._validate_and_repair_existing_agents(agents_dir)
316
- if repair_results["repaired"]:
317
- results["repaired"] = repair_results["repaired"]
318
- self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
319
- for agent_name in repair_results["repaired"]:
320
- self.logger.debug(f" - Repaired: {agent_name}")
321
-
322
- # Determine source tier for logging
323
- source_tier = "SYSTEM"
324
- if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
325
- source_tier = "PROJECT"
326
- elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
327
- source_tier = "USER"
243
+ self._repair_existing_agents(agents_dir, results)
328
244
 
245
+ # Log deployment source tier
246
+ source_tier = self._determine_source_tier()
329
247
  self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
330
248
 
331
249
  # Note: System instructions are now loaded directly by SimpleClaudeRunner
@@ -342,162 +260,23 @@ class AgentDeploymentService:
342
260
  results["converted"] = conversion_results.get("converted", [])
343
261
 
344
262
  # Load base agent content
345
- # OPERATIONAL NOTE: Base agent contains shared configuration and instructions
346
- # that all agents inherit. This reduces duplication and ensures consistency.
347
- # If base agent fails to load, deployment continues with agent-specific configs only.
348
- base_agent_data = {}
349
- base_agent_version = 0
350
- if self.base_agent_path.exists():
351
- try:
352
- import json
353
- base_agent_data = json.loads(self.base_agent_path.read_text())
354
- # Handle both 'base_version' (new format) and 'version' (old format)
355
- # MIGRATION PATH: Supporting both formats during transition period
356
- base_agent_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
357
- self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
358
- except Exception as e:
359
- # NON-FATAL: Base agent is optional enhancement, not required
360
- self.logger.warning(f"Could not load base agent: {e}")
361
-
362
- # Get all template files
363
- template_files = list(self.templates_dir.glob("*.json"))
364
-
365
- # Build the combined exclusion set
366
- # Start with hardcoded exclusions (these are ALWAYS excluded)
367
- hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS", "README", "pm", "PM", "project_manager"}
368
-
369
- # Add user-configured exclusions
370
- user_exclusions = set()
371
- for agent in excluded_agents:
372
- if case_sensitive:
373
- user_exclusions.add(agent)
374
- else:
375
- # For case-insensitive, we'll check during filtering
376
- user_exclusions.add(agent.lower())
377
-
378
- # Filter out excluded agents
379
- filtered_files = []
380
- excluded_count = 0
381
- for f in template_files:
382
- agent_name = f.stem
383
-
384
- # Check hardcoded exclusions (always case-sensitive)
385
- if agent_name in hardcoded_exclusions:
386
- self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
387
- excluded_count += 1
388
- continue
389
-
390
- # Check file patterns
391
- if agent_name.startswith(".") or agent_name.endswith(".backup"):
392
- self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
393
- excluded_count += 1
394
- continue
395
-
396
- # Check user-configured exclusions
397
- compare_name = agent_name.lower() if not case_sensitive else agent_name
398
- if compare_name in user_exclusions:
399
- self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
400
- excluded_count += 1
401
- continue
402
-
403
- # Agent is not excluded, add to filtered list
404
- filtered_files.append(f)
263
+ base_agent_data, base_agent_version = self._load_base_agent()
405
264
 
406
- template_files = filtered_files
265
+ # Get and filter template files
266
+ template_files = self._get_filtered_templates(excluded_agents, config)
407
267
  results["total"] = len(template_files)
408
268
 
409
- if excluded_count > 0:
410
- self.logger.info(f"Excluded {excluded_count} agents from deployment")
411
-
269
+ # Deploy each agent template
412
270
  for template_file in template_files:
413
- try:
414
- # METRICS: Track individual agent deployment time
415
- agent_start_time = time.time()
416
-
417
- agent_name = template_file.stem
418
- target_file = agents_dir / f"{agent_name}.md"
419
-
420
- # Check if agent needs update
421
- needs_update = force_rebuild
422
- is_migration = False
423
-
424
- # In project deployment mode, always deploy regardless of version
425
- if deployment_mode == "project":
426
- if target_file.exists():
427
- # Check if it's a system agent that we should update
428
- needs_update = True
429
- self.logger.debug(f"Project deployment mode: will deploy {agent_name}")
430
- else:
431
- needs_update = True
432
- elif not needs_update and target_file.exists():
433
- # In update mode, check version compatibility
434
- needs_update, reason = self._check_agent_needs_update(
435
- target_file, template_file, base_agent_version
436
- )
437
- if needs_update:
438
- # Check if this is a migration from old format
439
- if "migration needed" in reason:
440
- is_migration = True
441
- self.logger.info(f"Migrating agent {agent_name}: {reason}")
442
- else:
443
- self.logger.info(f"Agent {agent_name} needs update: {reason}")
444
-
445
- # Skip if exists and doesn't need update (only in update mode)
446
- if target_file.exists() and not needs_update and deployment_mode != "project":
447
- results["skipped"].append(agent_name)
448
- self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
449
- continue
450
-
451
- # Build the agent file as markdown with YAML frontmatter
452
- agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
453
-
454
- # Write the agent file
455
- is_update = target_file.exists()
456
- target_file.write_text(agent_content)
457
-
458
- # METRICS: Record deployment time for this agent
459
- agent_deployment_time = (time.time() - agent_start_time) * 1000 # Convert to ms
460
- results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
461
-
462
- # METRICS: Update agent type deployment counts
463
- self._deployment_metrics['agent_type_counts'][agent_name] = \
464
- self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
465
-
466
- if is_migration:
467
- results["migrated"].append({
468
- "name": agent_name,
469
- "template": str(template_file),
470
- "target": str(target_file),
471
- "reason": reason,
472
- "deployment_time_ms": agent_deployment_time # METRICS: Include timing
473
- })
474
- self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
475
-
476
- # METRICS: Track migration statistics
477
- self._deployment_metrics['migrations_performed'] += 1
478
- self._deployment_metrics['version_migration_count'] += 1
479
-
480
- elif is_update:
481
- results["updated"].append({
482
- "name": agent_name,
483
- "template": str(template_file),
484
- "target": str(target_file),
485
- "deployment_time_ms": agent_deployment_time # METRICS: Include timing
486
- })
487
- self.logger.debug(f"Updated agent: {agent_name}")
488
- else:
489
- results["deployed"].append({
490
- "name": agent_name,
491
- "template": str(template_file),
492
- "target": str(target_file),
493
- "deployment_time_ms": agent_deployment_time # METRICS: Include timing
494
- })
495
- self.logger.debug(f"Built and deployed agent: {agent_name}")
496
-
497
- except Exception as e:
498
- error_msg = f"Failed to build {template_file.name}: {e}"
499
- self.logger.error(error_msg)
500
- results["errors"].append(error_msg)
271
+ self._deploy_single_agent(
272
+ template_file=template_file,
273
+ agents_dir=agents_dir,
274
+ base_agent_data=base_agent_data,
275
+ base_agent_version=base_agent_version,
276
+ force_rebuild=force_rebuild,
277
+ deployment_mode=deployment_mode,
278
+ results=results
279
+ )
501
280
 
502
281
  self.logger.info(
503
282
  f"Deployed {len(results['deployed'])} agents, "
@@ -509,7 +288,12 @@ class AgentDeploymentService:
509
288
  f"errors: {len(results['errors'])}"
510
289
  )
511
290
 
291
+ except AgentDeploymentError as e:
292
+ # Custom error with context already formatted
293
+ self.logger.error(str(e))
294
+ results["errors"].append(str(e))
512
295
  except Exception as e:
296
+ # Wrap unexpected errors
513
297
  error_msg = f"Agent deployment failed: {e}"
514
298
  self.logger.error(error_msg)
515
299
  results["errors"].append(error_msg)
@@ -740,14 +524,36 @@ class AgentDeploymentService:
740
524
  # IMPORTANT: No spaces after commas - Claude Code requires exact format
741
525
  tools_str = ','.join(tools) if isinstance(tools, list) else tools
742
526
 
527
+ # Validate tools format - CRITICAL: No spaces allowed!
528
+ if ', ' in tools_str:
529
+ self.logger.error(f"Tools contain spaces: '{tools_str}'")
530
+ raise AgentDeploymentError(
531
+ f"Tools must be comma-separated WITHOUT spaces",
532
+ context={"agent_name": agent_name, "invalid_tools": tools_str}
533
+ )
534
+
743
535
  # Extract proper agent_id and name from template
744
536
  agent_id = template_data.get('agent_id', agent_name)
745
537
  display_name = template_data.get('metadata', {}).get('name', agent_id)
746
538
 
539
+ # Convert agent_id to Claude Code compatible name (replace underscores with hyphens)
540
+ # Claude Code requires name to match pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
541
+ # CRITICAL: NO underscores allowed - they cause silent failures!
542
+ claude_code_name = agent_id.replace('_', '-').lower()
543
+
544
+ # Validate the name before proceeding
545
+ import re
546
+ if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', claude_code_name):
547
+ self.logger.error(f"Invalid agent name '{claude_code_name}' - must match ^[a-z0-9]+(-[a-z0-9]+)*$")
548
+ raise AgentDeploymentError(
549
+ f"Agent name '{claude_code_name}' does not meet Claude Code requirements",
550
+ context={"agent_name": agent_name, "invalid_name": claude_code_name}
551
+ )
552
+
747
553
  # Build frontmatter with only the fields Claude Code uses
748
554
  frontmatter_lines = [
749
555
  "---",
750
- f"name: {agent_id}",
556
+ f"name: {claude_code_name}",
751
557
  f"description: {description}",
752
558
  f"version: {version_string}",
753
559
  f"base_version: {self._format_version_display(base_version)}",
@@ -1224,9 +1030,15 @@ temperature: {temperature}"""
1224
1030
 
1225
1031
  return True
1226
1032
 
1033
+ except AgentDeploymentError:
1034
+ # Re-raise our custom exceptions
1035
+ raise
1227
1036
  except Exception as e:
1228
- self.logger.error(f"Failed to deploy agent {agent_name}: {e}")
1229
- return False
1037
+ # Wrap generic exceptions with context
1038
+ raise AgentDeploymentError(
1039
+ f"Failed to deploy agent {agent_name}",
1040
+ context={"agent_name": agent_name, "error": str(e)}
1041
+ ) from e
1230
1042
 
1231
1043
  def list_available_agents(self) -> List[Dict[str, Any]]:
1232
1044
  """
@@ -1494,6 +1306,7 @@ temperature: {temperature}"""
1494
1306
  error_msg = f"Failed to remove {agent_file.name}: {e}"
1495
1307
  self.logger.error(error_msg)
1496
1308
  results["errors"].append(error_msg)
1309
+ # Not raising AgentDeploymentError here to continue cleanup
1497
1310
 
1498
1311
  return results
1499
1312
 
@@ -1722,10 +1535,10 @@ temperature: {temperature}"""
1722
1535
  """
1723
1536
  # Base configuration all agents share
1724
1537
  base_config = {
1725
- 'timeout': 600,
1726
- 'max_tokens': 8192,
1727
- 'memory_limit': 2048,
1728
- 'cpu_limit': 50,
1538
+ 'timeout': TimeoutConfig.DEFAULT_TIMEOUT,
1539
+ 'max_tokens': SystemLimits.MAX_TOKEN_LIMIT,
1540
+ 'memory_limit': ResourceLimits.STANDARD_MEMORY_RANGE[0], # Use lower bound of standard memory
1541
+ 'cpu_limit': ResourceLimits.STANDARD_CPU_RANGE[1], # Use upper bound of standard CPU
1729
1542
  'network_access': True,
1730
1543
  }
1731
1544
 
@@ -1909,6 +1722,7 @@ temperature: {temperature}"""
1909
1722
  error_msg = f"Failed to deploy system instructions: {e}"
1910
1723
  self.logger.error(error_msg)
1911
1724
  results["errors"].append(error_msg)
1725
+ # Not raising AgentDeploymentError as this is non-critical
1912
1726
 
1913
1727
  def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
1914
1728
  """
@@ -2118,6 +1932,443 @@ metadata:
2118
1932
 
2119
1933
  return None
2120
1934
 
1935
+ def _try_async_deployment(self, target_dir: Optional[Path], force_rebuild: bool,
1936
+ config: Optional[Config], deployment_start_time: float) -> Optional[Dict[str, Any]]:
1937
+ """
1938
+ Try to use async deployment for better performance.
1939
+
1940
+ WHY: Async deployment is 50-70% faster than synchronous deployment
1941
+ by using concurrent operations for file I/O and processing.
1942
+
1943
+ Args:
1944
+ target_dir: Target directory for deployment
1945
+ force_rebuild: Whether to force rebuild
1946
+ config: Configuration object
1947
+ deployment_start_time: Start time for metrics
1948
+
1949
+ Returns:
1950
+ Deployment results if successful, None if async not available
1951
+ """
1952
+ try:
1953
+ from .async_agent_deployment import deploy_agents_async_wrapper
1954
+ self.logger.info("Using async deployment for improved performance")
1955
+
1956
+ # Run async deployment
1957
+ results = deploy_agents_async_wrapper(
1958
+ templates_dir=self.templates_dir,
1959
+ base_agent_path=self.base_agent_path,
1960
+ working_directory=self.working_directory,
1961
+ target_dir=target_dir,
1962
+ force_rebuild=force_rebuild,
1963
+ config=config
1964
+ )
1965
+
1966
+ # Add metrics about async vs sync
1967
+ if 'metrics' in results:
1968
+ results['metrics']['deployment_method'] = 'async'
1969
+ duration_ms = results['metrics'].get('duration_ms', 0)
1970
+ self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
1971
+
1972
+ # Update internal metrics
1973
+ self._deployment_metrics['total_deployments'] += 1
1974
+ if not results.get('errors'):
1975
+ self._deployment_metrics['successful_deployments'] += 1
1976
+ else:
1977
+ self._deployment_metrics['failed_deployments'] += 1
1978
+
1979
+ return results
1980
+
1981
+ except ImportError:
1982
+ self.logger.warning("Async deployment not available, falling back to sync")
1983
+ return None
1984
+ except Exception as e:
1985
+ self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
1986
+ return None
1987
+
1988
+ def _load_deployment_config(self, config: Optional[Config]) -> tuple:
1989
+ """
1990
+ Load and process deployment configuration.
1991
+
1992
+ WHY: Centralized configuration loading reduces duplication
1993
+ and ensures consistent handling of exclusion settings.
1994
+
1995
+ Args:
1996
+ config: Optional configuration object
1997
+
1998
+ Returns:
1999
+ Tuple of (config, excluded_agents)
2000
+ """
2001
+ # Load configuration if not provided
2002
+ if config is None:
2003
+ config = Config()
2004
+
2005
+ # Get agent exclusion configuration
2006
+ excluded_agents = config.get('agent_deployment.excluded_agents', [])
2007
+ case_sensitive = config.get('agent_deployment.case_sensitive', False)
2008
+ exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
2009
+
2010
+ # Normalize excluded agents list for comparison
2011
+ if not case_sensitive:
2012
+ excluded_agents = [agent.lower() for agent in excluded_agents]
2013
+
2014
+ # Log exclusion configuration if agents are being excluded
2015
+ if excluded_agents:
2016
+ self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
2017
+ self.logger.debug(f"Case sensitive matching: {case_sensitive}")
2018
+ self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
2019
+
2020
+ return config, excluded_agents
2021
+
2022
+ def _determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
2023
+ """
2024
+ Determine the correct agents directory based on input.
2025
+
2026
+ WHY: Different deployment scenarios require different directory
2027
+ structures. This method centralizes the logic for consistency.
2028
+
2029
+ Args:
2030
+ target_dir: Optional target directory
2031
+
2032
+ Returns:
2033
+ Path to agents directory
2034
+ """
2035
+ if not target_dir:
2036
+ # Default to working directory's .claude/agents directory (not home)
2037
+ # This ensures we deploy to the user's project directory
2038
+ return self.working_directory / ".claude" / "agents"
2039
+
2040
+ # If target_dir provided, use it directly (caller decides structure)
2041
+ target_dir = Path(target_dir)
2042
+
2043
+ # Check if this is already an agents directory
2044
+ if target_dir.name == "agents":
2045
+ # Already an agents directory, use as-is
2046
+ return target_dir
2047
+ elif target_dir.name == ".claude-mpm":
2048
+ # .claude-mpm directory, add agents subdirectory
2049
+ return target_dir / "agents"
2050
+ elif target_dir.name == ".claude":
2051
+ # .claude directory, add agents subdirectory
2052
+ return target_dir / "agents"
2053
+ else:
2054
+ # Assume it's a project directory, add .claude/agents
2055
+ return target_dir / ".claude" / "agents"
2056
+
2057
+ def _initialize_deployment_results(self, agents_dir: Path, deployment_start_time: float) -> Dict[str, Any]:
2058
+ """
2059
+ Initialize the deployment results dictionary.
2060
+
2061
+ WHY: Consistent result structure ensures all deployment
2062
+ operations return the same format for easier processing.
2063
+
2064
+ Args:
2065
+ agents_dir: Target agents directory
2066
+ deployment_start_time: Start time for metrics
2067
+
2068
+ Returns:
2069
+ Initialized results dictionary
2070
+ """
2071
+ return {
2072
+ "target_dir": str(agents_dir),
2073
+ "deployed": [],
2074
+ "errors": [],
2075
+ "skipped": [],
2076
+ "updated": [],
2077
+ "migrated": [], # Track agents migrated from old format
2078
+ "converted": [], # Track YAML to MD conversions
2079
+ "repaired": [], # Track agents with repaired frontmatter
2080
+ "total": 0,
2081
+ # METRICS: Add detailed timing and performance data to results
2082
+ "metrics": {
2083
+ "start_time": deployment_start_time,
2084
+ "end_time": None,
2085
+ "duration_ms": None,
2086
+ "agent_timings": {}, # Track individual agent deployment times
2087
+ "validation_times": {}, # Track template validation times
2088
+ "resource_usage": {} # Could track memory/CPU if needed
2089
+ }
2090
+ }
2091
+
2092
+ def _repair_existing_agents(self, agents_dir: Path, results: Dict[str, Any]) -> None:
2093
+ """
2094
+ Validate and repair broken frontmatter in existing agents.
2095
+
2096
+ WHY: Ensures all existing agents have valid YAML frontmatter
2097
+ before deployment, preventing runtime errors in Claude Code.
2098
+
2099
+ Args:
2100
+ agents_dir: Directory containing agent files
2101
+ results: Results dictionary to update
2102
+ """
2103
+ repair_results = self._validate_and_repair_existing_agents(agents_dir)
2104
+ if repair_results["repaired"]:
2105
+ results["repaired"] = repair_results["repaired"]
2106
+ self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
2107
+ for agent_name in repair_results["repaired"]:
2108
+ self.logger.debug(f" - Repaired: {agent_name}")
2109
+
2110
+ def _determine_source_tier(self) -> str:
2111
+ """
2112
+ Determine the source tier for logging.
2113
+
2114
+ WHY: Understanding which tier (SYSTEM/USER/PROJECT) agents
2115
+ are being deployed from helps with debugging and auditing.
2116
+
2117
+ Returns:
2118
+ Source tier string
2119
+ """
2120
+ if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2121
+ return "PROJECT"
2122
+ elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2123
+ return "USER"
2124
+ return "SYSTEM"
2125
+
2126
+ def _load_base_agent(self) -> tuple:
2127
+ """
2128
+ Load base agent content and version.
2129
+
2130
+ WHY: Base agent contains shared configuration that all agents
2131
+ inherit, reducing duplication and ensuring consistency.
2132
+
2133
+ Returns:
2134
+ Tuple of (base_agent_data, base_agent_version)
2135
+ """
2136
+ base_agent_data = {}
2137
+ base_agent_version = (0, 0, 0)
2138
+
2139
+ if self.base_agent_path.exists():
2140
+ try:
2141
+ import json
2142
+ base_agent_data = json.loads(self.base_agent_path.read_text())
2143
+ # Handle both 'base_version' (new format) and 'version' (old format)
2144
+ # MIGRATION PATH: Supporting both formats during transition period
2145
+ base_agent_version = self._parse_version(
2146
+ base_agent_data.get('base_version') or base_agent_data.get('version', 0)
2147
+ )
2148
+ self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
2149
+ except Exception as e:
2150
+ # NON-FATAL: Base agent is optional enhancement, not required
2151
+ self.logger.warning(f"Could not load base agent: {e}")
2152
+
2153
+ return base_agent_data, base_agent_version
2154
+
2155
+ def _get_filtered_templates(self, excluded_agents: list, config: Config) -> list:
2156
+ """
2157
+ Get and filter template files based on exclusion rules.
2158
+
2159
+ WHY: Centralized filtering logic ensures consistent exclusion
2160
+ handling across different deployment scenarios.
2161
+
2162
+ Args:
2163
+ excluded_agents: List of agents to exclude
2164
+ config: Configuration object
2165
+
2166
+ Returns:
2167
+ List of filtered template files
2168
+ """
2169
+ # Get all template files
2170
+ template_files = list(self.templates_dir.glob("*.json"))
2171
+
2172
+ # Build the combined exclusion set
2173
+ # Start with hardcoded exclusions (these are ALWAYS excluded)
2174
+ hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS",
2175
+ "README", "pm", "PM", "project_manager"}
2176
+
2177
+ # Get case sensitivity setting
2178
+ case_sensitive = config.get('agent_deployment.case_sensitive', False)
2179
+
2180
+ # Filter out excluded agents
2181
+ filtered_files = []
2182
+ excluded_count = 0
2183
+
2184
+ for f in template_files:
2185
+ agent_name = f.stem
2186
+
2187
+ # Check hardcoded exclusions (always case-sensitive)
2188
+ if agent_name in hardcoded_exclusions:
2189
+ self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
2190
+ excluded_count += 1
2191
+ continue
2192
+
2193
+ # Check file patterns
2194
+ if agent_name.startswith(".") or agent_name.endswith(".backup"):
2195
+ self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
2196
+ excluded_count += 1
2197
+ continue
2198
+
2199
+ # Check user-configured exclusions
2200
+ compare_name = agent_name.lower() if not case_sensitive else agent_name
2201
+ if compare_name in excluded_agents:
2202
+ self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
2203
+ excluded_count += 1
2204
+ continue
2205
+
2206
+ # Agent is not excluded, add to filtered list
2207
+ filtered_files.append(f)
2208
+
2209
+ if excluded_count > 0:
2210
+ self.logger.info(f"Excluded {excluded_count} agents from deployment")
2211
+
2212
+ return filtered_files
2213
+
2214
+ def _deploy_single_agent(self, template_file: Path, agents_dir: Path,
2215
+ base_agent_data: dict, base_agent_version: tuple,
2216
+ force_rebuild: bool, deployment_mode: str,
2217
+ results: Dict[str, Any]) -> None:
2218
+ """
2219
+ Deploy a single agent template.
2220
+
2221
+ WHY: Extracting single agent deployment logic reduces complexity
2222
+ and makes the main deployment loop more readable.
2223
+
2224
+ Args:
2225
+ template_file: Agent template file
2226
+ agents_dir: Target agents directory
2227
+ base_agent_data: Base agent data
2228
+ base_agent_version: Base agent version
2229
+ force_rebuild: Whether to force rebuild
2230
+ deployment_mode: Deployment mode (update/project)
2231
+ results: Results dictionary to update
2232
+ """
2233
+ try:
2234
+ # METRICS: Track individual agent deployment time
2235
+ agent_start_time = time.time()
2236
+
2237
+ agent_name = template_file.stem
2238
+ target_file = agents_dir / f"{agent_name}.md"
2239
+
2240
+ # Check if agent needs update
2241
+ needs_update, is_migration, reason = self._check_update_status(
2242
+ target_file, template_file, base_agent_version,
2243
+ force_rebuild, deployment_mode
2244
+ )
2245
+
2246
+ # Skip if exists and doesn't need update (only in update mode)
2247
+ if target_file.exists() and not needs_update and deployment_mode != "project":
2248
+ results["skipped"].append(agent_name)
2249
+ self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
2250
+ return
2251
+
2252
+ # Build the agent file as markdown with YAML frontmatter
2253
+ agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
2254
+
2255
+ # Write the agent file
2256
+ is_update = target_file.exists()
2257
+ target_file.write_text(agent_content)
2258
+
2259
+ # Record metrics and update results
2260
+ self._record_agent_deployment(
2261
+ agent_name, template_file, target_file,
2262
+ is_update, is_migration, reason,
2263
+ agent_start_time, results
2264
+ )
2265
+
2266
+ except AgentDeploymentError as e:
2267
+ # Re-raise our custom exceptions
2268
+ self.logger.error(str(e))
2269
+ results["errors"].append(str(e))
2270
+ except Exception as e:
2271
+ # Wrap generic exceptions with context
2272
+ error_msg = f"Failed to build {template_file.name}: {e}"
2273
+ self.logger.error(error_msg)
2274
+ results["errors"].append(error_msg)
2275
+
2276
+ def _check_update_status(self, target_file: Path, template_file: Path,
2277
+ base_agent_version: tuple, force_rebuild: bool,
2278
+ deployment_mode: str) -> tuple:
2279
+ """
2280
+ Check if agent needs update and determine status.
2281
+
2282
+ WHY: Centralized update checking logic ensures consistent
2283
+ version comparison and migration detection.
2284
+
2285
+ Args:
2286
+ target_file: Target agent file
2287
+ template_file: Template file
2288
+ base_agent_version: Base agent version
2289
+ force_rebuild: Whether to force rebuild
2290
+ deployment_mode: Deployment mode
2291
+
2292
+ Returns:
2293
+ Tuple of (needs_update, is_migration, reason)
2294
+ """
2295
+ needs_update = force_rebuild
2296
+ is_migration = False
2297
+ reason = ""
2298
+
2299
+ # In project deployment mode, always deploy regardless of version
2300
+ if deployment_mode == "project":
2301
+ if target_file.exists():
2302
+ needs_update = True
2303
+ self.logger.debug(f"Project deployment mode: will deploy {template_file.stem}")
2304
+ else:
2305
+ needs_update = True
2306
+ elif not needs_update and target_file.exists():
2307
+ # In update mode, check version compatibility
2308
+ needs_update, reason = self._check_agent_needs_update(
2309
+ target_file, template_file, base_agent_version
2310
+ )
2311
+ if needs_update:
2312
+ # Check if this is a migration from old format
2313
+ if "migration needed" in reason:
2314
+ is_migration = True
2315
+ self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
2316
+ else:
2317
+ self.logger.info(f"Agent {template_file.stem} needs update: {reason}")
2318
+
2319
+ return needs_update, is_migration, reason
2320
+
2321
+ def _record_agent_deployment(self, agent_name: str, template_file: Path,
2322
+ target_file: Path, is_update: bool,
2323
+ is_migration: bool, reason: str,
2324
+ agent_start_time: float, results: Dict[str, Any]) -> None:
2325
+ """
2326
+ Record deployment metrics and update results.
2327
+
2328
+ WHY: Centralized metrics recording ensures consistent tracking
2329
+ of deployment performance and statistics.
2330
+
2331
+ Args:
2332
+ agent_name: Name of the agent
2333
+ template_file: Template file
2334
+ target_file: Target file
2335
+ is_update: Whether this is an update
2336
+ is_migration: Whether this is a migration
2337
+ reason: Update/migration reason
2338
+ agent_start_time: Start time for this agent
2339
+ results: Results dictionary to update
2340
+ """
2341
+ # METRICS: Record deployment time for this agent
2342
+ agent_deployment_time = (time.time() - agent_start_time) * 1000 # Convert to ms
2343
+ results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
2344
+
2345
+ # METRICS: Update agent type deployment counts
2346
+ self._deployment_metrics['agent_type_counts'][agent_name] = \
2347
+ self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
2348
+
2349
+ deployment_info = {
2350
+ "name": agent_name,
2351
+ "template": str(template_file),
2352
+ "target": str(target_file),
2353
+ "deployment_time_ms": agent_deployment_time
2354
+ }
2355
+
2356
+ if is_migration:
2357
+ deployment_info["reason"] = reason
2358
+ results["migrated"].append(deployment_info)
2359
+ self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
2360
+
2361
+ # METRICS: Track migration statistics
2362
+ self._deployment_metrics['migrations_performed'] += 1
2363
+ self._deployment_metrics['version_migration_count'] += 1
2364
+
2365
+ elif is_update:
2366
+ results["updated"].append(deployment_info)
2367
+ self.logger.debug(f"Updated agent: {agent_name}")
2368
+ else:
2369
+ results["deployed"].append(deployment_info)
2370
+ self.logger.debug(f"Built and deployed agent: {agent_name}")
2371
+
2121
2372
  def _validate_and_repair_existing_agents(self, agents_dir: Path) -> Dict[str, Any]:
2122
2373
  """
2123
2374
  Validate and repair broken frontmatter in existing agent files.
@@ -2240,4 +2491,76 @@ metadata:
2240
2491
  self.logger.error(error_msg)
2241
2492
  results["errors"].append(error_msg)
2242
2493
 
2243
- return results
2494
+ return results
2495
+
2496
+ # ================================================================================
2497
+ # Interface Adapter Methods
2498
+ # ================================================================================
2499
+ # These methods adapt the existing implementation to comply with AgentDeploymentInterface
2500
+
2501
+ def validate_agent(self, agent_path: Path) -> tuple[bool, List[str]]:
2502
+ """Validate agent configuration and structure.
2503
+
2504
+ WHY: This adapter method provides interface compliance while leveraging
2505
+ the existing validation logic in _check_agent_needs_update and other methods.
2506
+
2507
+ Args:
2508
+ agent_path: Path to agent configuration file
2509
+
2510
+ Returns:
2511
+ Tuple of (is_valid, list_of_errors)
2512
+ """
2513
+ errors = []
2514
+
2515
+ try:
2516
+ if not agent_path.exists():
2517
+ return False, [f"Agent file not found: {agent_path}"]
2518
+
2519
+ content = agent_path.read_text()
2520
+
2521
+ # Check YAML frontmatter format
2522
+ if not content.startswith("---"):
2523
+ errors.append("Missing YAML frontmatter")
2524
+
2525
+ # Extract and validate version
2526
+ import re
2527
+ version_match = re.search(r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE)
2528
+ if not version_match:
2529
+ errors.append("Missing version field in frontmatter")
2530
+
2531
+ # Check for required fields
2532
+ required_fields = ['name', 'description', 'tools']
2533
+ for field in required_fields:
2534
+ field_match = re.search(rf'^{field}:\s*.+$', content, re.MULTILINE)
2535
+ if not field_match:
2536
+ errors.append(f"Missing required field: {field}")
2537
+
2538
+ # If no errors, validation passed
2539
+ return len(errors) == 0, errors
2540
+
2541
+ except Exception as e:
2542
+ return False, [f"Validation error: {str(e)}"]
2543
+
2544
+ def get_deployment_status(self) -> Dict[str, Any]:
2545
+ """Get current deployment status and metrics.
2546
+
2547
+ WHY: This adapter method provides interface compliance by wrapping
2548
+ verify_deployment and adding deployment metrics.
2549
+
2550
+ Returns:
2551
+ Dictionary with deployment status information
2552
+ """
2553
+ # Get verification results
2554
+ verification = self.verify_deployment()
2555
+
2556
+ # Add deployment metrics
2557
+ status = {
2558
+ "deployment_metrics": self._deployment_metrics.copy(),
2559
+ "verification": verification,
2560
+ "agents_deployed": len(verification.get("agents_found", [])),
2561
+ "agents_needing_migration": len(verification.get("agents_needing_migration", [])),
2562
+ "has_warnings": len(verification.get("warnings", [])) > 0,
2563
+ "environment_configured": bool(verification.get("environment", {}))
2564
+ }
2565
+
2566
+ return status