claude-mpm 3.5.1__py3-none-any.whl → 3.5.4__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 (30) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +29 -2
  3. claude_mpm/agents/agent_loader.py +109 -15
  4. claude_mpm/agents/base_agent.json +1 -1
  5. claude_mpm/agents/frontmatter_validator.py +448 -0
  6. claude_mpm/agents/templates/data_engineer.json +4 -3
  7. claude_mpm/agents/templates/documentation.json +4 -3
  8. claude_mpm/agents/templates/engineer.json +4 -3
  9. claude_mpm/agents/templates/ops.json +4 -3
  10. claude_mpm/agents/templates/pm.json +5 -4
  11. claude_mpm/agents/templates/qa.json +4 -3
  12. claude_mpm/agents/templates/research.json +8 -7
  13. claude_mpm/agents/templates/security.json +4 -3
  14. claude_mpm/agents/templates/test_integration.json +4 -3
  15. claude_mpm/agents/templates/version_control.json +4 -3
  16. claude_mpm/cli/__main__.py +24 -0
  17. claude_mpm/cli/commands/agents.py +354 -6
  18. claude_mpm/cli/parser.py +36 -0
  19. claude_mpm/constants.py +2 -0
  20. claude_mpm/core/agent_registry.py +4 -1
  21. claude_mpm/core/claude_runner.py +224 -8
  22. claude_mpm/services/agents/deployment/agent_deployment.py +39 -9
  23. claude_mpm/services/agents/registry/agent_registry.py +22 -1
  24. claude_mpm/validation/agent_validator.py +56 -1
  25. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/METADATA +18 -3
  26. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/RECORD +30 -28
  27. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/WHEEL +0 -0
  28. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/entry_points.txt +0 -0
  29. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/licenses/LICENSE +0 -0
  30. {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/top_level.txt +0 -0
@@ -110,6 +110,22 @@ class ClaudeRunner:
110
110
  self.logger.error(f"Failed to load configuration: {e}")
111
111
  raise RuntimeError(f"Configuration initialization failed: {e}") from e
112
112
 
113
+ # Initialize response logging if enabled
114
+ self.response_logger = None
115
+ response_config = self.config.get('response_logging', {})
116
+ if response_config.get('enabled', False):
117
+ try:
118
+ from claude_mpm.services.claude_session_logger import get_session_logger
119
+ self.response_logger = get_session_logger(self.config)
120
+ if self.project_logger:
121
+ self.project_logger.log_system(
122
+ "Response logging initialized",
123
+ level="INFO",
124
+ component="logging"
125
+ )
126
+ except Exception as e:
127
+ self.logger.warning(f"Failed to initialize response logger: {e}")
128
+
113
129
  # Initialize hook service
114
130
  try:
115
131
  self.hook_service = HookService(self.config)
@@ -199,7 +215,7 @@ class ClaudeRunner:
199
215
  return False
200
216
 
201
217
  except FileNotFoundError as e:
202
- error_msg = f"Agent templates not found: {e}"
218
+ error_msg = f"Agent files not found: {e}"
203
219
  self.logger.error(error_msg)
204
220
  print(f"❌ {error_msg}")
205
221
  print("💡 Ensure claude-mpm is properly installed")
@@ -228,9 +244,10 @@ class ClaudeRunner:
228
244
  def ensure_project_agents(self) -> bool:
229
245
  """Ensure system agents are available in the project directory.
230
246
 
231
- Deploys system agents to project's .claude-mpm/agents/ directory
232
- if they don't exist or are outdated. This enables project-level
233
- agent customization and ensures all agents are available locally.
247
+ Deploys system agents to project's .claude/agents/ directory
248
+ if they don't exist or are outdated. This ensures agents are
249
+ available for Claude Code to use. Project-specific JSON templates
250
+ should be placed in .claude-mpm/agents/.
234
251
 
235
252
  Returns:
236
253
  bool: True if agents are available, False on error
@@ -250,10 +267,12 @@ class ClaudeRunner:
250
267
  component="deployment"
251
268
  )
252
269
 
253
- # Deploy agents to project directory with project deployment mode
270
+ # Deploy agents to project's .claude/agents directory (not .claude-mpm)
254
271
  # This ensures all system agents are deployed regardless of version
272
+ # .claude-mpm/agents/ should only contain JSON source templates
273
+ # .claude/agents/ should contain the built MD files for Claude Code
255
274
  results = self.deployment_service.deploy_agents(
256
- target_dir=project_dir / ".claude-mpm",
275
+ target_dir=project_dir / ".claude",
257
276
  force_rebuild=False,
258
277
  deployment_mode="project"
259
278
  )
@@ -286,8 +305,146 @@ class ClaudeRunner:
286
305
  )
287
306
  return False
288
307
 
308
+ def deploy_project_agents_to_claude(self) -> bool:
309
+ """Deploy project agents from .claude-mpm/agents/ to .claude/agents/.
310
+
311
+ This method handles the deployment of project-specific agents (JSON format)
312
+ from the project's agents directory to Claude's agent directory.
313
+ Project agents take precedence over system agents.
314
+
315
+ WHY: Project agents allow teams to define custom, project-specific agents
316
+ that override system agents. These are stored in JSON format in
317
+ .claude-mpm/agents/ and need to be deployed to .claude/agents/
318
+ as MD files for Claude to use them.
319
+
320
+ Returns:
321
+ bool: True if deployment successful or no agents to deploy, False on error
322
+ """
323
+ try:
324
+ project_dir = Path.cwd()
325
+ project_agents_dir = project_dir / ".claude-mpm" / "agents"
326
+ claude_agents_dir = project_dir / ".claude" / "agents"
327
+
328
+ # Check if project agents directory exists
329
+ if not project_agents_dir.exists():
330
+ self.logger.debug("No project agents directory found")
331
+ return True # Not an error - just no project agents
332
+
333
+ # Get JSON agent files from agents directory
334
+ json_files = list(project_agents_dir.glob("*.json"))
335
+ if not json_files:
336
+ self.logger.debug("No JSON agents in project")
337
+ return True
338
+
339
+ # Create .claude/agents directory if needed
340
+ claude_agents_dir.mkdir(parents=True, exist_ok=True)
341
+
342
+ self.logger.info(f"Deploying {len(json_files)} project agents to .claude/agents/")
343
+ if self.project_logger:
344
+ self.project_logger.log_system(
345
+ f"Deploying project agents from {project_agents_dir} to {claude_agents_dir}",
346
+ level="INFO",
347
+ component="deployment"
348
+ )
349
+
350
+ deployed_count = 0
351
+ updated_count = 0
352
+ errors = []
353
+
354
+ # Deploy each JSON agent
355
+ for json_file in json_files:
356
+ try:
357
+ agent_name = json_file.stem
358
+ target_file = claude_agents_dir / f"{agent_name}.md"
359
+
360
+ # Check if agent needs update
361
+ needs_update = True
362
+ if target_file.exists():
363
+ # Check if it's a project agent (has project marker)
364
+ existing_content = target_file.read_text()
365
+ if "author: claude-mpm-project" in existing_content or "source: project" in existing_content:
366
+ # Compare modification times
367
+ if target_file.stat().st_mtime >= json_file.stat().st_mtime:
368
+ needs_update = False
369
+ self.logger.debug(f"Project agent {agent_name} is up to date")
370
+
371
+ if needs_update:
372
+ # Use deployment service to build the agent
373
+ from claude_mpm.services.agents.deployment.agent_deployment import AgentDeploymentService
374
+
375
+ # Create a temporary deployment service for this specific task
376
+ project_deployment = AgentDeploymentService(
377
+ templates_dir=project_agents_dir,
378
+ base_agent_path=project_dir / ".claude-mpm" / "agents" / "base_agent.json"
379
+ )
380
+
381
+ # Load base agent data if available
382
+ base_agent_data = {}
383
+ base_agent_path = project_dir / ".claude-mpm" / "agents" / "base_agent.json"
384
+ if base_agent_path.exists():
385
+ import json
386
+ try:
387
+ base_agent_data = json.loads(base_agent_path.read_text())
388
+ except Exception as e:
389
+ self.logger.warning(f"Could not load project base agent: {e}")
390
+
391
+ # Build the agent markdown
392
+ agent_content = project_deployment._build_agent_markdown(
393
+ agent_name, json_file, base_agent_data
394
+ )
395
+
396
+ # Mark as project agent
397
+ agent_content = agent_content.replace(
398
+ "author: claude-mpm",
399
+ "author: claude-mpm-project"
400
+ )
401
+
402
+ # Write the agent file
403
+ is_update = target_file.exists()
404
+ target_file.write_text(agent_content)
405
+
406
+ if is_update:
407
+ updated_count += 1
408
+ self.logger.info(f"Updated project agent: {agent_name}")
409
+ else:
410
+ deployed_count += 1
411
+ self.logger.info(f"Deployed project agent: {agent_name}")
412
+
413
+ except Exception as e:
414
+ error_msg = f"Failed to deploy project agent {json_file.name}: {e}"
415
+ self.logger.error(error_msg)
416
+ errors.append(error_msg)
417
+
418
+ # Report results
419
+ if deployed_count > 0 or updated_count > 0:
420
+ print(f"✓ Deployed {deployed_count} project agents, updated {updated_count}")
421
+ if self.project_logger:
422
+ self.project_logger.log_system(
423
+ f"Project agent deployment: {deployed_count} deployed, {updated_count} updated",
424
+ level="INFO",
425
+ component="deployment"
426
+ )
427
+
428
+ if errors:
429
+ for error in errors:
430
+ print(f"⚠️ {error}")
431
+ return False
432
+
433
+ return True
434
+
435
+ except Exception as e:
436
+ error_msg = f"Failed to deploy project agents: {e}"
437
+ self.logger.error(error_msg)
438
+ print(f"⚠️ {error_msg}")
439
+ if self.project_logger:
440
+ self.project_logger.log_system(error_msg, level="ERROR", component="deployment")
441
+ return False
442
+
289
443
  def run_interactive(self, initial_context: Optional[str] = None):
290
444
  """Run Claude in interactive mode."""
445
+ # TODO: Add response logging for interactive mode
446
+ # This requires capturing stdout from the exec'd process or using subprocess with PTY
447
+
291
448
  # Connect to Socket.IO server if enabled
292
449
  if self.enable_websocket:
293
450
  try:
@@ -336,10 +493,13 @@ class ClaudeRunner:
336
493
  component="session"
337
494
  )
338
495
 
339
- # Setup agents
496
+ # Setup agents - first deploy system agents, then project agents
340
497
  if not self.setup_agents():
341
498
  print("Continuing without native agents...")
342
499
 
500
+ # Deploy project-specific agents if they exist
501
+ self.deploy_project_agents_to_claude()
502
+
343
503
  # Build command with system instructions
344
504
  cmd = [
345
505
  "claude",
@@ -587,10 +747,13 @@ class ClaudeRunner:
587
747
  component="session"
588
748
  )
589
749
 
590
- # Setup agents
750
+ # Setup agents - first deploy system agents, then project agents
591
751
  if not self.setup_agents():
592
752
  print("Continuing without native agents...")
593
753
 
754
+ # Deploy project-specific agents if they exist
755
+ self.deploy_project_agents_to_claude()
756
+
594
757
  # Combine context and prompt
595
758
  full_prompt = prompt
596
759
  if context:
@@ -671,6 +834,22 @@ class ClaudeRunner:
671
834
  response = result.stdout.strip()
672
835
  print(response)
673
836
 
837
+ # Log response if logging enabled
838
+ if self.response_logger and response:
839
+ execution_time = time.time() - start_time
840
+ response_summary = prompt[:200] + "..." if len(prompt) > 200 else prompt
841
+ self.response_logger.log_response(
842
+ request_summary=response_summary,
843
+ response_content=response,
844
+ metadata={
845
+ "mode": "oneshot",
846
+ "model": "opus",
847
+ "exit_code": result.returncode,
848
+ "execution_time": execution_time
849
+ },
850
+ agent="claude-direct"
851
+ )
852
+
674
853
  # Broadcast output to WebSocket clients
675
854
  if self.websocket_server and response:
676
855
  self.websocket_server.claude_output(response, "stdout")
@@ -1185,6 +1364,10 @@ class ClaudeRunner:
1185
1364
  import tty
1186
1365
  import signal
1187
1366
 
1367
+ # Collect output for response logging if enabled
1368
+ collected_output = [] if self.response_logger else None
1369
+ collected_input = [] if self.response_logger else None
1370
+
1188
1371
  # Save original terminal settings
1189
1372
  original_tty = None
1190
1373
  if sys.stdin.isatty():
@@ -1247,6 +1430,13 @@ class ClaudeRunner:
1247
1430
  data = os.read(master_fd, 4096)
1248
1431
  if data:
1249
1432
  os.write(sys.stdout.fileno(), data)
1433
+ # Collect output for response logging
1434
+ if collected_output is not None:
1435
+ try:
1436
+ output_text = data.decode('utf-8', errors='replace')
1437
+ collected_output.append(output_text)
1438
+ except Exception:
1439
+ pass
1250
1440
  # Broadcast output to WebSocket clients
1251
1441
  if self.websocket_server:
1252
1442
  try:
@@ -1265,12 +1455,38 @@ class ClaudeRunner:
1265
1455
  data = os.read(sys.stdin.fileno(), 4096)
1266
1456
  if data:
1267
1457
  os.write(master_fd, data)
1458
+ # Collect input for response logging
1459
+ if collected_input is not None:
1460
+ try:
1461
+ input_text = data.decode('utf-8', errors='replace')
1462
+ collected_input.append(input_text)
1463
+ except Exception:
1464
+ pass
1268
1465
  except OSError:
1269
1466
  break
1270
1467
 
1271
1468
  # Wait for process to complete
1272
1469
  process.wait()
1273
1470
 
1471
+ # Log the interactive session if response logging is enabled
1472
+ if self.response_logger and collected_output is not None and collected_output:
1473
+ try:
1474
+ full_output = ''.join(collected_output)
1475
+ full_input = ''.join(collected_input) if collected_input else "Interactive session"
1476
+ self.response_logger.log_response(
1477
+ request_summary=f"Interactive session: {full_input[:200]}..." if len(full_input) > 200 else f"Interactive session: {full_input}",
1478
+ response_content=full_output,
1479
+ metadata={
1480
+ "mode": "interactive-subprocess",
1481
+ "model": "opus",
1482
+ "exit_code": process.returncode,
1483
+ "session_type": "subprocess"
1484
+ },
1485
+ agent="claude-interactive"
1486
+ )
1487
+ except Exception as e:
1488
+ self.logger.debug(f"Failed to log interactive session: {e}")
1489
+
1274
1490
  if self.project_logger:
1275
1491
  self.project_logger.log_system(
1276
1492
  f"Claude subprocess exited with code {process.returncode}",
@@ -75,7 +75,7 @@ class AgentDeploymentService:
75
75
  Initialize agent deployment service.
76
76
 
77
77
  Args:
78
- templates_dir: Directory containing agent template files
78
+ templates_dir: Directory containing agent JSON files
79
79
  base_agent_path: Path to base_agent.md file
80
80
 
81
81
  METRICS OPPORTUNITY: Track initialization performance:
@@ -105,6 +105,8 @@ class AgentDeploymentService:
105
105
  self.templates_dir = Path(templates_dir)
106
106
  else:
107
107
  # Use centralized paths instead of fragile parent calculations
108
+ # For system agents, still use templates subdirectory
109
+ # For project/user agents, this should be overridden with actual agents dir
108
110
  self.templates_dir = paths.agents_dir / "templates"
109
111
 
110
112
  # Find base agent file
@@ -224,13 +226,20 @@ class AgentDeploymentService:
224
226
  try:
225
227
  # Create agents directory if needed
226
228
  agents_dir.mkdir(parents=True, exist_ok=True)
227
- self.logger.info(f"Building and deploying agents to: {agents_dir}")
229
+ # Determine source tier for logging
230
+ source_tier = "SYSTEM"
231
+ if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
232
+ source_tier = "PROJECT"
233
+ elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
234
+ source_tier = "USER"
235
+
236
+ self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
228
237
 
229
238
  # Note: System instructions are now loaded directly by SimpleClaudeRunner
230
239
 
231
240
  # Check if templates directory exists
232
241
  if not self.templates_dir.exists():
233
- error_msg = f"Templates directory not found: {self.templates_dir}"
242
+ error_msg = f"Agents directory not found: {self.templates_dir}"
234
243
  self.logger.error(error_msg)
235
244
  results["errors"].append(error_msg)
236
245
  return results
@@ -568,20 +577,36 @@ class AgentDeploymentService:
568
577
  # Simplify model name for Claude Code
569
578
  model_map = {
570
579
  'claude-4-sonnet-20250514': 'sonnet',
571
- 'claude-sonnet-4-20250514': 'sonnet',
580
+ 'claude-sonnet-4-20250514': 'sonnet',
581
+ 'claude-opus-4-20250514': 'opus',
572
582
  'claude-3-opus-20240229': 'opus',
573
583
  'claude-3-haiku-20240307': 'haiku',
574
584
  'claude-3.5-sonnet': 'sonnet',
575
585
  'claude-3-sonnet': 'sonnet'
576
586
  }
577
- model = model_map.get(model, model.split('-')[-1] if '-' in model else model)
587
+ # Better fallback: extract the model type (opus/sonnet/haiku) from the string
588
+ if model not in model_map:
589
+ if 'opus' in model.lower():
590
+ model = 'opus'
591
+ elif 'sonnet' in model.lower():
592
+ model = 'sonnet'
593
+ elif 'haiku' in model.lower():
594
+ model = 'haiku'
595
+ else:
596
+ # Last resort: try to extract from hyphenated format
597
+ model = model_map.get(model, model.split('-')[-1] if '-' in model else model)
598
+ else:
599
+ model = model_map[model]
578
600
 
579
601
  # Get response format from template or use base agent default
580
602
  response_format = template_data.get('response', {}).get('format', 'structured')
581
603
 
582
604
  # Convert lists to space-separated strings for Claude Code compatibility
583
605
  tags_str = ' '.join(tags) if isinstance(tags, list) else tags
584
- tools_str = ', '.join(tools) if isinstance(tools, list) else tools
606
+
607
+ # Convert tools list to comma-separated string for Claude Code compatibility
608
+ # IMPORTANT: No spaces after commas - Claude Code requires exact format
609
+ tools_str = ','.join(tools) if isinstance(tools, list) else tools
585
610
 
586
611
  # Build frontmatter with only the fields Claude Code uses
587
612
  frontmatter_lines = [
@@ -596,8 +621,13 @@ class AgentDeploymentService:
596
621
  ]
597
622
 
598
623
  # Add optional fields if present
599
- if template_data.get('color'):
600
- frontmatter_lines.append(f"color: {template_data['color']}")
624
+ # Check for color in metadata section (new format) or root (old format)
625
+ color = (
626
+ template_data.get('metadata', {}).get('color') or
627
+ template_data.get('color')
628
+ )
629
+ if color:
630
+ frontmatter_lines.append(f"color: {color}")
601
631
 
602
632
  frontmatter_lines.append("---")
603
633
  frontmatter_lines.append("")
@@ -1059,7 +1089,7 @@ temperature: {temperature}"""
1059
1089
  agents = []
1060
1090
 
1061
1091
  if not self.templates_dir.exists():
1062
- self.logger.warning(f"Templates directory not found: {self.templates_dir}")
1092
+ self.logger.warning(f"Agents directory not found: {self.templates_dir}")
1063
1093
  return agents
1064
1094
 
1065
1095
  template_files = sorted(self.templates_dir.glob("*.json"))
@@ -31,6 +31,7 @@ from enum import Enum
31
31
 
32
32
  from claude_mpm.core.config_paths import ConfigPaths
33
33
  from claude_mpm.services.memory.cache.simple_cache import SimpleCacheService
34
+ from claude_mpm.agents.frontmatter_validator import FrontmatterValidator, ValidationResult
34
35
 
35
36
  logger = logging.getLogger(__name__)
36
37
 
@@ -130,6 +131,9 @@ class AgentRegistry:
130
131
 
131
132
  self.model_selector = model_selector
132
133
 
134
+ # Initialize frontmatter validator
135
+ self.frontmatter_validator = FrontmatterValidator()
136
+
133
137
  # Registry storage
134
138
  self.registry: Dict[str, AgentMetadata] = {}
135
139
  self.discovery_paths: List[Path] = []
@@ -337,10 +341,27 @@ class AgentRegistry:
337
341
  if len(parts) >= 3:
338
342
  frontmatter_text = parts[1].strip()
339
343
  data = yaml.safe_load(frontmatter_text)
344
+
345
+ # Validate and correct frontmatter
346
+ validation_result = self.frontmatter_validator.validate_and_correct(data)
347
+ if validation_result.corrections:
348
+ logger.info(f"Applied corrections to {file_path.name}:")
349
+ for correction in validation_result.corrections:
350
+ logger.info(f" - {correction}")
351
+
352
+ # Use corrected frontmatter if available
353
+ if validation_result.corrected_frontmatter:
354
+ data = validation_result.corrected_frontmatter
355
+
356
+ if validation_result.errors:
357
+ logger.warning(f"Validation errors in {file_path.name}:")
358
+ for error in validation_result.errors:
359
+ logger.warning(f" - {error}")
360
+
340
361
  description = data.get('description', '')
341
362
  version = data.get('version', '0.0.0')
342
363
  capabilities = data.get('tools', []) # Tools in .md format
343
- metadata = data.get('metadata', {})
364
+ metadata = data
344
365
  else:
345
366
  # No frontmatter, use defaults
346
367
  description = f"{file_path.stem} agent"
@@ -53,6 +53,23 @@ class AgentValidator:
53
53
  - Privilege escalation (via tool combinations)
54
54
  """
55
55
 
56
+ # Model name mappings for normalization to tier names
57
+ MODEL_MAPPINGS = {
58
+ # Sonnet variations
59
+ "claude-3-5-sonnet-20241022": "sonnet",
60
+ "claude-3-5-sonnet-20240620": "sonnet",
61
+ "claude-sonnet-4-20250514": "sonnet",
62
+ "claude-4-sonnet-20250514": "sonnet",
63
+ "claude-3-sonnet-20240229": "sonnet",
64
+ # Opus variations
65
+ "claude-3-opus-20240229": "opus",
66
+ "claude-opus-4-20250514": "opus",
67
+ "claude-4-opus-20250514": "opus",
68
+ # Haiku variations
69
+ "claude-3-haiku-20240307": "haiku",
70
+ "claude-3-5-haiku-20241022": "haiku",
71
+ }
72
+
56
73
  def __init__(self, schema_path: Optional[Path] = None):
57
74
  """Initialize the validator with the agent schema."""
58
75
  if schema_path is None:
@@ -83,6 +100,33 @@ class AgentValidator:
83
100
  logger.error(f"Failed to load schema from {self.schema_path}: {e}")
84
101
  raise
85
102
 
103
+ def _normalize_model(self, model: str) -> str:
104
+ """Normalize model name to standard tier (opus, sonnet, haiku).
105
+
106
+ Args:
107
+ model: Original model name
108
+
109
+ Returns:
110
+ Normalized model tier name
111
+ """
112
+ # Direct mapping check
113
+ if model in self.MODEL_MAPPINGS:
114
+ return self.MODEL_MAPPINGS[model]
115
+
116
+ # Already normalized
117
+ if model in {"opus", "sonnet", "haiku"}:
118
+ return model
119
+
120
+ # Check if model contains tier name
121
+ model_lower = model.lower()
122
+ for tier in {"opus", "sonnet", "haiku"}:
123
+ if tier in model_lower:
124
+ return tier
125
+
126
+ # Default to sonnet if unrecognized
127
+ logger.warning(f"Unrecognized model '{model}', defaulting to 'sonnet'")
128
+ return "sonnet"
129
+
86
130
  def validate_agent(self, agent_data: Dict[str, Any]) -> ValidationResult:
87
131
  """
88
132
  Validate a single agent configuration against the schema.
@@ -101,6 +145,14 @@ class AgentValidator:
101
145
  """
102
146
  result = ValidationResult(is_valid=True)
103
147
 
148
+ # Normalize model name before validation
149
+ if "capabilities" in agent_data and "model" in agent_data["capabilities"]:
150
+ original_model = agent_data["capabilities"]["model"]
151
+ normalized_model = self._normalize_model(original_model)
152
+ if original_model != normalized_model:
153
+ agent_data["capabilities"]["model"] = normalized_model
154
+ result.warnings.append(f"Normalized model from '{original_model}' to '{normalized_model}'")
155
+
104
156
  # Perform JSON schema validation
105
157
  try:
106
158
  validate(instance=agent_data, schema=self.schema)
@@ -242,8 +294,11 @@ class AgentValidator:
242
294
  model = agent_data.get("capabilities", {}).get("model", "")
243
295
  tools = agent_data.get("capabilities", {}).get("tools", [])
244
296
 
297
+ # Normalize model name for comparison
298
+ normalized_model = self._normalize_model(model)
299
+
245
300
  # Haiku models shouldn't use resource-intensive tools
246
- if "haiku" in model.lower():
301
+ if normalized_model == "haiku":
247
302
  intensive_tools = {"docker", "kubectl", "terraform", "aws", "gcloud", "azure"}
248
303
  used_intensive = set(tools) & intensive_tools
249
304
  if used_intensive:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 3.5.1
3
+ Version: 3.5.4
4
4
  Summary: Claude Multi-agent Project Manager - Clean orchestration with ticket management
5
5
  Home-page: https://github.com/bobmatnyc/claude-mpm
6
6
  Author: Claude MPM Team
@@ -92,6 +92,19 @@ claude-mpm run --monitor
92
92
  claude-mpm run --resume
93
93
  ```
94
94
 
95
+ ### Agent Management
96
+
97
+ ```bash
98
+ # View agent hierarchy and precedence
99
+ claude-mpm agents list --by-tier
100
+
101
+ # Inspect specific agent configuration
102
+ claude-mpm agents view engineer
103
+
104
+ # Fix agent configuration issues
105
+ claude-mpm agents fix --all --dry-run
106
+ ```
107
+
95
108
  For detailed usage, see [QUICKSTART.md](QUICKSTART.md)
96
109
 
97
110
  ## Key Capabilities
@@ -108,6 +121,8 @@ The PM agent automatically delegates work to specialized agents:
108
121
  - **Test Integration**: E2E testing and cross-system validation
109
122
  - **Version Control**: Git workflows and release management
110
123
 
124
+ **Three-Tier Agent System**: PROJECT > USER > SYSTEM precedence allows project-specific agent customization while maintaining fallbacks. Use `claude-mpm agents list --by-tier` to see the active agent hierarchy.
125
+
111
126
  ### Session Management
112
127
  - All work is tracked in persistent sessions
113
128
  - Resume any session with `--resume`
@@ -144,14 +159,14 @@ The `--monitor` flag opens a web dashboard showing:
144
159
  - Tool usage and results
145
160
  - Session management UI
146
161
 
147
- See [docs/developer/monitoring.md](docs/developer/monitoring.md) for full monitoring guide.
162
+ See [docs/developer/11-dashboard/README.md](docs/developer/11-dashboard/README.md) for full monitoring guide.
148
163
 
149
164
 
150
165
  ## Documentation
151
166
 
152
167
  - **[Quick Start Guide](QUICKSTART.md)** - Get running in 5 minutes
153
168
  - **[Agent Memory System](docs/MEMORY.md)** - Comprehensive memory documentation
154
- - **[Monitoring Dashboard](docs/developer/monitoring.md)** - Real-time monitoring features
169
+ - **[Monitoring Dashboard](docs/developer/11-dashboard/README.md)** - Real-time monitoring features
155
170
  - **[Project Structure](docs/STRUCTURE.md)** - Codebase organization
156
171
  - **[Deployment Guide](docs/DEPLOY.md)** - Publishing and versioning
157
172
  - **[User Guide](docs/user/)** - Detailed usage documentation