claude-mpm 3.5.6__py3-none-any.whl → 3.6.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
- claude_mpm/agents/BASE_PM.md +273 -0
- claude_mpm/agents/INSTRUCTIONS.md +114 -103
- claude_mpm/agents/agent_loader.py +36 -1
- claude_mpm/agents/async_agent_loader.py +421 -0
- claude_mpm/agents/templates/code_analyzer.json +81 -0
- claude_mpm/agents/templates/data_engineer.json +18 -3
- claude_mpm/agents/templates/documentation.json +18 -3
- claude_mpm/agents/templates/engineer.json +19 -4
- claude_mpm/agents/templates/ops.json +18 -3
- claude_mpm/agents/templates/qa.json +20 -4
- claude_mpm/agents/templates/research.json +20 -4
- claude_mpm/agents/templates/security.json +18 -3
- claude_mpm/agents/templates/version_control.json +16 -3
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +212 -3
- claude_mpm/cli/commands/aggregate.py +462 -0
- claude_mpm/cli/commands/config.py +277 -0
- claude_mpm/cli/commands/run.py +224 -36
- claude_mpm/cli/parser.py +176 -1
- claude_mpm/constants.py +19 -0
- claude_mpm/core/claude_runner.py +320 -44
- claude_mpm/core/config.py +161 -4
- claude_mpm/core/framework_loader.py +81 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
- claude_mpm/init.py +40 -5
- claude_mpm/models/agent_session.py +511 -0
- claude_mpm/scripts/__init__.py +15 -0
- claude_mpm/scripts/start_activity_logging.py +86 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
- claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
- claude_mpm/services/event_aggregator.py +547 -0
- claude_mpm/utils/agent_dependency_loader.py +655 -0
- claude_mpm/utils/console.py +11 -0
- claude_mpm/utils/dependency_cache.py +376 -0
- claude_mpm/utils/dependency_strategies.py +343 -0
- claude_mpm/utils/environment_context.py +310 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +47 -3
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +45 -31
- claude_mpm/agents/templates/pm.json +0 -122
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
    
        claude_mpm/core/claude_runner.py
    CHANGED
    
    | @@ -471,9 +471,25 @@ class ClaudeRunner: | |
| 471 471 | 
             
                        return False
         | 
| 472 472 |  | 
| 473 473 | 
             
                def run_interactive(self, initial_context: Optional[str] = None):
         | 
| 474 | 
            -
                    """Run Claude in interactive mode. | 
| 475 | 
            -
                     | 
| 476 | 
            -
                     | 
| 474 | 
            +
                    """Run Claude in interactive mode.
         | 
| 475 | 
            +
                    
         | 
| 476 | 
            +
                    WHY: This method manages the interactive Claude session with optional response
         | 
| 477 | 
            +
                    logging through the hook system. Response logging works seamlessly with both 
         | 
| 478 | 
            +
                    exec and subprocess launch methods via Claude Code's built-in hook infrastructure.
         | 
| 479 | 
            +
                    
         | 
| 480 | 
            +
                    DESIGN DECISION: The hook system captures Claude events (UserPromptSubmit,
         | 
| 481 | 
            +
                    PreToolUse, PostToolUse, Task delegations) directly from Claude Code, enabling 
         | 
| 482 | 
            +
                    response logging without process control overhead. This architecture provides:
         | 
| 483 | 
            +
                    - Better performance (no I/O stream interception needed)
         | 
| 484 | 
            +
                    - Full compatibility with exec mode (preserves default behavior)
         | 
| 485 | 
            +
                    - Clean separation of concerns (hooks handle logging independently)
         | 
| 486 | 
            +
                    - Comprehensive event capture (including agent delegations)
         | 
| 487 | 
            +
                    
         | 
| 488 | 
            +
                    Args:
         | 
| 489 | 
            +
                        initial_context: Optional initial context to pass to Claude
         | 
| 490 | 
            +
                    """
         | 
| 491 | 
            +
                    # Use the launch method as specified
         | 
| 492 | 
            +
                    effective_launch_method = self.launch_method
         | 
| 477 493 |  | 
| 478 494 | 
             
                    # Connect to Socket.IO server if enabled
         | 
| 479 495 | 
             
                    if self.enable_websocket:
         | 
| @@ -491,7 +507,7 @@ class ClaudeRunner: | |
| 491 507 | 
             
                            # Notify session start
         | 
| 492 508 | 
             
                            self.websocket_server.session_started(
         | 
| 493 509 | 
             
                                session_id=session_id,
         | 
| 494 | 
            -
                                launch_method= | 
| 510 | 
            +
                                launch_method=effective_launch_method,
         | 
| 495 511 | 
             
                                working_dir=working_dir
         | 
| 496 512 | 
             
                            )
         | 
| 497 513 | 
             
                        except ImportError as e:
         | 
| @@ -580,14 +596,14 @@ class ClaudeRunner: | |
| 580 596 |  | 
| 581 597 | 
             
                        if self.project_logger:
         | 
| 582 598 | 
             
                            self.project_logger.log_system(
         | 
| 583 | 
            -
                                f"Launching Claude interactive mode with { | 
| 599 | 
            +
                                f"Launching Claude interactive mode with {effective_launch_method}",
         | 
| 584 600 | 
             
                                level="INFO",
         | 
| 585 601 | 
             
                                component="session"
         | 
| 586 602 | 
             
                            )
         | 
| 587 603 | 
             
                            self._log_session_event({
         | 
| 588 604 | 
             
                                "event": "launching_claude_interactive",
         | 
| 589 605 | 
             
                                "command": " ".join(cmd),
         | 
| 590 | 
            -
                                "method":  | 
| 606 | 
            +
                                "method": effective_launch_method
         | 
| 591 607 | 
             
                            })
         | 
| 592 608 |  | 
| 593 609 | 
             
                        # Notify WebSocket clients
         | 
| @@ -598,7 +614,7 @@ class ClaudeRunner: | |
| 598 614 | 
             
                            )
         | 
| 599 615 |  | 
| 600 616 | 
             
                        # Launch using selected method
         | 
| 601 | 
            -
                        if  | 
| 617 | 
            +
                        if effective_launch_method == "subprocess":
         | 
| 602 618 | 
             
                            self._launch_subprocess_interactive(cmd, clean_env)
         | 
| 603 619 | 
             
                        else:
         | 
| 604 620 | 
             
                            # Default to exec for backward compatibility
         | 
| @@ -1142,7 +1158,19 @@ class ClaudeRunner: | |
| 1142 1158 | 
             
                            from claude_mpm.services.framework_claude_md_generator.content_assembler import ContentAssembler
         | 
| 1143 1159 | 
             
                            assembler = ContentAssembler()
         | 
| 1144 1160 | 
             
                            processed_instructions = assembler.apply_template_variables(raw_instructions)
         | 
| 1145 | 
            -
                             | 
| 1161 | 
            +
                            
         | 
| 1162 | 
            +
                            # Append BASE_PM.md framework requirements with dynamic content
         | 
| 1163 | 
            +
                            base_pm_path = Path(__file__).parent.parent / "agents" / "BASE_PM.md"
         | 
| 1164 | 
            +
                            if base_pm_path.exists():
         | 
| 1165 | 
            +
                                base_pm_content = base_pm_path.read_text()
         | 
| 1166 | 
            +
                                
         | 
| 1167 | 
            +
                                # Process BASE_PM.md with dynamic content injection
         | 
| 1168 | 
            +
                                base_pm_content = self._process_base_pm_content(base_pm_content)
         | 
| 1169 | 
            +
                                
         | 
| 1170 | 
            +
                                processed_instructions += f"\n\n{base_pm_content}"
         | 
| 1171 | 
            +
                                self.logger.info(f"Appended BASE_PM.md with dynamic capabilities from deployed agents")
         | 
| 1172 | 
            +
                            
         | 
| 1173 | 
            +
                            self.logger.info(f"Loaded and processed {instructions_source} PM instructions")
         | 
| 1146 1174 | 
             
                            return processed_instructions
         | 
| 1147 1175 | 
             
                        except ImportError:
         | 
| 1148 1176 | 
             
                            self.logger.warning("ContentAssembler not available, using raw instructions")
         | 
| @@ -1157,6 +1185,274 @@ class ClaudeRunner: | |
| 1157 1185 | 
             
                        self.logger.error(f"Failed to load system instructions: {e}")
         | 
| 1158 1186 | 
             
                        return None
         | 
| 1159 1187 |  | 
| 1188 | 
            +
                def _process_base_pm_content(self, base_pm_content: str) -> str:
         | 
| 1189 | 
            +
                    """Process BASE_PM.md content with dynamic injections.
         | 
| 1190 | 
            +
                    
         | 
| 1191 | 
            +
                    This method replaces template variables in BASE_PM.md with:
         | 
| 1192 | 
            +
                    - {{agent-capabilities}}: List of deployed agents from .claude/agents/
         | 
| 1193 | 
            +
                    - {{current-date}}: Today's date for temporal context
         | 
| 1194 | 
            +
                    """
         | 
| 1195 | 
            +
                    from datetime import datetime
         | 
| 1196 | 
            +
                    
         | 
| 1197 | 
            +
                    # Replace {{current-date}} with actual date
         | 
| 1198 | 
            +
                    current_date = datetime.now().strftime('%Y-%m-%d')
         | 
| 1199 | 
            +
                    base_pm_content = base_pm_content.replace('{{current-date}}', current_date)
         | 
| 1200 | 
            +
                    
         | 
| 1201 | 
            +
                    # Replace {{agent-capabilities}} with deployed agents
         | 
| 1202 | 
            +
                    if '{{agent-capabilities}}' in base_pm_content:
         | 
| 1203 | 
            +
                        capabilities_section = self._generate_deployed_agent_capabilities()
         | 
| 1204 | 
            +
                        base_pm_content = base_pm_content.replace('{{agent-capabilities}}', capabilities_section)
         | 
| 1205 | 
            +
                    
         | 
| 1206 | 
            +
                    return base_pm_content
         | 
| 1207 | 
            +
                
         | 
| 1208 | 
            +
                def _generate_deployed_agent_capabilities(self) -> str:
         | 
| 1209 | 
            +
                    """Generate agent capabilities from deployed agents following Claude Code's hierarchy.
         | 
| 1210 | 
            +
                    
         | 
| 1211 | 
            +
                    Follows the agent precedence order:
         | 
| 1212 | 
            +
                    1. Project agents (.claude/agents/) - highest priority
         | 
| 1213 | 
            +
                    2. User agents (~/.config/claude/agents/) - middle priority  
         | 
| 1214 | 
            +
                    3. System agents (claude-desktop installation) - lowest priority
         | 
| 1215 | 
            +
                    
         | 
| 1216 | 
            +
                    Project agents override user/system agents with the same ID.
         | 
| 1217 | 
            +
                    """
         | 
| 1218 | 
            +
                    try:
         | 
| 1219 | 
            +
                        # Track discovered agents by ID to handle overrides
         | 
| 1220 | 
            +
                        discovered_agents = {}
         | 
| 1221 | 
            +
                        
         | 
| 1222 | 
            +
                        # 1. First read system agents (lowest priority)
         | 
| 1223 | 
            +
                        system_agents_dirs = [
         | 
| 1224 | 
            +
                            Path.home() / "Library" / "Application Support" / "Claude" / "agents",  # macOS
         | 
| 1225 | 
            +
                            Path.home() / ".config" / "claude" / "agents",  # Linux
         | 
| 1226 | 
            +
                            Path.home() / "AppData" / "Roaming" / "Claude" / "agents",  # Windows
         | 
| 1227 | 
            +
                        ]
         | 
| 1228 | 
            +
                        
         | 
| 1229 | 
            +
                        for system_dir in system_agents_dirs:
         | 
| 1230 | 
            +
                            if system_dir.exists():
         | 
| 1231 | 
            +
                                self._discover_agents_from_dir(system_dir, discovered_agents, "system")
         | 
| 1232 | 
            +
                                break
         | 
| 1233 | 
            +
                        
         | 
| 1234 | 
            +
                        # 2. Then read user agents (middle priority, overrides system)
         | 
| 1235 | 
            +
                        user_agents_dir = Path.home() / ".config" / "claude" / "agents"
         | 
| 1236 | 
            +
                        if user_agents_dir.exists():
         | 
| 1237 | 
            +
                            self._discover_agents_from_dir(user_agents_dir, discovered_agents, "user")
         | 
| 1238 | 
            +
                        
         | 
| 1239 | 
            +
                        # 3. Finally read project agents (highest priority, overrides all)
         | 
| 1240 | 
            +
                        project_agents_dir = Path.cwd() / ".claude" / "agents"
         | 
| 1241 | 
            +
                        if project_agents_dir.exists():
         | 
| 1242 | 
            +
                            self._discover_agents_from_dir(project_agents_dir, discovered_agents, "project")
         | 
| 1243 | 
            +
                        
         | 
| 1244 | 
            +
                        if not discovered_agents:
         | 
| 1245 | 
            +
                            self.logger.warning("No agents found in any tier")
         | 
| 1246 | 
            +
                            return self._get_fallback_capabilities()
         | 
| 1247 | 
            +
                        
         | 
| 1248 | 
            +
                        # Build capabilities section from discovered agents
         | 
| 1249 | 
            +
                        section = "\n## Available Agent Capabilities\n\n"
         | 
| 1250 | 
            +
                        section += "You have the following specialized agents available for delegation:\n\n"
         | 
| 1251 | 
            +
                        
         | 
| 1252 | 
            +
                        # Group agents by category
         | 
| 1253 | 
            +
                        agents_by_category = {}
         | 
| 1254 | 
            +
                        for agent_id, agent_info in discovered_agents.items():
         | 
| 1255 | 
            +
                            category = agent_info['category']
         | 
| 1256 | 
            +
                            if category not in agents_by_category:
         | 
| 1257 | 
            +
                                agents_by_category[category] = []
         | 
| 1258 | 
            +
                            agents_by_category[category].append(agent_info)
         | 
| 1259 | 
            +
                        
         | 
| 1260 | 
            +
                        # Output agents by category
         | 
| 1261 | 
            +
                        for category in sorted(agents_by_category.keys()):
         | 
| 1262 | 
            +
                            section += f"\n### {category} Agents\n"
         | 
| 1263 | 
            +
                            for agent in sorted(agents_by_category[category], key=lambda x: x['name']):
         | 
| 1264 | 
            +
                                tier_indicator = f" [{agent['tier']}]" if agent['tier'] != 'project' else ""
         | 
| 1265 | 
            +
                                section += f"- **{agent['name']}** (`{agent['id']}`{tier_indicator}): {agent['description']}\n"
         | 
| 1266 | 
            +
                        
         | 
| 1267 | 
            +
                        # Add summary
         | 
| 1268 | 
            +
                        section += f"\n**Total Available Agents**: {len(discovered_agents)}\n"
         | 
| 1269 | 
            +
                        
         | 
| 1270 | 
            +
                        # Show tier distribution
         | 
| 1271 | 
            +
                        tier_counts = {}
         | 
| 1272 | 
            +
                        for agent in discovered_agents.values():
         | 
| 1273 | 
            +
                            tier = agent['tier']
         | 
| 1274 | 
            +
                            tier_counts[tier] = tier_counts.get(tier, 0) + 1
         | 
| 1275 | 
            +
                        
         | 
| 1276 | 
            +
                        if len(tier_counts) > 1:
         | 
| 1277 | 
            +
                            section += f"**Agent Sources**: "
         | 
| 1278 | 
            +
                            tier_summary = []
         | 
| 1279 | 
            +
                            for tier in ['project', 'user', 'system']:
         | 
| 1280 | 
            +
                                if tier in tier_counts:
         | 
| 1281 | 
            +
                                    tier_summary.append(f"{tier_counts[tier]} {tier}")
         | 
| 1282 | 
            +
                            section += ", ".join(tier_summary) + "\n"
         | 
| 1283 | 
            +
                        
         | 
| 1284 | 
            +
                        section += "Use the agent ID in parentheses when delegating tasks via the Task tool.\n"
         | 
| 1285 | 
            +
                        
         | 
| 1286 | 
            +
                        self.logger.info(f"Generated capabilities for {len(discovered_agents)} agents " +
         | 
| 1287 | 
            +
                                       f"(project: {tier_counts.get('project', 0)}, " +
         | 
| 1288 | 
            +
                                       f"user: {tier_counts.get('user', 0)}, " +
         | 
| 1289 | 
            +
                                       f"system: {tier_counts.get('system', 0)})")
         | 
| 1290 | 
            +
                        return section
         | 
| 1291 | 
            +
                        
         | 
| 1292 | 
            +
                    except Exception as e:
         | 
| 1293 | 
            +
                        self.logger.error(f"Failed to generate deployed agent capabilities: {e}")
         | 
| 1294 | 
            +
                        return self._get_fallback_capabilities()
         | 
| 1295 | 
            +
                
         | 
| 1296 | 
            +
                def _discover_agents_from_dir(self, agents_dir: Path, discovered_agents: dict, tier: str):
         | 
| 1297 | 
            +
                    """Discover agents from a specific directory and add/override in discovered_agents.
         | 
| 1298 | 
            +
                    
         | 
| 1299 | 
            +
                    Args:
         | 
| 1300 | 
            +
                        agents_dir: Directory to search for agent .md files
         | 
| 1301 | 
            +
                        discovered_agents: Dictionary to update with discovered agents
         | 
| 1302 | 
            +
                        tier: The tier this directory represents (system/user/project)
         | 
| 1303 | 
            +
                    """
         | 
| 1304 | 
            +
                    if not agents_dir.exists():
         | 
| 1305 | 
            +
                        return
         | 
| 1306 | 
            +
                    
         | 
| 1307 | 
            +
                    agent_files = list(agents_dir.glob("*.md"))
         | 
| 1308 | 
            +
                    for agent_file in sorted(agent_files):
         | 
| 1309 | 
            +
                        agent_id = agent_file.stem
         | 
| 1310 | 
            +
                        
         | 
| 1311 | 
            +
                        # Skip pm.md if it exists (PM is not a deployable agent)
         | 
| 1312 | 
            +
                        if agent_id.lower() == 'pm':
         | 
| 1313 | 
            +
                            continue
         | 
| 1314 | 
            +
                        
         | 
| 1315 | 
            +
                        # Read agent content and extract metadata
         | 
| 1316 | 
            +
                        try:
         | 
| 1317 | 
            +
                            content = agent_file.read_text()
         | 
| 1318 | 
            +
                            import re
         | 
| 1319 | 
            +
                            
         | 
| 1320 | 
            +
                            # Check for YAML frontmatter
         | 
| 1321 | 
            +
                            name = agent_id.replace('_', ' ').title()
         | 
| 1322 | 
            +
                            desc = "Specialized agent for delegation"
         | 
| 1323 | 
            +
                            
         | 
| 1324 | 
            +
                            if content.startswith('---'):
         | 
| 1325 | 
            +
                                # Parse YAML frontmatter
         | 
| 1326 | 
            +
                                frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
         | 
| 1327 | 
            +
                                if frontmatter_match:
         | 
| 1328 | 
            +
                                    frontmatter = frontmatter_match.group(1)
         | 
| 1329 | 
            +
                                    # Extract name from frontmatter
         | 
| 1330 | 
            +
                                    name_fm_match = re.search(r'^name:\s*(.+)$', frontmatter, re.MULTILINE)
         | 
| 1331 | 
            +
                                    if name_fm_match:
         | 
| 1332 | 
            +
                                        name_value = name_fm_match.group(1).strip()
         | 
| 1333 | 
            +
                                        # Format the name nicely
         | 
| 1334 | 
            +
                                        name = name_value.replace('_', ' ').title()
         | 
| 1335 | 
            +
                                    
         | 
| 1336 | 
            +
                                    # Extract description from frontmatter
         | 
| 1337 | 
            +
                                    desc_fm_match = re.search(r'^description:\s*(.+)$', frontmatter, re.MULTILINE)
         | 
| 1338 | 
            +
                                    if desc_fm_match:
         | 
| 1339 | 
            +
                                        desc = desc_fm_match.group(1).strip()
         | 
| 1340 | 
            +
                            else:
         | 
| 1341 | 
            +
                                # No frontmatter, extract from content
         | 
| 1342 | 
            +
                                name_match = re.search(r'^#\s+(.+?)(?:\s+Agent)?$', content, re.MULTILINE)
         | 
| 1343 | 
            +
                                if name_match:
         | 
| 1344 | 
            +
                                    name = name_match.group(1)
         | 
| 1345 | 
            +
                                
         | 
| 1346 | 
            +
                                # Get first non-heading line after the title
         | 
| 1347 | 
            +
                                lines = content.split('\n')
         | 
| 1348 | 
            +
                                for i, line in enumerate(lines):
         | 
| 1349 | 
            +
                                    if line.startswith('#'):
         | 
| 1350 | 
            +
                                        # Found title, look for description after it
         | 
| 1351 | 
            +
                                        for desc_line in lines[i+1:]:
         | 
| 1352 | 
            +
                                            desc_line = desc_line.strip()
         | 
| 1353 | 
            +
                                            if desc_line and not desc_line.startswith('#'):
         | 
| 1354 | 
            +
                                                desc = desc_line
         | 
| 1355 | 
            +
                                                break
         | 
| 1356 | 
            +
                                        break
         | 
| 1357 | 
            +
                            
         | 
| 1358 | 
            +
                            # Categorize based on agent name/type
         | 
| 1359 | 
            +
                            category = self._categorize_agent(agent_id, content)
         | 
| 1360 | 
            +
                            
         | 
| 1361 | 
            +
                            # Add or override agent in discovered_agents
         | 
| 1362 | 
            +
                            discovered_agents[agent_id] = {
         | 
| 1363 | 
            +
                                'id': agent_id,
         | 
| 1364 | 
            +
                                'name': name,
         | 
| 1365 | 
            +
                                'description': desc[:150] + '...' if len(desc) > 150 else desc,
         | 
| 1366 | 
            +
                                'category': category,
         | 
| 1367 | 
            +
                                'tier': tier,
         | 
| 1368 | 
            +
                                'path': str(agent_file)
         | 
| 1369 | 
            +
                            }
         | 
| 1370 | 
            +
                            
         | 
| 1371 | 
            +
                            self.logger.debug(f"Discovered {tier} agent: {agent_id} from {agent_file}")
         | 
| 1372 | 
            +
                            
         | 
| 1373 | 
            +
                        except Exception as e:
         | 
| 1374 | 
            +
                            self.logger.debug(f"Could not parse agent {agent_file}: {e}")
         | 
| 1375 | 
            +
                            continue
         | 
| 1376 | 
            +
                def _categorize_agent(self, agent_id: str, content: str) -> str:
         | 
| 1377 | 
            +
                    """Categorize an agent based on its ID and content."""
         | 
| 1378 | 
            +
                    agent_id_lower = agent_id.lower()
         | 
| 1379 | 
            +
                    content_lower = content.lower()
         | 
| 1380 | 
            +
                    
         | 
| 1381 | 
            +
                    if 'engineer' in agent_id_lower or 'engineering' in content_lower:
         | 
| 1382 | 
            +
                        return "Engineering"
         | 
| 1383 | 
            +
                    elif 'research' in agent_id_lower or 'analysis' in content_lower or 'analyzer' in agent_id_lower:
         | 
| 1384 | 
            +
                        return "Research"
         | 
| 1385 | 
            +
                    elif 'qa' in agent_id_lower or 'quality' in content_lower or 'test' in agent_id_lower:
         | 
| 1386 | 
            +
                        return "Quality"
         | 
| 1387 | 
            +
                    elif 'security' in agent_id_lower or 'security' in content_lower:
         | 
| 1388 | 
            +
                        return "Security"
         | 
| 1389 | 
            +
                    elif 'doc' in agent_id_lower or 'documentation' in content_lower:
         | 
| 1390 | 
            +
                        return "Documentation"
         | 
| 1391 | 
            +
                    elif 'data' in agent_id_lower:
         | 
| 1392 | 
            +
                        return "Data"
         | 
| 1393 | 
            +
                    elif 'ops' in agent_id_lower or 'deploy' in agent_id_lower or 'operations' in content_lower:
         | 
| 1394 | 
            +
                        return "Operations"
         | 
| 1395 | 
            +
                    elif 'version' in agent_id_lower or 'git' in content_lower:
         | 
| 1396 | 
            +
                        return "Version Control"
         | 
| 1397 | 
            +
                    else:
         | 
| 1398 | 
            +
                        return "General"
         | 
| 1399 | 
            +
                
         | 
| 1400 | 
            +
                def _get_fallback_capabilities(self) -> str:
         | 
| 1401 | 
            +
                    """Return fallback agent capabilities when deployed agents can't be read."""
         | 
| 1402 | 
            +
                    return """
         | 
| 1403 | 
            +
            ## Available Agent Capabilities
         | 
| 1404 | 
            +
             | 
| 1405 | 
            +
            You have the following specialized agents available for delegation:
         | 
| 1406 | 
            +
             | 
| 1407 | 
            +
            - **Engineer Agent**: Code implementation and development
         | 
| 1408 | 
            +
            - **Research Agent**: Investigation and analysis
         | 
| 1409 | 
            +
            - **QA Agent**: Testing and quality assurance
         | 
| 1410 | 
            +
            - **Documentation Agent**: Documentation creation and maintenance
         | 
| 1411 | 
            +
            - **Security Agent**: Security analysis and protection
         | 
| 1412 | 
            +
            - **Data Engineer Agent**: Data management and pipelines
         | 
| 1413 | 
            +
            - **Ops Agent**: Deployment and operations
         | 
| 1414 | 
            +
            - **Version Control Agent**: Git operations and version management
         | 
| 1415 | 
            +
             | 
| 1416 | 
            +
            Use these agents to delegate specialized work via the Task tool.
         | 
| 1417 | 
            +
            """
         | 
| 1418 | 
            +
                
         | 
| 1419 | 
            +
                def _generate_agent_capabilities_section(self, agents: dict) -> str:
         | 
| 1420 | 
            +
                    """Generate dynamic agent capabilities section from available agents."""
         | 
| 1421 | 
            +
                    if not agents:
         | 
| 1422 | 
            +
                        return ""
         | 
| 1423 | 
            +
                    
         | 
| 1424 | 
            +
                    # Build capabilities section
         | 
| 1425 | 
            +
                    section = "\n\n## Available Agent Capabilities\n\n"
         | 
| 1426 | 
            +
                    section += "You have the following specialized agents available for delegation:\n\n"
         | 
| 1427 | 
            +
                    
         | 
| 1428 | 
            +
                    # Group agents by category
         | 
| 1429 | 
            +
                    categories = {}
         | 
| 1430 | 
            +
                    for agent_id, info in agents.items():
         | 
| 1431 | 
            +
                        category = info.get('category', 'general')
         | 
| 1432 | 
            +
                        if category not in categories:
         | 
| 1433 | 
            +
                            categories[category] = []
         | 
| 1434 | 
            +
                        categories[category].append((agent_id, info))
         | 
| 1435 | 
            +
                    
         | 
| 1436 | 
            +
                    # List agents by category
         | 
| 1437 | 
            +
                    for category in sorted(categories.keys()):
         | 
| 1438 | 
            +
                        section += f"\n### {category.title()} Agents\n"
         | 
| 1439 | 
            +
                        for agent_id, info in sorted(categories[category]):
         | 
| 1440 | 
            +
                            name = info.get('name', agent_id)
         | 
| 1441 | 
            +
                            desc = info.get('description', 'Specialized agent')
         | 
| 1442 | 
            +
                            tools = info.get('tools', [])
         | 
| 1443 | 
            +
                            section += f"- **{name}** (`{agent_id}`): {desc}\n"
         | 
| 1444 | 
            +
                            if tools:
         | 
| 1445 | 
            +
                                section += f"  - Tools: {', '.join(tools[:5])}"
         | 
| 1446 | 
            +
                                if len(tools) > 5:
         | 
| 1447 | 
            +
                                    section += f" (+{len(tools)-5} more)"
         | 
| 1448 | 
            +
                                section += "\n"
         | 
| 1449 | 
            +
                    
         | 
| 1450 | 
            +
                    # Add summary
         | 
| 1451 | 
            +
                    section += f"\n**Total Available Agents**: {len(agents)}\n"
         | 
| 1452 | 
            +
                    section += "Use the agent ID in parentheses when delegating tasks via the Task tool.\n"
         | 
| 1453 | 
            +
                    
         | 
| 1454 | 
            +
                    return section
         | 
| 1455 | 
            +
                
         | 
| 1160 1456 | 
             
                def _create_system_prompt(self) -> str:
         | 
| 1161 1457 | 
             
                    """Create the complete system prompt including instructions."""
         | 
| 1162 1458 | 
             
                    if self.system_instructions:
         | 
| @@ -1418,16 +1714,27 @@ class ClaudeRunner: | |
| 1418 1714 | 
             
                        # Don't fail the entire initialization - memory system is optional
         | 
| 1419 1715 |  | 
| 1420 1716 | 
             
                def _launch_subprocess_interactive(self, cmd: list, env: dict):
         | 
| 1421 | 
            -
                    """Launch Claude as a subprocess with PTY for interactive mode. | 
| 1717 | 
            +
                    """Launch Claude as a subprocess with PTY for interactive mode.
         | 
| 1718 | 
            +
                    
         | 
| 1719 | 
            +
                    WHY: This method launches Claude as a subprocess when explicitly requested
         | 
| 1720 | 
            +
                    (via --launch-method subprocess). Subprocess mode maintains the parent process,
         | 
| 1721 | 
            +
                    which can be useful for:
         | 
| 1722 | 
            +
                    1. Maintaining WebSocket connections and monitoring
         | 
| 1723 | 
            +
                    2. Providing proper cleanup and error handling
         | 
| 1724 | 
            +
                    3. Debugging and development scenarios
         | 
| 1725 | 
            +
                    
         | 
| 1726 | 
            +
                    DESIGN DECISION: We use PTY (pseudo-terminal) to maintain full interactive
         | 
| 1727 | 
            +
                    capabilities. Response logging is handled through the hook system, not I/O
         | 
| 1728 | 
            +
                    interception, for better performance and compatibility.
         | 
| 1729 | 
            +
                    """
         | 
| 1422 1730 | 
             
                    import pty
         | 
| 1423 1731 | 
             
                    import select
         | 
| 1424 1732 | 
             
                    import termios
         | 
| 1425 1733 | 
             
                    import tty
         | 
| 1426 1734 | 
             
                    import signal
         | 
| 1427 1735 |  | 
| 1428 | 
            -
                    #  | 
| 1429 | 
            -
                     | 
| 1430 | 
            -
                    collected_input = [] if self.response_logger else None
         | 
| 1736 | 
            +
                    # Note: Response logging is handled through the hook system,
         | 
| 1737 | 
            +
                    # not through I/O interception (better performance)
         | 
| 1431 1738 |  | 
| 1432 1739 | 
             
                    # Save original terminal settings
         | 
| 1433 1740 | 
             
                    original_tty = None
         | 
| @@ -1491,13 +1798,6 @@ class ClaudeRunner: | |
| 1491 1798 | 
             
                                    data = os.read(master_fd, 4096)
         | 
| 1492 1799 | 
             
                                    if data:
         | 
| 1493 1800 | 
             
                                        os.write(sys.stdout.fileno(), data)
         | 
| 1494 | 
            -
                                        # Collect output for response logging
         | 
| 1495 | 
            -
                                        if collected_output is not None:
         | 
| 1496 | 
            -
                                            try:
         | 
| 1497 | 
            -
                                                output_text = data.decode('utf-8', errors='replace')
         | 
| 1498 | 
            -
                                                collected_output.append(output_text)
         | 
| 1499 | 
            -
                                            except Exception:
         | 
| 1500 | 
            -
                                                pass
         | 
| 1501 1801 | 
             
                                        # Broadcast output to WebSocket clients
         | 
| 1502 1802 | 
             
                                        if self.websocket_server:
         | 
| 1503 1803 | 
             
                                            try:
         | 
| @@ -1516,37 +1816,13 @@ class ClaudeRunner: | |
| 1516 1816 | 
             
                                    data = os.read(sys.stdin.fileno(), 4096)
         | 
| 1517 1817 | 
             
                                    if data:
         | 
| 1518 1818 | 
             
                                        os.write(master_fd, data)
         | 
| 1519 | 
            -
                                        # Collect input for response logging
         | 
| 1520 | 
            -
                                        if collected_input is not None:
         | 
| 1521 | 
            -
                                            try:
         | 
| 1522 | 
            -
                                                input_text = data.decode('utf-8', errors='replace')
         | 
| 1523 | 
            -
                                                collected_input.append(input_text)
         | 
| 1524 | 
            -
                                            except Exception:
         | 
| 1525 | 
            -
                                                pass
         | 
| 1526 1819 | 
             
                                except OSError:
         | 
| 1527 1820 | 
             
                                    break
         | 
| 1528 1821 |  | 
| 1529 1822 | 
             
                        # Wait for process to complete
         | 
| 1530 1823 | 
             
                        process.wait()
         | 
| 1531 1824 |  | 
| 1532 | 
            -
                        #  | 
| 1533 | 
            -
                        if self.response_logger and collected_output is not None and collected_output:
         | 
| 1534 | 
            -
                            try:
         | 
| 1535 | 
            -
                                full_output = ''.join(collected_output)
         | 
| 1536 | 
            -
                                full_input = ''.join(collected_input) if collected_input else "Interactive session"
         | 
| 1537 | 
            -
                                self.response_logger.log_response(
         | 
| 1538 | 
            -
                                    request_summary=f"Interactive session: {full_input[:200]}..." if len(full_input) > 200 else f"Interactive session: {full_input}",
         | 
| 1539 | 
            -
                                    response_content=full_output,
         | 
| 1540 | 
            -
                                    metadata={
         | 
| 1541 | 
            -
                                        "mode": "interactive-subprocess",
         | 
| 1542 | 
            -
                                        "model": "opus",
         | 
| 1543 | 
            -
                                        "exit_code": process.returncode,
         | 
| 1544 | 
            -
                                        "session_type": "subprocess"
         | 
| 1545 | 
            -
                                    },
         | 
| 1546 | 
            -
                                    agent="claude-interactive"
         | 
| 1547 | 
            -
                                )
         | 
| 1548 | 
            -
                            except Exception as e:
         | 
| 1549 | 
            -
                                self.logger.debug(f"Failed to log interactive session: {e}")
         | 
| 1825 | 
            +
                        # Note: Response logging is handled through the hook system
         | 
| 1550 1826 |  | 
| 1551 1827 | 
             
                        if self.project_logger:
         | 
| 1552 1828 | 
             
                            self.project_logger.log_system(
         | 
    
        claude_mpm/core/config.py
    CHANGED
    
    | @@ -7,8 +7,10 @@ and default values with proper validation and type conversion. | |
| 7 7 |  | 
| 8 8 | 
             
            import os
         | 
| 9 9 | 
             
            from pathlib import Path
         | 
| 10 | 
            -
            from typing import Any, Dict, Optional, Union
         | 
| 10 | 
            +
            from typing import Any, Dict, Optional, Union, List, Tuple
         | 
| 11 11 | 
             
            import logging
         | 
| 12 | 
            +
            import yaml
         | 
| 13 | 
            +
            import json
         | 
| 12 14 |  | 
| 13 15 | 
             
            from ..utils.config_manager import ConfigurationManager
         | 
| 14 16 | 
             
            from .config_paths import ConfigPaths
         | 
| @@ -49,14 +51,25 @@ class Config: | |
| 49 51 | 
             
                    if config:
         | 
| 50 52 | 
             
                        self._config.update(config)
         | 
| 51 53 |  | 
| 54 | 
            +
                    # Track where configuration was loaded from
         | 
| 55 | 
            +
                    self._loaded_from = None
         | 
| 56 | 
            +
                    
         | 
| 52 57 | 
             
                    # Load from file if provided
         | 
| 53 58 | 
             
                    if config_file:
         | 
| 54 59 | 
             
                        self.load_file(config_file)
         | 
| 60 | 
            +
                        self._loaded_from = str(config_file)
         | 
| 55 61 | 
             
                    else:
         | 
| 56 62 | 
             
                        # Try to load from standard location: .claude-mpm/configuration.yaml
         | 
| 57 63 | 
             
                        default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
         | 
| 58 64 | 
             
                        if default_config.exists():
         | 
| 59 65 | 
             
                            self.load_file(default_config)
         | 
| 66 | 
            +
                            self._loaded_from = str(default_config)
         | 
| 67 | 
            +
                        else:
         | 
| 68 | 
            +
                            # Also try .yml extension
         | 
| 69 | 
            +
                            alt_config = Path.cwd() / ".claude-mpm" / "configuration.yml"
         | 
| 70 | 
            +
                            if alt_config.exists():
         | 
| 71 | 
            +
                                self.load_file(alt_config)
         | 
| 72 | 
            +
                                self._loaded_from = str(alt_config)
         | 
| 60 73 |  | 
| 61 74 | 
             
                    # Load from environment variables (new and legacy prefixes)
         | 
| 62 75 | 
             
                    self._load_env_vars()
         | 
| @@ -66,21 +79,64 @@ class Config: | |
| 66 79 | 
             
                    self._apply_defaults()
         | 
| 67 80 |  | 
| 68 81 | 
             
                def load_file(self, file_path: Union[str, Path]) -> None:
         | 
| 69 | 
            -
                    """Load configuration from file. | 
| 82 | 
            +
                    """Load configuration from file with enhanced error handling.
         | 
| 83 | 
            +
                    
         | 
| 84 | 
            +
                    WHY: Configuration loading failures can cause silent issues. We need
         | 
| 85 | 
            +
                    to provide clear, actionable error messages to help users fix problems.
         | 
| 86 | 
            +
                    """
         | 
| 70 87 | 
             
                    file_path = Path(file_path)
         | 
| 71 88 |  | 
| 72 89 | 
             
                    if not file_path.exists():
         | 
| 73 90 | 
             
                        logger.warning(f"Configuration file not found: {file_path}")
         | 
| 91 | 
            +
                        logger.info(f"TIP: Create a configuration file with: mkdir -p {file_path.parent} && touch {file_path}")
         | 
| 74 92 | 
             
                        return
         | 
| 75 93 |  | 
| 76 94 | 
             
                    try:
         | 
| 95 | 
            +
                        # Check if file is readable
         | 
| 96 | 
            +
                        if not os.access(file_path, os.R_OK):
         | 
| 97 | 
            +
                            logger.error(f"Configuration file is not readable: {file_path}")
         | 
| 98 | 
            +
                            logger.info(f"TIP: Fix permissions with: chmod 644 {file_path}")
         | 
| 99 | 
            +
                            return
         | 
| 100 | 
            +
                            
         | 
| 101 | 
            +
                        # Check file size (warn if too large)
         | 
| 102 | 
            +
                        file_size = file_path.stat().st_size
         | 
| 103 | 
            +
                        if file_size > 1024 * 1024:  # 1MB
         | 
| 104 | 
            +
                            logger.warning(f"Configuration file is large ({file_size} bytes): {file_path}")
         | 
| 105 | 
            +
                            
         | 
| 106 | 
            +
                        # Try to load the configuration
         | 
| 77 107 | 
             
                        file_config = self._config_mgr.load_auto(file_path)
         | 
| 78 108 | 
             
                        if file_config:
         | 
| 79 109 | 
             
                            self._config = self._config_mgr.merge_configs(self._config, file_config)
         | 
| 80 | 
            -
                            logger.info(f" | 
| 81 | 
            -
             | 
| 110 | 
            +
                            logger.info(f"✓ Successfully loaded configuration from {file_path}")
         | 
| 111 | 
            +
                            
         | 
| 112 | 
            +
                            # Log important configuration values for debugging
         | 
| 113 | 
            +
                            if logger.isEnabledFor(logging.DEBUG):
         | 
| 114 | 
            +
                                response_logging = file_config.get('response_logging', {})
         | 
| 115 | 
            +
                                if response_logging:
         | 
| 116 | 
            +
                                    logger.debug(f"Response logging enabled: {response_logging.get('enabled', False)}")
         | 
| 117 | 
            +
                                    logger.debug(f"Response logging format: {response_logging.get('format', 'json')}")
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    except yaml.YAMLError as e:
         | 
| 120 | 
            +
                        logger.error(f"YAML syntax error in {file_path}: {e}")
         | 
| 121 | 
            +
                        if hasattr(e, 'problem_mark'):
         | 
| 122 | 
            +
                            mark = e.problem_mark
         | 
| 123 | 
            +
                            logger.error(f"Error at line {mark.line + 1}, column {mark.column + 1}")
         | 
| 124 | 
            +
                        logger.info("TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py")
         | 
| 125 | 
            +
                        logger.info("TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/    /g' " + str(file_path))
         | 
| 126 | 
            +
                        # Store error for later retrieval
         | 
| 127 | 
            +
                        self._config['_load_error'] = str(e)
         | 
| 128 | 
            +
                        
         | 
| 129 | 
            +
                    except json.JSONDecodeError as e:
         | 
| 130 | 
            +
                        logger.error(f"JSON syntax error in {file_path}: {e}")
         | 
| 131 | 
            +
                        logger.error(f"Error at line {e.lineno}, column {e.colno}")
         | 
| 132 | 
            +
                        logger.info("TIP: Validate your JSON at https://jsonlint.com/")
         | 
| 133 | 
            +
                        self._config['_load_error'] = str(e)
         | 
| 134 | 
            +
                        
         | 
| 82 135 | 
             
                    except Exception as e:
         | 
| 83 136 | 
             
                        logger.error(f"Failed to load configuration from {file_path}: {e}")
         | 
| 137 | 
            +
                        logger.info("TIP: Check file permissions and format (YAML/JSON)")
         | 
| 138 | 
            +
                        logger.info("TIP: Run validation with: python scripts/validate_configuration.py")
         | 
| 139 | 
            +
                        self._config['_load_error'] = str(e)
         | 
| 84 140 |  | 
| 85 141 | 
             
                def _load_env_vars(self) -> None:
         | 
| 86 142 | 
             
                    """Load configuration from environment variables."""
         | 
| @@ -323,6 +379,12 @@ class Config: | |
| 323 379 | 
             
                                    "emergency_stop": True
         | 
| 324 380 | 
             
                                }
         | 
| 325 381 | 
             
                            }
         | 
| 382 | 
            +
                        },
         | 
| 383 | 
            +
                        # Agent deployment configuration
         | 
| 384 | 
            +
                        "agent_deployment": {
         | 
| 385 | 
            +
                            "excluded_agents": [],              # List of agent IDs to exclude from deployment
         | 
| 386 | 
            +
                            "exclude_dependencies": False,      # Whether to exclude agent dependencies too
         | 
| 387 | 
            +
                            "case_sensitive": False            # Whether agent name matching is case-sensitive
         | 
| 326 388 | 
             
                        }
         | 
| 327 389 | 
             
                    }
         | 
| 328 390 |  | 
| @@ -515,6 +577,101 @@ class Config: | |
| 515 577 |  | 
| 516 578 | 
             
                    return base_config
         | 
| 517 579 |  | 
| 580 | 
            +
                def validate_configuration(self) -> Tuple[bool, List[str], List[str]]:
         | 
| 581 | 
            +
                    """Validate the loaded configuration programmatically.
         | 
| 582 | 
            +
                    
         | 
| 583 | 
            +
                    WHY: Provide a programmatic way to validate configuration that can be
         | 
| 584 | 
            +
                    used by other components to check configuration health.
         | 
| 585 | 
            +
                    
         | 
| 586 | 
            +
                    Returns:
         | 
| 587 | 
            +
                        Tuple of (is_valid, errors, warnings)
         | 
| 588 | 
            +
                    """
         | 
| 589 | 
            +
                    errors = []
         | 
| 590 | 
            +
                    warnings = []
         | 
| 591 | 
            +
                    
         | 
| 592 | 
            +
                    # Check if there was a load error
         | 
| 593 | 
            +
                    if '_load_error' in self._config:
         | 
| 594 | 
            +
                        errors.append(f"Configuration load error: {self._config['_load_error']}")
         | 
| 595 | 
            +
                        
         | 
| 596 | 
            +
                    # Validate response_logging configuration
         | 
| 597 | 
            +
                    response_logging = self.get('response_logging', {})
         | 
| 598 | 
            +
                    if response_logging:
         | 
| 599 | 
            +
                        # Check enabled field
         | 
| 600 | 
            +
                        if 'enabled' in response_logging and not isinstance(response_logging['enabled'], bool):
         | 
| 601 | 
            +
                            errors.append(
         | 
| 602 | 
            +
                                f"response_logging.enabled must be boolean, got {type(response_logging['enabled']).__name__}"
         | 
| 603 | 
            +
                            )
         | 
| 604 | 
            +
                            
         | 
| 605 | 
            +
                        # Check format field
         | 
| 606 | 
            +
                        if 'format' in response_logging:
         | 
| 607 | 
            +
                            valid_formats = ['json', 'syslog', 'journald']
         | 
| 608 | 
            +
                            if response_logging['format'] not in valid_formats:
         | 
| 609 | 
            +
                                errors.append(
         | 
| 610 | 
            +
                                    f"response_logging.format must be one of {valid_formats}, "
         | 
| 611 | 
            +
                                    f"got '{response_logging['format']}'"
         | 
| 612 | 
            +
                                )
         | 
| 613 | 
            +
                                
         | 
| 614 | 
            +
                        # Check session_directory
         | 
| 615 | 
            +
                        if 'session_directory' in response_logging:
         | 
| 616 | 
            +
                            session_dir = Path(response_logging['session_directory'])
         | 
| 617 | 
            +
                            if session_dir.is_absolute() and not session_dir.parent.exists():
         | 
| 618 | 
            +
                                warnings.append(
         | 
| 619 | 
            +
                                    f"Parent directory for session_directory does not exist: {session_dir.parent}"
         | 
| 620 | 
            +
                                )
         | 
| 621 | 
            +
                                
         | 
| 622 | 
            +
                    # Validate memory configuration
         | 
| 623 | 
            +
                    memory_config = self.get('memory', {})
         | 
| 624 | 
            +
                    if memory_config:
         | 
| 625 | 
            +
                        if 'enabled' in memory_config and not isinstance(memory_config['enabled'], bool):
         | 
| 626 | 
            +
                            errors.append(f"memory.enabled must be boolean")
         | 
| 627 | 
            +
                            
         | 
| 628 | 
            +
                        # Check limits
         | 
| 629 | 
            +
                        limits = memory_config.get('limits', {})
         | 
| 630 | 
            +
                        for field in ['default_size_kb', 'max_sections', 'max_items_per_section']:
         | 
| 631 | 
            +
                            if field in limits:
         | 
| 632 | 
            +
                                value = limits[field]
         | 
| 633 | 
            +
                                if not isinstance(value, int) or value <= 0:
         | 
| 634 | 
            +
                                    errors.append(f"memory.limits.{field} must be positive integer, got {value}")
         | 
| 635 | 
            +
                                    
         | 
| 636 | 
            +
                    # Validate health thresholds
         | 
| 637 | 
            +
                    health_thresholds = self.get('health_thresholds', {})
         | 
| 638 | 
            +
                    if health_thresholds:
         | 
| 639 | 
            +
                        cpu = health_thresholds.get('cpu_percent')
         | 
| 640 | 
            +
                        if cpu is not None and (not isinstance(cpu, (int, float)) or cpu < 0 or cpu > 100):
         | 
| 641 | 
            +
                            errors.append(f"health_thresholds.cpu_percent must be 0-100, got {cpu}")
         | 
| 642 | 
            +
                            
         | 
| 643 | 
            +
                        mem = health_thresholds.get('memory_mb')
         | 
| 644 | 
            +
                        if mem is not None and (not isinstance(mem, (int, float)) or mem <= 0):
         | 
| 645 | 
            +
                            errors.append(f"health_thresholds.memory_mb must be positive, got {mem}")
         | 
| 646 | 
            +
                            
         | 
| 647 | 
            +
                    is_valid = len(errors) == 0
         | 
| 648 | 
            +
                    return is_valid, errors, warnings
         | 
| 649 | 
            +
                    
         | 
| 650 | 
            +
                def get_configuration_status(self) -> Dict[str, Any]:
         | 
| 651 | 
            +
                    """Get detailed configuration status for debugging.
         | 
| 652 | 
            +
                    
         | 
| 653 | 
            +
                    WHY: Provide a comprehensive view of configuration state for
         | 
| 654 | 
            +
                    troubleshooting and health checks.
         | 
| 655 | 
            +
                    
         | 
| 656 | 
            +
                    Returns:
         | 
| 657 | 
            +
                        Dictionary with configuration status information
         | 
| 658 | 
            +
                    """
         | 
| 659 | 
            +
                    is_valid, errors, warnings = self.validate_configuration()
         | 
| 660 | 
            +
                    
         | 
| 661 | 
            +
                    status = {
         | 
| 662 | 
            +
                        'valid': is_valid,
         | 
| 663 | 
            +
                        'errors': errors,
         | 
| 664 | 
            +
                        'warnings': warnings,
         | 
| 665 | 
            +
                        'loaded_from': getattr(self, '_loaded_from', 'defaults'),
         | 
| 666 | 
            +
                        'key_count': len(self._config),
         | 
| 667 | 
            +
                        'has_response_logging': 'response_logging' in self._config,
         | 
| 668 | 
            +
                        'has_memory_config': 'memory' in self._config,
         | 
| 669 | 
            +
                        'response_logging_enabled': self.get('response_logging.enabled', False),
         | 
| 670 | 
            +
                        'memory_enabled': self.get('memory.enabled', False)
         | 
| 671 | 
            +
                    }
         | 
| 672 | 
            +
                    
         | 
| 673 | 
            +
                    return status
         | 
| 674 | 
            +
             | 
| 518 675 | 
             
                def __repr__(self) -> str:
         | 
| 519 676 | 
             
                    """String representation of configuration."""
         | 
| 520 677 | 
             
                    return f"<Config({len(self._config)} keys)>"
         |