claude-mpm 3.7.8__py3-none-any.whl → 3.9.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 (100) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -96
  4. claude_mpm/agents/MEMORY.md +94 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/templates/code_analyzer.json +2 -2
  7. claude_mpm/agents/templates/data_engineer.json +1 -1
  8. claude_mpm/agents/templates/documentation.json +1 -1
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/ops.json +1 -1
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/agents/templates/security.json +1 -1
  14. claude_mpm/agents/templates/ticketing.json +3 -8
  15. claude_mpm/agents/templates/version_control.json +1 -1
  16. claude_mpm/agents/templates/web_qa.json +2 -2
  17. claude_mpm/agents/templates/web_ui.json +2 -2
  18. claude_mpm/cli/__init__.py +2 -2
  19. claude_mpm/cli/commands/__init__.py +2 -1
  20. claude_mpm/cli/commands/agents.py +8 -3
  21. claude_mpm/cli/commands/tickets.py +596 -19
  22. claude_mpm/cli/parser.py +217 -5
  23. claude_mpm/config/__init__.py +30 -39
  24. claude_mpm/config/socketio_config.py +8 -5
  25. claude_mpm/constants.py +13 -0
  26. claude_mpm/core/__init__.py +8 -18
  27. claude_mpm/core/cache.py +596 -0
  28. claude_mpm/core/claude_runner.py +166 -622
  29. claude_mpm/core/config.py +7 -3
  30. claude_mpm/core/constants.py +339 -0
  31. claude_mpm/core/container.py +548 -38
  32. claude_mpm/core/exceptions.py +392 -0
  33. claude_mpm/core/framework_loader.py +249 -93
  34. claude_mpm/core/interactive_session.py +479 -0
  35. claude_mpm/core/interfaces.py +424 -0
  36. claude_mpm/core/lazy.py +467 -0
  37. claude_mpm/core/logging_config.py +444 -0
  38. claude_mpm/core/oneshot_session.py +465 -0
  39. claude_mpm/core/optimized_agent_loader.py +485 -0
  40. claude_mpm/core/optimized_startup.py +490 -0
  41. claude_mpm/core/service_registry.py +52 -26
  42. claude_mpm/core/socketio_pool.py +162 -5
  43. claude_mpm/core/types.py +292 -0
  44. claude_mpm/core/typing_utils.py +477 -0
  45. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  46. claude_mpm/init.py +2 -1
  47. claude_mpm/services/__init__.py +78 -14
  48. claude_mpm/services/agent/__init__.py +24 -0
  49. claude_mpm/services/agent/deployment.py +2548 -0
  50. claude_mpm/services/agent/management.py +598 -0
  51. claude_mpm/services/agent/registry.py +813 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +728 -308
  53. claude_mpm/services/agents/memory/agent_memory_manager.py +160 -4
  54. claude_mpm/services/async_session_logger.py +8 -3
  55. claude_mpm/services/communication/__init__.py +21 -0
  56. claude_mpm/services/communication/socketio.py +1933 -0
  57. claude_mpm/services/communication/websocket.py +479 -0
  58. claude_mpm/services/core/__init__.py +123 -0
  59. claude_mpm/services/core/base.py +247 -0
  60. claude_mpm/services/core/interfaces.py +951 -0
  61. claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
  62. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
  63. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  64. claude_mpm/services/framework_claude_md_generator.py +3 -2
  65. claude_mpm/services/health_monitor.py +4 -3
  66. claude_mpm/services/hook_service.py +64 -4
  67. claude_mpm/services/infrastructure/__init__.py +21 -0
  68. claude_mpm/services/infrastructure/logging.py +202 -0
  69. claude_mpm/services/infrastructure/monitoring.py +893 -0
  70. claude_mpm/services/memory/indexed_memory.py +648 -0
  71. claude_mpm/services/project/__init__.py +21 -0
  72. claude_mpm/services/project/analyzer.py +864 -0
  73. claude_mpm/services/project/registry.py +608 -0
  74. claude_mpm/services/project_analyzer.py +95 -2
  75. claude_mpm/services/recovery_manager.py +15 -9
  76. claude_mpm/services/response_tracker.py +3 -5
  77. claude_mpm/services/socketio/__init__.py +25 -0
  78. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  79. claude_mpm/services/socketio/handlers/base.py +121 -0
  80. claude_mpm/services/socketio/handlers/connection.py +198 -0
  81. claude_mpm/services/socketio/handlers/file.py +213 -0
  82. claude_mpm/services/socketio/handlers/git.py +723 -0
  83. claude_mpm/services/socketio/handlers/memory.py +27 -0
  84. claude_mpm/services/socketio/handlers/project.py +25 -0
  85. claude_mpm/services/socketio/handlers/registry.py +145 -0
  86. claude_mpm/services/socketio_client_manager.py +12 -7
  87. claude_mpm/services/socketio_server.py +156 -30
  88. claude_mpm/services/ticket_manager.py +172 -9
  89. claude_mpm/services/ticket_manager_di.py +1 -1
  90. claude_mpm/services/version_control/semantic_versioning.py +80 -7
  91. claude_mpm/services/version_control/version_parser.py +528 -0
  92. claude_mpm/utils/error_handler.py +1 -1
  93. claude_mpm/validation/agent_validator.py +27 -14
  94. claude_mpm/validation/frontmatter_validator.py +231 -0
  95. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/METADATA +38 -128
  96. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/RECORD +100 -59
  97. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/WHEEL +0 -0
  98. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/entry_points.txt +0 -0
  99. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/licenses/LICENSE +0 -0
  100. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.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
@@ -103,12 +110,13 @@ class AgentDeploymentService:
103
110
  }
104
111
 
105
112
  # Determine the actual working directory
106
- # Priority: working_directory param > CLAUDE_MPM_USER_PWD env var > current directory
113
+ # For deployment, we need to track the working directory but NOT use it
114
+ # for determining where system agents go (they always go to ~/.claude/agents/)
115
+ # Priority: working_directory param > current directory
107
116
  if working_directory:
108
117
  self.working_directory = Path(working_directory)
109
- elif 'CLAUDE_MPM_USER_PWD' in os.environ:
110
- self.working_directory = Path(os.environ['CLAUDE_MPM_USER_PWD'])
111
118
  else:
119
+ # Use current directory but don't let CLAUDE_MPM_USER_PWD affect system agent deployment
112
120
  self.working_directory = Path.cwd()
113
121
 
114
122
  self.logger.info(f"Working directory for deployment: {self.working_directory}")
@@ -132,7 +140,7 @@ class AgentDeploymentService:
132
140
  self.logger.info(f"Templates directory: {self.templates_dir}")
133
141
  self.logger.info(f"Base agent path: {self.base_agent_path}")
134
142
 
135
- def deploy_agents(self, target_dir: Optional[Path] = None, force_rebuild: bool = False, deployment_mode: str = "update", config: Optional[Config] = None, use_async: bool = True) -> Dict[str, Any]:
143
+ def deploy_agents(self, target_dir: Optional[Path] = None, force_rebuild: bool = False, deployment_mode: str = "update", config: Optional[Config] = None, use_async: bool = False) -> Dict[str, Any]:
136
144
  """
137
145
  Build and deploy agents by combining base_agent.md with templates.
138
146
  Also deploys system instructions for PM framework.
@@ -207,125 +215,36 @@ class AgentDeploymentService:
207
215
 
208
216
  # Try async deployment for better performance if requested
209
217
  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}")
218
+ async_results = self._try_async_deployment(
219
+ target_dir=target_dir,
220
+ force_rebuild=force_rebuild,
221
+ config=config,
222
+ deployment_start_time=deployment_start_time
223
+ )
224
+ if async_results is not None:
225
+ return async_results
243
226
 
244
227
  # Continue with synchronous deployment
245
228
  self.logger.info("Using synchronous deployment")
246
229
 
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]
230
+ # Load and process configuration
231
+ config, excluded_agents = self._load_deployment_config(config)
259
232
 
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}")
233
+ # Determine target agents directory
234
+ agents_dir = self._determine_agents_directory(target_dir)
265
235
 
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"
287
-
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
- }
236
+ # Initialize results dictionary
237
+ results = self._initialize_deployment_results(agents_dir, deployment_start_time)
308
238
 
309
239
  try:
310
240
  # Create agents directory if needed
311
241
  agents_dir.mkdir(parents=True, exist_ok=True)
312
242
 
313
243
  # 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"
244
+ self._repair_existing_agents(agents_dir, results)
328
245
 
246
+ # Log deployment source tier
247
+ source_tier = self._determine_source_tier()
329
248
  self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
330
249
 
331
250
  # Note: System instructions are now loaded directly by SimpleClaudeRunner
@@ -342,162 +261,26 @@ class AgentDeploymentService:
342
261
  results["converted"] = conversion_results.get("converted", [])
343
262
 
344
263
  # 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"))
264
+ base_agent_data, base_agent_version = self._load_base_agent()
364
265
 
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)
405
-
406
- template_files = filtered_files
266
+ # Get and filter template files
267
+ template_files = self._get_filtered_templates(excluded_agents, config)
407
268
  results["total"] = len(template_files)
408
269
 
409
- if excluded_count > 0:
410
- self.logger.info(f"Excluded {excluded_count} agents from deployment")
411
-
270
+ # Deploy each agent template
412
271
  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)
272
+ self._deploy_single_agent(
273
+ template_file=template_file,
274
+ agents_dir=agents_dir,
275
+ base_agent_data=base_agent_data,
276
+ base_agent_version=base_agent_version,
277
+ force_rebuild=force_rebuild,
278
+ deployment_mode=deployment_mode,
279
+ results=results
280
+ )
281
+
282
+ # Deploy system instructions and framework files
283
+ self._deploy_system_instructions(agents_dir, force_rebuild, results)
501
284
 
502
285
  self.logger.info(
503
286
  f"Deployed {len(results['deployed'])} agents, "
@@ -509,7 +292,12 @@ class AgentDeploymentService:
509
292
  f"errors: {len(results['errors'])}"
510
293
  )
511
294
 
295
+ except AgentDeploymentError as e:
296
+ # Custom error with context already formatted
297
+ self.logger.error(str(e))
298
+ results["errors"].append(str(e))
512
299
  except Exception as e:
300
+ # Wrap unexpected errors
513
301
  error_msg = f"Agent deployment failed: {e}"
514
302
  self.logger.error(error_msg)
515
303
  results["errors"].append(error_msg)
@@ -740,14 +528,32 @@ class AgentDeploymentService:
740
528
  # IMPORTANT: No spaces after commas - Claude Code requires exact format
741
529
  tools_str = ','.join(tools) if isinstance(tools, list) else tools
742
530
 
531
+ # Validate tools format - CRITICAL: No spaces allowed!
532
+ if ', ' in tools_str:
533
+ self.logger.error(f"Tools contain spaces: '{tools_str}'")
534
+ raise AgentDeploymentError(
535
+ f"Tools must be comma-separated WITHOUT spaces",
536
+ context={"agent_name": agent_name, "invalid_tools": tools_str}
537
+ )
538
+
743
539
  # Extract proper agent_id and name from template
744
540
  agent_id = template_data.get('agent_id', agent_name)
745
541
  display_name = template_data.get('metadata', {}).get('name', agent_id)
746
542
 
747
543
  # Convert agent_id to Claude Code compatible name (replace underscores with hyphens)
748
544
  # Claude Code requires name to match pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
545
+ # CRITICAL: NO underscores allowed - they cause silent failures!
749
546
  claude_code_name = agent_id.replace('_', '-').lower()
750
547
 
548
+ # Validate the name before proceeding
549
+ import re
550
+ if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', claude_code_name):
551
+ self.logger.error(f"Invalid agent name '{claude_code_name}' - must match ^[a-z0-9]+(-[a-z0-9]+)*$")
552
+ raise AgentDeploymentError(
553
+ f"Agent name '{claude_code_name}' does not meet Claude Code requirements",
554
+ context={"agent_name": agent_name, "invalid_name": claude_code_name}
555
+ )
556
+
751
557
  # Build frontmatter with only the fields Claude Code uses
752
558
  frontmatter_lines = [
753
559
  "---",
@@ -1228,9 +1034,15 @@ temperature: {temperature}"""
1228
1034
 
1229
1035
  return True
1230
1036
 
1037
+ except AgentDeploymentError:
1038
+ # Re-raise our custom exceptions
1039
+ raise
1231
1040
  except Exception as e:
1232
- self.logger.error(f"Failed to deploy agent {agent_name}: {e}")
1233
- return False
1041
+ # Wrap generic exceptions with context
1042
+ raise AgentDeploymentError(
1043
+ f"Failed to deploy agent {agent_name}",
1044
+ context={"agent_name": agent_name, "error": str(e)}
1045
+ ) from e
1234
1046
 
1235
1047
  def list_available_agents(self) -> List[Dict[str, Any]]:
1236
1048
  """
@@ -1498,6 +1310,7 @@ temperature: {temperature}"""
1498
1310
  error_msg = f"Failed to remove {agent_file.name}: {e}"
1499
1311
  self.logger.error(error_msg)
1500
1312
  results["errors"].append(error_msg)
1313
+ # Not raising AgentDeploymentError here to continue cleanup
1501
1314
 
1502
1315
  return results
1503
1316
 
@@ -1726,10 +1539,10 @@ temperature: {temperature}"""
1726
1539
  """
1727
1540
  # Base configuration all agents share
1728
1541
  base_config = {
1729
- 'timeout': 600,
1730
- 'max_tokens': 8192,
1731
- 'memory_limit': 2048,
1732
- 'cpu_limit': 50,
1542
+ 'timeout': TimeoutConfig.DEFAULT_TIMEOUT,
1543
+ 'max_tokens': SystemLimits.MAX_TOKEN_LIMIT,
1544
+ 'memory_limit': ResourceLimits.STANDARD_MEMORY_RANGE[0], # Use lower bound of standard memory
1545
+ 'cpu_limit': ResourceLimits.STANDARD_CPU_RANGE[1], # Use upper bound of standard CPU
1733
1546
  'network_access': True,
1734
1547
  }
1735
1548
 
@@ -1859,7 +1672,11 @@ temperature: {temperature}"""
1859
1672
 
1860
1673
  def _deploy_system_instructions(self, target_dir: Path, force_rebuild: bool, results: Dict[str, Any]) -> None:
1861
1674
  """
1862
- Deploy system instructions for PM framework.
1675
+ Deploy system instructions and framework files for PM framework.
1676
+
1677
+ Deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files following hierarchy:
1678
+ - System/User versions → Deploy to ~/.claude/
1679
+ - Project-specific versions → Deploy to <project>/.claude/
1863
1680
 
1864
1681
  Args:
1865
1682
  target_dir: Target directory for deployment
@@ -1867,52 +1684,70 @@ temperature: {temperature}"""
1867
1684
  results: Results dictionary to update
1868
1685
  """
1869
1686
  try:
1870
- # Find the INSTRUCTIONS.md file
1871
- module_path = Path(__file__).parent.parent
1872
- instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
1873
-
1874
- if not instructions_path.exists():
1875
- self.logger.warning(f"System instructions not found: {instructions_path}")
1876
- return
1877
-
1878
- # Target file for system instructions - use CLAUDE.md in user's home .claude directory
1879
- target_file = Path("~/.claude/CLAUDE.md").expanduser()
1687
+ # Determine target location based on deployment type
1688
+ if self._is_project_specific_deployment():
1689
+ # Project-specific files go to project's .claude directory
1690
+ claude_dir = self.working_directory / ".claude"
1691
+ else:
1692
+ # System and user files go to home ~/.claude directory
1693
+ claude_dir = Path.home() / ".claude"
1880
1694
 
1881
1695
  # Ensure .claude directory exists
1882
- target_file.parent.mkdir(exist_ok=True)
1696
+ claude_dir.mkdir(parents=True, exist_ok=True)
1883
1697
 
1884
- # Check if update needed
1885
- if not force_rebuild and target_file.exists():
1886
- # Compare modification times
1887
- if target_file.stat().st_mtime >= instructions_path.stat().st_mtime:
1888
- results["skipped"].append("CLAUDE.md")
1889
- self.logger.debug("System instructions up to date")
1890
- return
1698
+ # Framework files to deploy
1699
+ framework_files = [
1700
+ ("INSTRUCTIONS.md", "CLAUDE.md"), # INSTRUCTIONS.md deploys as CLAUDE.md
1701
+ ("WORKFLOW.md", "WORKFLOW.md"),
1702
+ ("MEMORY.md", "MEMORY.md")
1703
+ ]
1891
1704
 
1892
- # Read and deploy system instructions
1893
- instructions_content = instructions_path.read_text()
1894
- target_file.write_text(instructions_content)
1705
+ # Find the agents directory with framework files
1706
+ # Use centralized paths for consistency
1707
+ from claude_mpm.config.paths import paths
1708
+ agents_path = paths.agents_dir
1895
1709
 
1896
- is_update = target_file.exists()
1897
- if is_update:
1898
- results["updated"].append({
1899
- "name": "CLAUDE.md",
1900
- "template": str(instructions_path),
1901
- "target": str(target_file)
1902
- })
1903
- self.logger.info("Updated system instructions")
1904
- else:
1905
- results["deployed"].append({
1906
- "name": "CLAUDE.md",
1907
- "template": str(instructions_path),
1710
+ for source_name, target_name in framework_files:
1711
+ source_path = agents_path / source_name
1712
+
1713
+ if not source_path.exists():
1714
+ self.logger.warning(f"Framework file not found: {source_path}")
1715
+ continue
1716
+
1717
+ target_file = claude_dir / target_name
1718
+
1719
+ # Check if update needed
1720
+ if not force_rebuild and target_file.exists():
1721
+ # Compare modification times
1722
+ if target_file.stat().st_mtime >= source_path.stat().st_mtime:
1723
+ results["skipped"].append(target_name)
1724
+ self.logger.debug(f"Framework file {target_name} up to date")
1725
+ continue
1726
+
1727
+ # Read and deploy framework file
1728
+ file_content = source_path.read_text()
1729
+ target_file.write_text(file_content)
1730
+
1731
+ # Track deployment
1732
+ file_existed = target_file.exists()
1733
+ deployment_info = {
1734
+ "name": target_name,
1735
+ "template": str(source_path),
1908
1736
  "target": str(target_file)
1909
- })
1910
- self.logger.info("Deployed system instructions")
1737
+ }
1738
+
1739
+ if file_existed:
1740
+ results["updated"].append(deployment_info)
1741
+ self.logger.info(f"Updated framework file: {target_name}")
1742
+ else:
1743
+ results["deployed"].append(deployment_info)
1744
+ self.logger.info(f"Deployed framework file: {target_name}")
1911
1745
 
1912
1746
  except Exception as e:
1913
1747
  error_msg = f"Failed to deploy system instructions: {e}"
1914
1748
  self.logger.error(error_msg)
1915
1749
  results["errors"].append(error_msg)
1750
+ # Not raising AgentDeploymentError as this is non-critical
1916
1751
 
1917
1752
  def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
1918
1753
  """
@@ -2122,6 +1957,519 @@ metadata:
2122
1957
 
2123
1958
  return None
2124
1959
 
1960
+ def _try_async_deployment(self, target_dir: Optional[Path], force_rebuild: bool,
1961
+ config: Optional[Config], deployment_start_time: float) -> Optional[Dict[str, Any]]:
1962
+ """
1963
+ Try to use async deployment for better performance.
1964
+
1965
+ WHY: Async deployment is 50-70% faster than synchronous deployment
1966
+ by using concurrent operations for file I/O and processing.
1967
+
1968
+ Args:
1969
+ target_dir: Target directory for deployment
1970
+ force_rebuild: Whether to force rebuild
1971
+ config: Configuration object
1972
+ deployment_start_time: Start time for metrics
1973
+
1974
+ Returns:
1975
+ Deployment results if successful, None if async not available
1976
+ """
1977
+ try:
1978
+ from .async_agent_deployment import deploy_agents_async_wrapper
1979
+ self.logger.info("Using async deployment for improved performance")
1980
+
1981
+ # Run async deployment
1982
+ results = deploy_agents_async_wrapper(
1983
+ templates_dir=self.templates_dir,
1984
+ base_agent_path=self.base_agent_path,
1985
+ working_directory=self.working_directory,
1986
+ target_dir=target_dir,
1987
+ force_rebuild=force_rebuild,
1988
+ config=config
1989
+ )
1990
+
1991
+ # Add metrics about async vs sync
1992
+ if 'metrics' in results:
1993
+ results['metrics']['deployment_method'] = 'async'
1994
+ duration_ms = results['metrics'].get('duration_ms', 0)
1995
+ self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
1996
+
1997
+ # Update internal metrics
1998
+ self._deployment_metrics['total_deployments'] += 1
1999
+ if not results.get('errors'):
2000
+ self._deployment_metrics['successful_deployments'] += 1
2001
+ else:
2002
+ self._deployment_metrics['failed_deployments'] += 1
2003
+
2004
+ return results
2005
+
2006
+ except ImportError:
2007
+ self.logger.warning("Async deployment not available, falling back to sync")
2008
+ return None
2009
+ except Exception as e:
2010
+ self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
2011
+ return None
2012
+
2013
+ def _load_deployment_config(self, config: Optional[Config]) -> tuple:
2014
+ """
2015
+ Load and process deployment configuration.
2016
+
2017
+ WHY: Centralized configuration loading reduces duplication
2018
+ and ensures consistent handling of exclusion settings.
2019
+
2020
+ Args:
2021
+ config: Optional configuration object
2022
+
2023
+ Returns:
2024
+ Tuple of (config, excluded_agents)
2025
+ """
2026
+ # Load configuration if not provided
2027
+ if config is None:
2028
+ config = Config()
2029
+
2030
+ # Get agent exclusion configuration
2031
+ excluded_agents = config.get('agent_deployment.excluded_agents', [])
2032
+ case_sensitive = config.get('agent_deployment.case_sensitive', False)
2033
+ exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
2034
+
2035
+ # Normalize excluded agents list for comparison
2036
+ if not case_sensitive:
2037
+ excluded_agents = [agent.lower() for agent in excluded_agents]
2038
+
2039
+ # Log exclusion configuration if agents are being excluded
2040
+ if excluded_agents:
2041
+ self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
2042
+ self.logger.debug(f"Case sensitive matching: {case_sensitive}")
2043
+ self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
2044
+
2045
+ return config, excluded_agents
2046
+
2047
+ def _determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
2048
+ """
2049
+ Determine the correct agents directory based on input.
2050
+
2051
+ WHY: Different deployment scenarios require different directory
2052
+ structures. This method centralizes the logic for consistency.
2053
+
2054
+ HIERARCHY:
2055
+ - System agents → Deploy to ~/.claude/agents/ (user's home directory)
2056
+ - User custom agents from ~/.claude-mpm/agents/ → Deploy to ~/.claude/agents/
2057
+ - Project-specific agents from <project>/.claude-mpm/agents/ → Deploy to <project>/.claude/agents/
2058
+
2059
+ Args:
2060
+ target_dir: Optional target directory
2061
+
2062
+ Returns:
2063
+ Path to agents directory
2064
+ """
2065
+ if not target_dir:
2066
+ # Default deployment location depends on agent source
2067
+ # Check if we're deploying system agents or user/project agents
2068
+ if self._is_system_agent_deployment():
2069
+ # System agents go to user's home ~/.claude/agents/
2070
+ return Path.home() / ".claude" / "agents"
2071
+ elif self._is_project_specific_deployment():
2072
+ # Project agents stay in project directory
2073
+ return self.working_directory / ".claude" / "agents"
2074
+ else:
2075
+ # Default: User custom agents go to home ~/.claude/agents/
2076
+ return Path.home() / ".claude" / "agents"
2077
+
2078
+ # If target_dir provided, use it directly (caller decides structure)
2079
+ target_dir = Path(target_dir)
2080
+
2081
+ # Check if this is already an agents directory
2082
+ if target_dir.name == "agents":
2083
+ # Already an agents directory, use as-is
2084
+ return target_dir
2085
+ elif target_dir.name == ".claude-mpm":
2086
+ # .claude-mpm directory, add agents subdirectory
2087
+ return target_dir / "agents"
2088
+ elif target_dir.name == ".claude":
2089
+ # .claude directory, add agents subdirectory
2090
+ return target_dir / "agents"
2091
+ else:
2092
+ # Assume it's a project directory, add .claude/agents
2093
+ return target_dir / ".claude" / "agents"
2094
+
2095
+ def _is_system_agent_deployment(self) -> bool:
2096
+ """
2097
+ Check if this is a deployment of system agents.
2098
+
2099
+ System agents are those provided by the claude-mpm package itself,
2100
+ located in the package's agents/templates directory.
2101
+
2102
+ Returns:
2103
+ True if deploying system agents, False otherwise
2104
+ """
2105
+ # Check if templates_dir points to the system templates
2106
+ if self.templates_dir and self.templates_dir.exists():
2107
+ # System agents are in the package's agents/templates directory
2108
+ try:
2109
+ # Check if templates_dir is within the claude_mpm package structure
2110
+ templates_str = str(self.templates_dir.resolve())
2111
+ return ("site-packages/claude_mpm" in templates_str or
2112
+ "src/claude_mpm/agents/templates" in templates_str or
2113
+ (paths.agents_dir / "templates").resolve() == self.templates_dir.resolve())
2114
+ except Exception:
2115
+ pass
2116
+ return False
2117
+
2118
+ def _is_project_specific_deployment(self) -> bool:
2119
+ """
2120
+ Check if deploying project-specific agents.
2121
+
2122
+ Project-specific agents are those found in the project's
2123
+ .claude-mpm/agents/ directory.
2124
+
2125
+ Returns:
2126
+ True if deploying project-specific agents, False otherwise
2127
+ """
2128
+ # Check if we're in a project directory with .claude-mpm/agents
2129
+ project_agents_dir = self.working_directory / ".claude-mpm" / "agents"
2130
+ if project_agents_dir.exists():
2131
+ # Check if templates_dir points to project agents
2132
+ if self.templates_dir and self.templates_dir.exists():
2133
+ try:
2134
+ return project_agents_dir.resolve() == self.templates_dir.resolve()
2135
+ except Exception:
2136
+ pass
2137
+ return False
2138
+
2139
+ def _is_user_custom_deployment(self) -> bool:
2140
+ """
2141
+ Check if deploying user custom agents.
2142
+
2143
+ User custom agents are those in ~/.claude-mpm/agents/
2144
+
2145
+ Returns:
2146
+ True if deploying user custom agents, False otherwise
2147
+ """
2148
+ user_agents_dir = Path.home() / ".claude-mpm" / "agents"
2149
+ if user_agents_dir.exists():
2150
+ # Check if templates_dir points to user agents
2151
+ if self.templates_dir and self.templates_dir.exists():
2152
+ try:
2153
+ return user_agents_dir.resolve() == self.templates_dir.resolve()
2154
+ except Exception:
2155
+ pass
2156
+ return False
2157
+
2158
+ def _initialize_deployment_results(self, agents_dir: Path, deployment_start_time: float) -> Dict[str, Any]:
2159
+ """
2160
+ Initialize the deployment results dictionary.
2161
+
2162
+ WHY: Consistent result structure ensures all deployment
2163
+ operations return the same format for easier processing.
2164
+
2165
+ Args:
2166
+ agents_dir: Target agents directory
2167
+ deployment_start_time: Start time for metrics
2168
+
2169
+ Returns:
2170
+ Initialized results dictionary
2171
+ """
2172
+ return {
2173
+ "target_dir": str(agents_dir),
2174
+ "deployed": [],
2175
+ "errors": [],
2176
+ "skipped": [],
2177
+ "updated": [],
2178
+ "migrated": [], # Track agents migrated from old format
2179
+ "converted": [], # Track YAML to MD conversions
2180
+ "repaired": [], # Track agents with repaired frontmatter
2181
+ "total": 0,
2182
+ # METRICS: Add detailed timing and performance data to results
2183
+ "metrics": {
2184
+ "start_time": deployment_start_time,
2185
+ "end_time": None,
2186
+ "duration_ms": None,
2187
+ "agent_timings": {}, # Track individual agent deployment times
2188
+ "validation_times": {}, # Track template validation times
2189
+ "resource_usage": {} # Could track memory/CPU if needed
2190
+ }
2191
+ }
2192
+
2193
+ def _repair_existing_agents(self, agents_dir: Path, results: Dict[str, Any]) -> None:
2194
+ """
2195
+ Validate and repair broken frontmatter in existing agents.
2196
+
2197
+ WHY: Ensures all existing agents have valid YAML frontmatter
2198
+ before deployment, preventing runtime errors in Claude Code.
2199
+
2200
+ Args:
2201
+ agents_dir: Directory containing agent files
2202
+ results: Results dictionary to update
2203
+ """
2204
+ repair_results = self._validate_and_repair_existing_agents(agents_dir)
2205
+ if repair_results["repaired"]:
2206
+ results["repaired"] = repair_results["repaired"]
2207
+ self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
2208
+ for agent_name in repair_results["repaired"]:
2209
+ self.logger.debug(f" - Repaired: {agent_name}")
2210
+
2211
+ def _determine_source_tier(self) -> str:
2212
+ """
2213
+ Determine the source tier for logging.
2214
+
2215
+ WHY: Understanding which tier (SYSTEM/USER/PROJECT) agents
2216
+ are being deployed from helps with debugging and auditing.
2217
+
2218
+ Returns:
2219
+ Source tier string
2220
+ """
2221
+ if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2222
+ return "PROJECT"
2223
+ elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2224
+ return "USER"
2225
+ return "SYSTEM"
2226
+
2227
+ def _load_base_agent(self) -> tuple:
2228
+ """
2229
+ Load base agent content and version.
2230
+
2231
+ WHY: Base agent contains shared configuration that all agents
2232
+ inherit, reducing duplication and ensuring consistency.
2233
+
2234
+ Returns:
2235
+ Tuple of (base_agent_data, base_agent_version)
2236
+ """
2237
+ base_agent_data = {}
2238
+ base_agent_version = (0, 0, 0)
2239
+
2240
+ if self.base_agent_path.exists():
2241
+ try:
2242
+ import json
2243
+ base_agent_data = json.loads(self.base_agent_path.read_text())
2244
+ # Handle both 'base_version' (new format) and 'version' (old format)
2245
+ # MIGRATION PATH: Supporting both formats during transition period
2246
+ base_agent_version = self._parse_version(
2247
+ base_agent_data.get('base_version') or base_agent_data.get('version', 0)
2248
+ )
2249
+ self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
2250
+ except Exception as e:
2251
+ # NON-FATAL: Base agent is optional enhancement, not required
2252
+ self.logger.warning(f"Could not load base agent: {e}")
2253
+
2254
+ return base_agent_data, base_agent_version
2255
+
2256
+ def _get_filtered_templates(self, excluded_agents: list, config: Config) -> list:
2257
+ """
2258
+ Get and filter template files based on exclusion rules.
2259
+
2260
+ WHY: Centralized filtering logic ensures consistent exclusion
2261
+ handling across different deployment scenarios.
2262
+
2263
+ Args:
2264
+ excluded_agents: List of agents to exclude
2265
+ config: Configuration object
2266
+
2267
+ Returns:
2268
+ List of filtered template files
2269
+ """
2270
+ # Get all template files
2271
+ template_files = list(self.templates_dir.glob("*.json"))
2272
+
2273
+ # Build the combined exclusion set
2274
+ # Start with hardcoded exclusions (these are ALWAYS excluded)
2275
+ hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS",
2276
+ "README", "pm", "PM", "project_manager"}
2277
+
2278
+ # Get case sensitivity setting
2279
+ case_sensitive = config.get('agent_deployment.case_sensitive', False)
2280
+
2281
+ # Filter out excluded agents
2282
+ filtered_files = []
2283
+ excluded_count = 0
2284
+
2285
+ for f in template_files:
2286
+ agent_name = f.stem
2287
+
2288
+ # Check hardcoded exclusions (always case-sensitive)
2289
+ if agent_name in hardcoded_exclusions:
2290
+ self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
2291
+ excluded_count += 1
2292
+ continue
2293
+
2294
+ # Check file patterns
2295
+ if agent_name.startswith(".") or agent_name.endswith(".backup"):
2296
+ self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
2297
+ excluded_count += 1
2298
+ continue
2299
+
2300
+ # Check user-configured exclusions
2301
+ compare_name = agent_name.lower() if not case_sensitive else agent_name
2302
+ if compare_name in excluded_agents:
2303
+ self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
2304
+ excluded_count += 1
2305
+ continue
2306
+
2307
+ # Agent is not excluded, add to filtered list
2308
+ filtered_files.append(f)
2309
+
2310
+ if excluded_count > 0:
2311
+ self.logger.info(f"Excluded {excluded_count} agents from deployment")
2312
+
2313
+ return filtered_files
2314
+
2315
+ def _deploy_single_agent(self, template_file: Path, agents_dir: Path,
2316
+ base_agent_data: dict, base_agent_version: tuple,
2317
+ force_rebuild: bool, deployment_mode: str,
2318
+ results: Dict[str, Any]) -> None:
2319
+ """
2320
+ Deploy a single agent template.
2321
+
2322
+ WHY: Extracting single agent deployment logic reduces complexity
2323
+ and makes the main deployment loop more readable.
2324
+
2325
+ Args:
2326
+ template_file: Agent template file
2327
+ agents_dir: Target agents directory
2328
+ base_agent_data: Base agent data
2329
+ base_agent_version: Base agent version
2330
+ force_rebuild: Whether to force rebuild
2331
+ deployment_mode: Deployment mode (update/project)
2332
+ results: Results dictionary to update
2333
+ """
2334
+ try:
2335
+ # METRICS: Track individual agent deployment time
2336
+ agent_start_time = time.time()
2337
+
2338
+ agent_name = template_file.stem
2339
+ target_file = agents_dir / f"{agent_name}.md"
2340
+
2341
+ # Check if agent needs update
2342
+ needs_update, is_migration, reason = self._check_update_status(
2343
+ target_file, template_file, base_agent_version,
2344
+ force_rebuild, deployment_mode
2345
+ )
2346
+
2347
+ # Skip if exists and doesn't need update (only in update mode)
2348
+ if target_file.exists() and not needs_update and deployment_mode != "project":
2349
+ results["skipped"].append(agent_name)
2350
+ self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
2351
+ return
2352
+
2353
+ # Build the agent file as markdown with YAML frontmatter
2354
+ agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
2355
+
2356
+ # Write the agent file
2357
+ is_update = target_file.exists()
2358
+ target_file.write_text(agent_content)
2359
+
2360
+ # Record metrics and update results
2361
+ self._record_agent_deployment(
2362
+ agent_name, template_file, target_file,
2363
+ is_update, is_migration, reason,
2364
+ agent_start_time, results
2365
+ )
2366
+
2367
+ except AgentDeploymentError as e:
2368
+ # Re-raise our custom exceptions
2369
+ self.logger.error(str(e))
2370
+ results["errors"].append(str(e))
2371
+ except Exception as e:
2372
+ # Wrap generic exceptions with context
2373
+ error_msg = f"Failed to build {template_file.name}: {e}"
2374
+ self.logger.error(error_msg)
2375
+ results["errors"].append(error_msg)
2376
+
2377
+ def _check_update_status(self, target_file: Path, template_file: Path,
2378
+ base_agent_version: tuple, force_rebuild: bool,
2379
+ deployment_mode: str) -> tuple:
2380
+ """
2381
+ Check if agent needs update and determine status.
2382
+
2383
+ WHY: Centralized update checking logic ensures consistent
2384
+ version comparison and migration detection.
2385
+
2386
+ Args:
2387
+ target_file: Target agent file
2388
+ template_file: Template file
2389
+ base_agent_version: Base agent version
2390
+ force_rebuild: Whether to force rebuild
2391
+ deployment_mode: Deployment mode
2392
+
2393
+ Returns:
2394
+ Tuple of (needs_update, is_migration, reason)
2395
+ """
2396
+ needs_update = force_rebuild
2397
+ is_migration = False
2398
+ reason = ""
2399
+
2400
+ # In project deployment mode, always deploy regardless of version
2401
+ if deployment_mode == "project":
2402
+ if target_file.exists():
2403
+ needs_update = True
2404
+ self.logger.debug(f"Project deployment mode: will deploy {template_file.stem}")
2405
+ else:
2406
+ needs_update = True
2407
+ elif not needs_update and target_file.exists():
2408
+ # In update mode, check version compatibility
2409
+ needs_update, reason = self._check_agent_needs_update(
2410
+ target_file, template_file, base_agent_version
2411
+ )
2412
+ if needs_update:
2413
+ # Check if this is a migration from old format
2414
+ if "migration needed" in reason:
2415
+ is_migration = True
2416
+ self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
2417
+ else:
2418
+ self.logger.info(f"Agent {template_file.stem} needs update: {reason}")
2419
+
2420
+ return needs_update, is_migration, reason
2421
+
2422
+ def _record_agent_deployment(self, agent_name: str, template_file: Path,
2423
+ target_file: Path, is_update: bool,
2424
+ is_migration: bool, reason: str,
2425
+ agent_start_time: float, results: Dict[str, Any]) -> None:
2426
+ """
2427
+ Record deployment metrics and update results.
2428
+
2429
+ WHY: Centralized metrics recording ensures consistent tracking
2430
+ of deployment performance and statistics.
2431
+
2432
+ Args:
2433
+ agent_name: Name of the agent
2434
+ template_file: Template file
2435
+ target_file: Target file
2436
+ is_update: Whether this is an update
2437
+ is_migration: Whether this is a migration
2438
+ reason: Update/migration reason
2439
+ agent_start_time: Start time for this agent
2440
+ results: Results dictionary to update
2441
+ """
2442
+ # METRICS: Record deployment time for this agent
2443
+ agent_deployment_time = (time.time() - agent_start_time) * 1000 # Convert to ms
2444
+ results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
2445
+
2446
+ # METRICS: Update agent type deployment counts
2447
+ self._deployment_metrics['agent_type_counts'][agent_name] = \
2448
+ self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
2449
+
2450
+ deployment_info = {
2451
+ "name": agent_name,
2452
+ "template": str(template_file),
2453
+ "target": str(target_file),
2454
+ "deployment_time_ms": agent_deployment_time
2455
+ }
2456
+
2457
+ if is_migration:
2458
+ deployment_info["reason"] = reason
2459
+ results["migrated"].append(deployment_info)
2460
+ self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
2461
+
2462
+ # METRICS: Track migration statistics
2463
+ self._deployment_metrics['migrations_performed'] += 1
2464
+ self._deployment_metrics['version_migration_count'] += 1
2465
+
2466
+ elif is_update:
2467
+ results["updated"].append(deployment_info)
2468
+ self.logger.debug(f"Updated agent: {agent_name}")
2469
+ else:
2470
+ results["deployed"].append(deployment_info)
2471
+ self.logger.debug(f"Built and deployed agent: {agent_name}")
2472
+
2125
2473
  def _validate_and_repair_existing_agents(self, agents_dir: Path) -> Dict[str, Any]:
2126
2474
  """
2127
2475
  Validate and repair broken frontmatter in existing agent files.
@@ -2244,4 +2592,76 @@ metadata:
2244
2592
  self.logger.error(error_msg)
2245
2593
  results["errors"].append(error_msg)
2246
2594
 
2247
- return results
2595
+ return results
2596
+
2597
+ # ================================================================================
2598
+ # Interface Adapter Methods
2599
+ # ================================================================================
2600
+ # These methods adapt the existing implementation to comply with AgentDeploymentInterface
2601
+
2602
+ def validate_agent(self, agent_path: Path) -> tuple[bool, List[str]]:
2603
+ """Validate agent configuration and structure.
2604
+
2605
+ WHY: This adapter method provides interface compliance while leveraging
2606
+ the existing validation logic in _check_agent_needs_update and other methods.
2607
+
2608
+ Args:
2609
+ agent_path: Path to agent configuration file
2610
+
2611
+ Returns:
2612
+ Tuple of (is_valid, list_of_errors)
2613
+ """
2614
+ errors = []
2615
+
2616
+ try:
2617
+ if not agent_path.exists():
2618
+ return False, [f"Agent file not found: {agent_path}"]
2619
+
2620
+ content = agent_path.read_text()
2621
+
2622
+ # Check YAML frontmatter format
2623
+ if not content.startswith("---"):
2624
+ errors.append("Missing YAML frontmatter")
2625
+
2626
+ # Extract and validate version
2627
+ import re
2628
+ version_match = re.search(r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE)
2629
+ if not version_match:
2630
+ errors.append("Missing version field in frontmatter")
2631
+
2632
+ # Check for required fields
2633
+ required_fields = ['name', 'description', 'tools']
2634
+ for field in required_fields:
2635
+ field_match = re.search(rf'^{field}:\s*.+$', content, re.MULTILINE)
2636
+ if not field_match:
2637
+ errors.append(f"Missing required field: {field}")
2638
+
2639
+ # If no errors, validation passed
2640
+ return len(errors) == 0, errors
2641
+
2642
+ except Exception as e:
2643
+ return False, [f"Validation error: {str(e)}"]
2644
+
2645
+ def get_deployment_status(self) -> Dict[str, Any]:
2646
+ """Get current deployment status and metrics.
2647
+
2648
+ WHY: This adapter method provides interface compliance by wrapping
2649
+ verify_deployment and adding deployment metrics.
2650
+
2651
+ Returns:
2652
+ Dictionary with deployment status information
2653
+ """
2654
+ # Get verification results
2655
+ verification = self.verify_deployment()
2656
+
2657
+ # Add deployment metrics
2658
+ status = {
2659
+ "deployment_metrics": self._deployment_metrics.copy(),
2660
+ "verification": verification,
2661
+ "agents_deployed": len(verification.get("agents_found", [])),
2662
+ "agents_needing_migration": len(verification.get("agents_needing_migration", [])),
2663
+ "has_warnings": len(verification.get("warnings", [])) > 0,
2664
+ "environment_configured": bool(verification.get("environment", {}))
2665
+ }
2666
+
2667
+ return status