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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +29 -2
- claude_mpm/agents/agent_loader.py +109 -15
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +448 -0
- claude_mpm/agents/templates/data_engineer.json +4 -3
- claude_mpm/agents/templates/documentation.json +4 -3
- claude_mpm/agents/templates/engineer.json +4 -3
- claude_mpm/agents/templates/ops.json +4 -3
- claude_mpm/agents/templates/pm.json +5 -4
- claude_mpm/agents/templates/qa.json +4 -3
- claude_mpm/agents/templates/research.json +8 -7
- claude_mpm/agents/templates/security.json +4 -3
- claude_mpm/agents/templates/test_integration.json +4 -3
- claude_mpm/agents/templates/version_control.json +4 -3
- claude_mpm/cli/__main__.py +24 -0
- claude_mpm/cli/commands/agents.py +354 -6
- claude_mpm/cli/parser.py +36 -0
- claude_mpm/constants.py +2 -0
- claude_mpm/core/agent_registry.py +4 -1
- claude_mpm/core/claude_runner.py +224 -8
- claude_mpm/services/agents/deployment/agent_deployment.py +39 -9
- claude_mpm/services/agents/registry/agent_registry.py +22 -1
- claude_mpm/validation/agent_validator.py +56 -1
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/METADATA +18 -3
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/RECORD +30 -28
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/WHEEL +0 -0
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.5.1.dist-info → claude_mpm-3.5.4.dist-info}/top_level.txt +0 -0
    
        claude_mpm/core/claude_runner.py
    CHANGED
    
    | @@ -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  | 
| 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 | 
| 232 | 
            -
                    if they don't exist or are outdated. This  | 
| 233 | 
            -
                     | 
| 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  | 
| 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 | 
| 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  | 
| 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 | 
            -
                         | 
| 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" | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 600 | 
            -
             | 
| 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" | 
| 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 | 
| 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" | 
| 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. | 
| 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/ | 
| 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/ | 
| 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
         |