claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/doctor.py +2 -2
  5. claude_mpm/cli/commands/mpm_init.py +3 -3
  6. claude_mpm/cli/parsers/configure_parser.py +4 -15
  7. claude_mpm/core/framework/__init__.py +38 -0
  8. claude_mpm/core/framework/formatters/__init__.py +11 -0
  9. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  10. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  11. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  12. claude_mpm/core/framework/loaders/__init__.py +13 -0
  13. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  14. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  15. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  16. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  17. claude_mpm/core/framework/processors/__init__.py +11 -0
  18. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  19. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  20. claude_mpm/core/framework/processors/template_processor.py +238 -0
  21. claude_mpm/core/framework_loader.py +277 -1798
  22. claude_mpm/hooks/__init__.py +9 -1
  23. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  24. claude_mpm/hooks/memory_integration_hook.py +1 -1
  25. claude_mpm/services/agents/memory/content_manager.py +5 -2
  26. claude_mpm/services/agents/memory/memory_file_service.py +1 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
  28. claude_mpm/services/core/path_resolver.py +1 -0
  29. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  30. claude_mpm/services/mcp_config_manager.py +67 -4
  31. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  32. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  33. claude_mpm/services/mcp_gateway/main.py +3 -13
  34. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  35. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  36. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  37. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  38. claude_mpm/services/shared/__init__.py +2 -1
  39. claude_mpm/services/shared/service_factory.py +8 -5
  40. claude_mpm/services/unified/__init__.py +65 -0
  41. claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
  42. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
  43. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
  44. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
  45. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
  46. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
  47. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  48. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  49. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  50. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  51. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  52. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  53. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  54. claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
  55. claude_mpm/services/unified/deployment_strategies/base.py +557 -0
  56. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
  57. claude_mpm/services/unified/deployment_strategies/local.py +594 -0
  58. claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
  59. claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
  60. claude_mpm/services/unified/interfaces.py +499 -0
  61. claude_mpm/services/unified/migration.py +532 -0
  62. claude_mpm/services/unified/strategies.py +551 -0
  63. claude_mpm/services/unified/unified_analyzer.py +534 -0
  64. claude_mpm/services/unified/unified_config.py +688 -0
  65. claude_mpm/services/unified/unified_deployment.py +470 -0
  66. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  67. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
  68. claude_mpm/cli/commands/configure_tui.py +0 -1927
  69. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  70. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  71. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  72. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  73. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  74. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,283 @@
1
+ """Framework content formatter for generating instructions."""
2
+
3
+ import re
4
+ from typing import Any, Dict, Optional
5
+
6
+ from claude_mpm.core.logging_utils import get_logger
7
+
8
+
9
+ class ContentFormatter:
10
+ """Formats framework content for injection into prompts."""
11
+
12
+ def __init__(self):
13
+ """Initialize the content formatter."""
14
+ self.logger = get_logger("content_formatter")
15
+
16
+ def strip_metadata_comments(self, content: str) -> str:
17
+ """Strip metadata HTML comments from content.
18
+
19
+ Removes comments like:
20
+ <!-- FRAMEWORK_VERSION: 0010 -->
21
+ <!-- LAST_MODIFIED: 2025-08-10T00:00:00Z -->
22
+
23
+ Args:
24
+ content: Content to clean
25
+
26
+ Returns:
27
+ Cleaned content without metadata comments
28
+ """
29
+ # Remove HTML comments that contain metadata
30
+ cleaned = re.sub(
31
+ r"<!--\s*(FRAMEWORK_VERSION|LAST_MODIFIED|WORKFLOW_VERSION|PROJECT_WORKFLOW_VERSION|CUSTOM_PROJECT_WORKFLOW)[^>]*-->\n?",
32
+ "",
33
+ content,
34
+ )
35
+ # Also remove any leading blank lines that might result
36
+ return cleaned.lstrip("\n")
37
+
38
+ def format_full_framework(
39
+ self,
40
+ framework_content: Dict[str, Any],
41
+ capabilities_section: str,
42
+ context_section: str,
43
+ inject_output_style: bool = False,
44
+ output_style_content: Optional[str] = None,
45
+ ) -> str:
46
+ """Format complete framework instructions.
47
+
48
+ Args:
49
+ framework_content: Dictionary containing framework content
50
+ capabilities_section: Generated agent capabilities section
51
+ context_section: Generated temporal/user context section
52
+ inject_output_style: Whether to inject output style content
53
+ output_style_content: Output style content to inject (if needed)
54
+
55
+ Returns:
56
+ Formatted framework instructions
57
+ """
58
+ # If we have the full framework INSTRUCTIONS.md, use it
59
+ if framework_content.get("framework_instructions"):
60
+ instructions = self.strip_metadata_comments(
61
+ framework_content["framework_instructions"]
62
+ )
63
+
64
+ # Add custom INSTRUCTIONS.md if present (overrides or extends framework instructions)
65
+ if framework_content.get("custom_instructions"):
66
+ level = framework_content.get("custom_instructions_level", "unknown")
67
+ instructions += f"\n\n## Custom PM Instructions ({level} level)\n\n"
68
+ instructions += "**The following custom instructions override or extend the framework defaults:**\n\n"
69
+ instructions += self.strip_metadata_comments(
70
+ framework_content["custom_instructions"]
71
+ )
72
+ instructions += "\n"
73
+
74
+ # Add WORKFLOW.md after instructions
75
+ if framework_content.get("workflow_instructions"):
76
+ workflow_content = self.strip_metadata_comments(
77
+ framework_content["workflow_instructions"]
78
+ )
79
+ level = framework_content.get("workflow_instructions_level", "system")
80
+ if level != "system":
81
+ instructions += f"\n\n## Workflow Instructions ({level} level)\n\n"
82
+ instructions += "**The following workflow instructions override system defaults:**\n\n"
83
+ instructions += f"{workflow_content}\n"
84
+
85
+ # Add MEMORY.md after workflow instructions
86
+ if framework_content.get("memory_instructions"):
87
+ memory_content = self.strip_metadata_comments(
88
+ framework_content["memory_instructions"]
89
+ )
90
+ level = framework_content.get("memory_instructions_level", "system")
91
+ if level != "system":
92
+ instructions += f"\n\n## Memory Instructions ({level} level)\n\n"
93
+ instructions += "**The following memory instructions override system defaults:**\n\n"
94
+ instructions += f"{memory_content}\n"
95
+
96
+ # Add actual PM memories after memory instructions
97
+ if framework_content.get("actual_memories"):
98
+ instructions += "\n\n## Current PM Memories\n\n"
99
+ instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
100
+ instructions += framework_content["actual_memories"]
101
+ instructions += "\n"
102
+
103
+ # Add agent memories if available
104
+ if framework_content.get("agent_memories"):
105
+ agent_memories = framework_content["agent_memories"]
106
+ if agent_memories:
107
+ instructions += "\n\n## Agent Memories\n\n"
108
+ instructions += "**The following are accumulated memories from specialized agents:**\n\n"
109
+
110
+ for agent_name in sorted(agent_memories.keys()):
111
+ memory_content = agent_memories[agent_name]
112
+ if memory_content:
113
+ instructions += f"### {agent_name.replace('_', ' ').title()} Agent Memory\n\n"
114
+ instructions += memory_content
115
+ instructions += "\n\n"
116
+
117
+ # Add dynamic agent capabilities section
118
+ instructions += capabilities_section
119
+
120
+ # Add enhanced temporal and user context for better awareness
121
+ instructions += context_section
122
+
123
+ # Add BASE_PM.md framework requirements AFTER INSTRUCTIONS.md
124
+ if framework_content.get("base_pm_instructions"):
125
+ base_pm = self.strip_metadata_comments(
126
+ framework_content["base_pm_instructions"]
127
+ )
128
+ instructions += f"\n\n{base_pm}"
129
+
130
+ # Inject output style content if needed (for Claude < 1.0.83)
131
+ if inject_output_style and output_style_content:
132
+ instructions += "\n\n## Output Style Configuration\n"
133
+ instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
134
+ instructions += output_style_content
135
+ instructions += "\n"
136
+
137
+ # Clean up any trailing whitespace
138
+ return instructions.rstrip() + "\n"
139
+
140
+ # Otherwise generate minimal framework
141
+ return self.format_minimal_framework(framework_content)
142
+
143
+ def format_minimal_framework(self, framework_content: Dict[str, Any]) -> str:
144
+ """Format minimal framework instructions when full framework not available.
145
+
146
+ Args:
147
+ framework_content: Dictionary containing framework content
148
+
149
+ Returns:
150
+ Minimal framework instructions
151
+ """
152
+ instructions = """# Claude MPM Framework Instructions
153
+
154
+ You are operating within the Claude Multi-Agent Project Manager (MPM) framework.
155
+
156
+ ## Core Role
157
+ You are a multi-agent orchestrator. Your primary responsibilities are:
158
+ - Delegate all implementation work to specialized agents via Task Tool
159
+ - Coordinate multi-agent workflows and cross-agent collaboration
160
+ - Extract and track TODO/BUG/FEATURE items for ticket creation
161
+ - Maintain project visibility and strategic oversight
162
+ - NEVER perform direct implementation work yourself
163
+
164
+ """
165
+
166
+ # Add agent definitions if available
167
+ if framework_content.get("agents"):
168
+ instructions += "## Available Agents\n\n"
169
+ instructions += "You have the following specialized agents available for delegation:\n\n"
170
+
171
+ # List agents with brief descriptions and correct IDs
172
+ agent_list = []
173
+ for agent_name in sorted(framework_content["agents"].keys()):
174
+ # Use the actual agent_name as the ID (it's the filename stem)
175
+ agent_id = agent_name
176
+ clean_name = agent_name.replace("-", " ").replace("_", " ").title()
177
+ if "engineer" in agent_name.lower() and "data" not in agent_name.lower():
178
+ agent_list.append(
179
+ f"- **Engineer Agent** (`{agent_id}`): Code implementation and development"
180
+ )
181
+ elif "qa" in agent_name.lower():
182
+ agent_list.append(
183
+ f"- **QA Agent** (`{agent_id}`): Testing and quality assurance"
184
+ )
185
+ elif "documentation" in agent_name.lower():
186
+ agent_list.append(
187
+ f"- **Documentation Agent** (`{agent_id}`): Documentation creation and maintenance"
188
+ )
189
+ elif "research" in agent_name.lower():
190
+ agent_list.append(
191
+ f"- **Research Agent** (`{agent_id}`): Investigation and analysis"
192
+ )
193
+ elif "security" in agent_name.lower():
194
+ agent_list.append(
195
+ f"- **Security Agent** (`{agent_id}`): Security analysis and protection"
196
+ )
197
+ elif "version" in agent_name.lower():
198
+ agent_list.append(
199
+ f"- **Version Control Agent** (`{agent_id}`): Git operations and version management"
200
+ )
201
+ elif "ops" in agent_name.lower():
202
+ agent_list.append(
203
+ f"- **Ops Agent** (`{agent_id}`): Deployment and operations"
204
+ )
205
+ elif "data" in agent_name.lower():
206
+ agent_list.append(
207
+ f"- **Data Engineer Agent** (`{agent_id}`): Data management and AI API integration"
208
+ )
209
+ else:
210
+ agent_list.append(
211
+ f"- **{clean_name}** (`{agent_id}`): Available for specialized tasks"
212
+ )
213
+
214
+ instructions += "\n".join(agent_list) + "\n\n"
215
+
216
+ # Add full agent details
217
+ instructions += "### Agent Details\n\n"
218
+ for agent_name, agent_content in sorted(framework_content["agents"].items()):
219
+ instructions += f"#### {agent_name.replace('-', ' ').title()}\n"
220
+ instructions += agent_content + "\n\n"
221
+
222
+ # Add orchestration principles
223
+ instructions += """
224
+ ## Orchestration Principles
225
+ 1. **Always Delegate**: Never perform direct work - use Task Tool for all implementation
226
+ 2. **Comprehensive Context**: Provide rich, filtered context to each agent
227
+ 3. **Track Everything**: Extract all TODO/BUG/FEATURE items systematically
228
+ 4. **Cross-Agent Coordination**: Orchestrate workflows spanning multiple agents
229
+ 5. **Results Integration**: Actively receive and integrate agent results
230
+
231
+ ## Task Tool Format
232
+ ```
233
+ **[Agent Name]**: [Clear task description with deliverables]
234
+
235
+ TEMPORAL CONTEXT: Today is [date]. Apply date awareness to [specific considerations].
236
+
237
+ **Task**: [Detailed task breakdown]
238
+ 1. [Specific action item 1]
239
+ 2. [Specific action item 2]
240
+ 3. [Specific action item 3]
241
+
242
+ **Context**: [Comprehensive filtered context for this agent]
243
+ **Authority**: [Agent's decision-making scope]
244
+ **Expected Results**: [Specific deliverables needed]
245
+ **Integration**: [How results integrate with other work]
246
+ ```
247
+
248
+ ## Ticket Extraction Patterns
249
+ Extract tickets from these patterns:
250
+ - TODO: [description] → TODO ticket
251
+ - BUG: [description] → BUG ticket
252
+ - FEATURE: [description] → FEATURE ticket
253
+ - ISSUE: [description] → ISSUE ticket
254
+ - FIXME: [description] → BUG ticket
255
+
256
+ ---
257
+ """
258
+
259
+ return instructions
260
+
261
+ def get_fallback_capabilities(self) -> str:
262
+ """Return fallback capabilities when dynamic discovery fails.
263
+
264
+ Returns:
265
+ Fallback agent capabilities section
266
+ """
267
+ return """
268
+
269
+ ## Available Agent Capabilities
270
+
271
+ You have the following specialized agents available for delegation:
272
+
273
+ - **Engineer** (`engineer`): Code implementation and development
274
+ - **Research** (`research-agent`): Investigation and analysis
275
+ - **QA** (`qa-agent`): Testing and quality assurance
276
+ - **Documentation** (`documentation-agent`): Documentation creation and maintenance
277
+ - **Security** (`security-agent`): Security analysis and protection
278
+ - **Data Engineer** (`data-engineer`): Data management and pipelines
279
+ - **Ops** (`ops-agent`): Deployment and operations
280
+ - **Version Control** (`version-control`): Git operations and version management
281
+
282
+ **IMPORTANT**: Use the exact agent ID in parentheses when delegating tasks.
283
+ """
@@ -0,0 +1,180 @@
1
+ """Temporal and user context generator for framework instructions."""
2
+
3
+ import getpass
4
+ import locale
5
+ import os
6
+ import platform
7
+ import time as time_module
8
+ from datetime import datetime, timezone
9
+
10
+ from claude_mpm.core.logging_utils import get_logger
11
+
12
+
13
+ class ContextGenerator:
14
+ """Generates temporal and user context for better PM awareness."""
15
+
16
+ def __init__(self):
17
+ """Initialize the context generator."""
18
+ self.logger = get_logger("context_generator")
19
+
20
+ def generate_temporal_user_context(self) -> str:
21
+ """Generate enhanced temporal and user context for better PM awareness.
22
+
23
+ Returns:
24
+ Formatted context string with datetime, user, and system information
25
+ """
26
+ context_lines = ["\n\n## Temporal & User Context\n"]
27
+
28
+ try:
29
+ # Get current datetime with timezone awareness
30
+ now = datetime.now(timezone.utc)
31
+
32
+ # Try to get timezone info - fallback to UTC offset if timezone name not available
33
+ try:
34
+ if hasattr(time_module, "tzname"):
35
+ tz_name = time_module.tzname[time_module.daylight]
36
+ tz_offset = time_module.strftime("%z")
37
+ if tz_offset:
38
+ # Format UTC offset properly (e.g., -0800 to -08:00)
39
+ tz_offset = (
40
+ f"{tz_offset[:3]}:{tz_offset[3:]}"
41
+ if len(tz_offset) >= 4
42
+ else tz_offset
43
+ )
44
+ tz_info = f"{tz_name} (UTC{tz_offset})"
45
+ else:
46
+ tz_info = tz_name
47
+ else:
48
+ tz_info = "Local Time"
49
+ except Exception:
50
+ tz_info = "Local Time"
51
+
52
+ # Format datetime components
53
+ date_str = now.strftime("%Y-%m-%d")
54
+ time_str = now.strftime("%H:%M:%S")
55
+ day_name = now.strftime("%A")
56
+
57
+ context_lines.append(f"**Current DateTime**: {date_str} {time_str} {tz_info}\n")
58
+ context_lines.append(f"**Day**: {day_name}\n")
59
+
60
+ except Exception as e:
61
+ # Fallback to basic date if enhanced datetime fails
62
+ self.logger.debug(f"Error generating enhanced datetime context: {e}")
63
+ context_lines.append(
64
+ f"**Today's Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}\n"
65
+ )
66
+
67
+ # Get user information
68
+ self._add_user_context(context_lines)
69
+
70
+ # Get system information
71
+ self._add_system_context(context_lines)
72
+
73
+ # Get environment information
74
+ self._add_environment_context(context_lines)
75
+
76
+ # Add instruction for applying context
77
+ context_lines.append(
78
+ "\nApply temporal and user awareness to all tasks, "
79
+ "decisions, and interactions.\n"
80
+ )
81
+ context_lines.append(
82
+ "Use this context for personalized responses and "
83
+ "time-sensitive operations.\n"
84
+ )
85
+
86
+ return "".join(context_lines)
87
+
88
+ def _add_user_context(self, context_lines: list) -> None:
89
+ """Add user information to context.
90
+
91
+ Args:
92
+ context_lines: List to append context lines to
93
+ """
94
+ try:
95
+ # Get user information with safe fallbacks
96
+ username = None
97
+
98
+ # Try multiple methods to get username
99
+ methods = [
100
+ lambda: os.environ.get("USER"),
101
+ lambda: os.environ.get("USERNAME"), # Windows fallback
102
+ lambda: getpass.getuser(),
103
+ ]
104
+
105
+ for method in methods:
106
+ try:
107
+ username = method()
108
+ if username:
109
+ break
110
+ except Exception:
111
+ continue
112
+
113
+ if username:
114
+ context_lines.append(f"**User**: {username}\n")
115
+
116
+ # Add home directory if available
117
+ try:
118
+ home_dir = os.path.expanduser("~")
119
+ if home_dir and home_dir != "~":
120
+ context_lines.append(f"**Home Directory**: {home_dir}\n")
121
+ except Exception:
122
+ pass
123
+
124
+ except Exception as e:
125
+ # User detection is optional, don't fail
126
+ self.logger.debug(f"Could not detect user information: {e}")
127
+
128
+ def _add_system_context(self, context_lines: list) -> None:
129
+ """Add system information to context.
130
+
131
+ Args:
132
+ context_lines: List to append context lines to
133
+ """
134
+ try:
135
+ # Get system information
136
+ system_info = platform.system()
137
+ if system_info:
138
+ # Enhance system name for common platforms
139
+ system_names = {
140
+ "Darwin": "Darwin (macOS)",
141
+ "Linux": "Linux",
142
+ "Windows": "Windows",
143
+ }
144
+ system_display = system_names.get(system_info, system_info)
145
+ context_lines.append(f"**System**: {system_display}\n")
146
+
147
+ # Add platform version if available
148
+ try:
149
+ platform_version = platform.release()
150
+ if platform_version:
151
+ context_lines.append(f"**System Version**: {platform_version}\n")
152
+ except Exception:
153
+ pass
154
+
155
+ except Exception as e:
156
+ # System info is optional
157
+ self.logger.debug(f"Could not detect system information: {e}")
158
+
159
+ def _add_environment_context(self, context_lines: list) -> None:
160
+ """Add environment information to context.
161
+
162
+ Args:
163
+ context_lines: List to append context lines to
164
+ """
165
+ try:
166
+ # Add current working directory
167
+ cwd = os.getcwd()
168
+ if cwd:
169
+ context_lines.append(f"**Working Directory**: {cwd}\n")
170
+ except Exception:
171
+ pass
172
+
173
+ try:
174
+ # Add locale information if available
175
+ current_locale = locale.getlocale()
176
+ if current_locale and current_locale[0]:
177
+ context_lines.append(f"**Locale**: {current_locale[0]}\n")
178
+ except Exception:
179
+ # Locale is optional
180
+ pass
@@ -0,0 +1,13 @@
1
+ """Framework loaders for handling various types of file and content loading."""
2
+
3
+ from .agent_loader import AgentLoader
4
+ from .file_loader import FileLoader
5
+ from .instruction_loader import InstructionLoader
6
+ from .packaged_loader import PackagedLoader
7
+
8
+ __all__ = [
9
+ "FileLoader",
10
+ "PackagedLoader",
11
+ "InstructionLoader",
12
+ "AgentLoader",
13
+ ]
@@ -0,0 +1,202 @@
1
+ """Loader for agent discovery and management."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional, Set, Tuple
5
+
6
+ from claude_mpm.core.logging_utils import get_logger
7
+
8
+
9
+ class AgentLoader:
10
+ """Handles agent discovery and loading from various sources."""
11
+
12
+ def __init__(self, framework_path: Optional[Path] = None):
13
+ """Initialize the agent loader.
14
+
15
+ Args:
16
+ framework_path: Path to framework installation
17
+ """
18
+ self.logger = get_logger("agent_loader")
19
+ self.framework_path = framework_path
20
+
21
+ def get_deployed_agents(self) -> Set[str]:
22
+ """
23
+ Get a set of deployed agent names from .claude/agents/ directories.
24
+
25
+ Returns:
26
+ Set of agent names (file stems) that are deployed
27
+ """
28
+ self.logger.debug("Scanning for deployed agents")
29
+ deployed = set()
30
+
31
+ # Check multiple locations for deployed agents
32
+ agents_dirs = [
33
+ Path.cwd() / ".claude" / "agents", # Project-specific agents
34
+ Path.home() / ".claude" / "agents", # User's system agents
35
+ ]
36
+
37
+ for agents_dir in agents_dirs:
38
+ if agents_dir.exists():
39
+ for agent_file in agents_dir.glob("*.md"):
40
+ if not agent_file.name.startswith("."):
41
+ # Use stem to get agent name without extension
42
+ deployed.add(agent_file.stem)
43
+ self.logger.debug(
44
+ f"Found deployed agent: {agent_file.stem} in {agents_dir}"
45
+ )
46
+
47
+ self.logger.debug(f"Total deployed agents found: {len(deployed)}")
48
+ return deployed
49
+
50
+ def load_single_agent(self, agent_file: Path) -> Tuple[Optional[str], Optional[str]]:
51
+ """
52
+ Load a single agent file.
53
+
54
+ Args:
55
+ agent_file: Path to the agent file
56
+
57
+ Returns:
58
+ Tuple of (agent_name, agent_content) or (None, None) on failure
59
+ """
60
+ try:
61
+ agent_name = agent_file.stem
62
+ # Skip README files
63
+ if agent_name.upper() == "README":
64
+ return None, None
65
+ content = agent_file.read_text()
66
+ self.logger.debug(f"Loaded agent: {agent_name}")
67
+ return agent_name, content
68
+ except Exception as e:
69
+ self.logger.error(f"Failed to load agent {agent_file}: {e}")
70
+ return None, None
71
+
72
+ def load_agents_directory(
73
+ self,
74
+ agents_dir: Optional[Path],
75
+ templates_dir: Optional[Path] = None,
76
+ main_dir: Optional[Path] = None,
77
+ ) -> Dict[str, str]:
78
+ """
79
+ Load agent definitions from the appropriate directory.
80
+
81
+ Args:
82
+ agents_dir: Primary agents directory to load from
83
+ templates_dir: Templates directory path
84
+ main_dir: Main agents directory path
85
+
86
+ Returns:
87
+ Dictionary mapping agent names to their content
88
+ """
89
+ agents = {}
90
+
91
+ if not agents_dir or not agents_dir.exists():
92
+ return agents
93
+
94
+ # Load all agent files
95
+ for agent_file in agents_dir.glob("*.md"):
96
+ agent_name, agent_content = self.load_single_agent(agent_file)
97
+ if agent_name and agent_content:
98
+ agents[agent_name] = agent_content
99
+
100
+ # If we used templates dir, also check main dir for base_agent.md
101
+ if agents_dir == templates_dir and main_dir and main_dir.exists():
102
+ if "base_agent" not in agents:
103
+ base_agent_file = main_dir / "base_agent.md"
104
+ if base_agent_file.exists():
105
+ agent_name, agent_content = self.load_single_agent(base_agent_file)
106
+ if agent_name and agent_content:
107
+ agents[agent_name] = agent_content
108
+
109
+ return agents
110
+
111
+ def discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
112
+ """Discover local JSON agent templates from .claude-mpm/agents/ directories.
113
+
114
+ Returns:
115
+ Dictionary mapping agent IDs to agent metadata
116
+ """
117
+ import json
118
+
119
+ local_agents = {}
120
+
121
+ # Check for local JSON templates in priority order
122
+ template_dirs = [
123
+ Path.cwd() / ".claude-mpm" / "agents", # Project local agents (highest priority)
124
+ Path.home() / ".claude-mpm" / "agents", # User local agents
125
+ ]
126
+
127
+ for priority, template_dir in enumerate(template_dirs):
128
+ if not template_dir.exists():
129
+ continue
130
+
131
+ for json_file in template_dir.glob("*.json"):
132
+ try:
133
+ with open(json_file) as f:
134
+ template_data = json.load(f)
135
+
136
+ # Extract agent metadata
137
+ agent_id = template_data.get("agent_id", json_file.stem)
138
+
139
+ # Skip if already found at higher priority
140
+ if agent_id in local_agents:
141
+ continue
142
+
143
+ # Extract metadata
144
+ metadata = template_data.get("metadata", {})
145
+
146
+ # Build agent data in expected format
147
+ agent_data = {
148
+ "id": agent_id,
149
+ "display_name": metadata.get(
150
+ "name", agent_id.replace("_", " ").title()
151
+ ),
152
+ "description": metadata.get(
153
+ "description", f"Local {agent_id} agent"
154
+ ),
155
+ "tools": self._extract_tools_from_template(template_data),
156
+ "is_local": True,
157
+ "tier": "project" if priority == 0 else "user",
158
+ "author": template_data.get("author", "local"),
159
+ "version": template_data.get("agent_version", "1.0.0"),
160
+ }
161
+
162
+ # Add routing data if present
163
+ if "routing" in template_data:
164
+ agent_data["routing"] = template_data["routing"]
165
+
166
+ # Add memory routing if present
167
+ if "memory_routing" in template_data:
168
+ agent_data["memory_routing"] = template_data["memory_routing"]
169
+
170
+ local_agents[agent_id] = agent_data
171
+ self.logger.debug(
172
+ f"Discovered local JSON agent: {agent_id} from {template_dir}"
173
+ )
174
+
175
+ except Exception as e:
176
+ self.logger.warning(
177
+ f"Failed to parse local JSON template {json_file}: {e}"
178
+ )
179
+
180
+ return local_agents
181
+
182
+ def _extract_tools_from_template(self, template_data: Dict[str, Any]) -> str:
183
+ """Extract tools string from template data.
184
+
185
+ Args:
186
+ template_data: JSON template data
187
+
188
+ Returns:
189
+ Tools string for display
190
+ """
191
+ capabilities = template_data.get("capabilities", {})
192
+ tools = capabilities.get("tools", "*")
193
+
194
+ if tools == "*":
195
+ return "All Tools"
196
+ if isinstance(tools, list):
197
+ return ", ".join(tools) if tools else "Standard Tools"
198
+ if isinstance(tools, str):
199
+ if "," in tools:
200
+ return tools
201
+ return tools
202
+ return "Standard Tools"