claude-mpm 1.0.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. claude_mpm/_version.py +4 -2
  2. claude_mpm/agents/INSTRUCTIONS.md +117 -312
  3. claude_mpm/agents/__init__.py +2 -2
  4. claude_mpm/agents/agent-template.yaml +83 -0
  5. claude_mpm/agents/agent_loader.py +192 -310
  6. claude_mpm/agents/base_agent.json +1 -1
  7. claude_mpm/agents/base_agent_loader.py +10 -15
  8. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +46 -0
  9. claude_mpm/agents/templates/{engineer_agent.json → backup/engineer_agent_20250726_234551.json} +1 -1
  10. claude_mpm/agents/templates/data_engineer.json +107 -0
  11. claude_mpm/agents/templates/documentation.json +106 -0
  12. claude_mpm/agents/templates/engineer.json +110 -0
  13. claude_mpm/agents/templates/ops.json +106 -0
  14. claude_mpm/agents/templates/qa.json +106 -0
  15. claude_mpm/agents/templates/research.json +107 -0
  16. claude_mpm/agents/templates/security.json +105 -0
  17. claude_mpm/agents/templates/version_control.json +103 -0
  18. claude_mpm/cli.py +41 -47
  19. claude_mpm/cli_enhancements.py +297 -0
  20. claude_mpm/core/factories.py +1 -46
  21. claude_mpm/core/service_registry.py +0 -8
  22. claude_mpm/core/simple_runner.py +43 -0
  23. claude_mpm/generators/__init__.py +5 -0
  24. claude_mpm/generators/agent_profile_generator.py +137 -0
  25. claude_mpm/hooks/README.md +75 -221
  26. claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
  27. claude_mpm/hooks/claude_hooks/__init__.py +5 -0
  28. claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
  29. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
  30. claude_mpm/hooks/validation_hooks.py +181 -0
  31. claude_mpm/schemas/agent_schema.json +328 -0
  32. claude_mpm/services/agent_management_service.py +4 -4
  33. claude_mpm/services/agent_profile_loader.py +1 -1
  34. claude_mpm/services/agent_registry.py +0 -1
  35. claude_mpm/services/base_agent_manager.py +3 -3
  36. claude_mpm/utils/error_handler.py +247 -0
  37. claude_mpm/validation/__init__.py +5 -0
  38. claude_mpm/validation/agent_validator.py +302 -0
  39. {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/METADATA +133 -22
  40. {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/RECORD +49 -37
  41. claude_mpm/agents/templates/data_engineer_agent.json +0 -46
  42. claude_mpm/agents/templates/update-optimized-specialized-agents.json +0 -374
  43. claude_mpm/config/hook_config.py +0 -42
  44. claude_mpm/hooks/hook_client.py +0 -264
  45. claude_mpm/hooks/hook_runner.py +0 -370
  46. claude_mpm/hooks/json_rpc_executor.py +0 -259
  47. claude_mpm/hooks/json_rpc_hook_client.py +0 -319
  48. claude_mpm/services/hook_service.py +0 -388
  49. claude_mpm/services/hook_service_manager.py +0 -223
  50. claude_mpm/services/json_rpc_hook_manager.py +0 -92
  51. /claude_mpm/agents/templates/{documentation_agent.json → backup/documentation_agent_20250726_234551.json} +0 -0
  52. /claude_mpm/agents/templates/{ops_agent.json → backup/ops_agent_20250726_234551.json} +0 -0
  53. /claude_mpm/agents/templates/{qa_agent.json → backup/qa_agent_20250726_234551.json} +0 -0
  54. /claude_mpm/agents/templates/{research_agent.json → backup/research_agent_20250726_234551.json} +0 -0
  55. /claude_mpm/agents/templates/{security_agent.json → backup/security_agent_20250726_234551.json} +0 -0
  56. /claude_mpm/agents/templates/{version_control_agent.json → backup/version_control_agent_20250726_234551.json} +0 -0
  57. {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/WHEEL +0 -0
  58. {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-1.0.0.dist-info → claude_mpm-2.0.0.dist-info}/top_level.txt +0 -0
@@ -3,35 +3,32 @@
3
3
  Unified Agent Loader System
4
4
  ==========================
5
5
 
6
- Provides unified loading of agent prompts from framework markdown files.
7
- Integrates with SharedPromptCache for performance optimization.
6
+ Provides unified loading of agent prompts from JSON template files using
7
+ the new standardized schema format.
8
8
 
9
9
  Key Features:
10
- - Loads agent prompts from framework/agent-roles/*.md files
10
+ - Loads agent prompts from src/claude_mpm/agents/templates/*.json files
11
11
  - Handles base_agent.md prepending
12
12
  - Provides backward-compatible get_*_agent_prompt() functions
13
13
  - Uses SharedPromptCache for performance
14
- - Special handling for ticketing agent's dynamic CLI help
15
-
16
- For advanced agent management features (CRUD, versioning, section updates), use:
17
- from claude_pm.agents.agent_loader_integration import get_enhanced_loader
18
- from claude_pm.services.agent_management_service import AgentManager
14
+ - Validates agents against schema before loading
19
15
 
20
16
  Usage:
21
17
  from claude_pm.agents.agent_loader import get_documentation_agent_prompt
22
18
 
23
- # Get agent prompt from MD file
19
+ # Get agent prompt from JSON template
24
20
  prompt = get_documentation_agent_prompt()
25
21
  """
26
22
 
23
+ import json
27
24
  import logging
28
25
  import os
29
26
  from pathlib import Path
30
- from typing import Optional, Dict, Any, Tuple, Union
27
+ from typing import Optional, Dict, Any, Tuple, Union, List
31
28
 
32
29
  from ..services.shared_prompt_cache import SharedPromptCache
33
30
  from .base_agent_loader import prepend_base_instructions
34
- # from ..services.task_complexity_analyzer import TaskComplexityAnalyzer, ComplexityLevel, ModelType
31
+ from ..validation.agent_validator import AgentValidator, ValidationResult
35
32
  from ..utils.paths import PathResolver
36
33
 
37
34
  # Temporary placeholders for missing module
@@ -49,65 +46,16 @@ class ModelType:
49
46
  logger = logging.getLogger(__name__)
50
47
 
51
48
 
52
- def _get_framework_agent_roles_dir() -> Path:
53
- """Get the framework agent-roles directory dynamically."""
54
- # Use PathResolver for consistent path discovery
55
- try:
56
- framework_root = PathResolver.get_framework_root()
57
-
58
- # Check if we're running from a wheel installation
59
- try:
60
- import claude_pm
61
- package_path = Path(claude_pm.__file__).parent
62
- path_str = str(package_path.resolve())
63
- if 'site-packages' in path_str or 'dist-packages' in path_str:
64
- # For wheel installations, check data directory
65
- data_agent_roles = package_path / "data" / "framework" / "agent-roles"
66
- if data_agent_roles.exists():
67
- logger.debug(f"Using wheel installation agent-roles: {data_agent_roles}")
68
- return data_agent_roles
69
- except Exception:
70
- pass
71
-
72
- # Check framework structure
73
- agent_roles_dir = framework_root / "framework" / "agent-roles"
74
- if agent_roles_dir.exists():
75
- logger.debug(f"Using framework agent-roles: {agent_roles_dir}")
76
- return agent_roles_dir
77
-
78
- # Try agents directory as fallback
79
- agents_dir = PathResolver.get_agents_dir()
80
- logger.debug(f"Using agents directory: {agents_dir}")
81
- return agents_dir
82
-
83
- except FileNotFoundError as e:
84
- # Ultimate fallback
85
- logger.warning(f"PathResolver could not find framework root: {e}")
86
- fallback = Path(__file__).parent.parent.parent / "framework" / "agent-roles"
87
- logger.warning(f"Using fallback agent-roles path: {fallback}")
88
- return fallback
49
+ def _get_agent_templates_dir() -> Path:
50
+ """Get the agent templates directory."""
51
+ return Path(__file__).parent / "templates"
89
52
 
90
53
 
91
- # Framework agent-roles directory (dynamically determined)
92
- FRAMEWORK_AGENT_ROLES_DIR = _get_framework_agent_roles_dir()
54
+ # Agent templates directory
55
+ AGENT_TEMPLATES_DIR = _get_agent_templates_dir()
93
56
 
94
57
  # Cache prefix for agent prompts
95
- AGENT_CACHE_PREFIX = "agent_prompt:"
96
-
97
- # Agent name mappings (agent name -> MD file name)
98
- AGENT_MAPPINGS = {
99
- "documentation": "documentation-agent.md",
100
- "version_control": "version-control-agent.md",
101
- "qa": "qa-agent.md",
102
- "research": "research-agent.md",
103
- "ops": "ops-agent.md",
104
- "security": "security-agent.md",
105
- "engineer": "engineer-agent.md",
106
- "data_engineer": "data-agent.md", # Note: data-agent.md maps to data_engineer
107
- "pm": "pm-orchestrator-agent.md",
108
- "orchestrator": "pm-orchestrator-agent.md",
109
- "pm_orchestrator": "pm-orchestrator-agent.md"
110
- }
58
+ AGENT_CACHE_PREFIX = "agent_prompt:v2:"
111
59
 
112
60
  # Model configuration thresholds
113
61
  MODEL_THRESHOLDS = {
@@ -116,82 +64,138 @@ MODEL_THRESHOLDS = {
116
64
  ModelType.OPUS: {"min_complexity": 71, "max_complexity": 100}
117
65
  }
118
66
 
119
- # Default model for each agent type (fallback when dynamic selection is disabled)
120
- DEFAULT_AGENT_MODELS = {
121
- 'orchestrator': 'claude-4-opus',
122
- 'pm': 'claude-4-opus',
123
- 'pm_orchestrator': 'claude-4-opus',
124
- 'engineer': 'claude-4-opus',
125
- 'architecture': 'claude-4-opus',
126
- 'documentation': 'claude-sonnet-4-20250514',
127
- 'version_control': 'claude-sonnet-4-20250514',
128
- 'qa': 'claude-sonnet-4-20250514',
129
- 'research': 'claude-sonnet-4-20250514',
130
- 'ops': 'claude-sonnet-4-20250514',
131
- 'security': 'claude-sonnet-4-20250514',
132
- 'data_engineer': 'claude-sonnet-4-20250514'
133
- }
134
-
135
- # Model name mappings for Claude API
67
+ # Model name mappings for Claude API (updated for new schema)
136
68
  MODEL_NAME_MAPPINGS = {
137
69
  ModelType.HAIKU: "claude-3-haiku-20240307",
138
70
  ModelType.SONNET: "claude-sonnet-4-20250514",
139
- ModelType.OPUS: "claude-4-opus"
71
+ ModelType.OPUS: "claude-opus-4-20250514"
140
72
  }
141
73
 
142
74
 
143
- def load_agent_prompt_from_md(agent_name: str, force_reload: bool = False) -> Optional[str]:
144
- """
145
- Load agent prompt from framework markdown file.
75
+ class AgentLoader:
76
+ """Loads and manages agent templates with schema validation."""
146
77
 
147
- Args:
148
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
149
- force_reload: Force reload from file, bypassing cache
78
+ def __init__(self):
79
+ """Initialize the agent loader."""
80
+ self.validator = AgentValidator()
81
+ self.cache = SharedPromptCache.get_instance()
82
+ self._agent_registry: Dict[str, Dict[str, Any]] = {}
83
+ self._load_agents()
84
+
85
+ def _load_agents(self) -> None:
86
+ """Load all valid agents from the templates directory."""
87
+ logger.info(f"Loading agents from {AGENT_TEMPLATES_DIR}")
150
88
 
151
- Returns:
152
- str: Agent prompt content from MD file, or None if not found
153
- """
154
- try:
155
- # Get cache instance
156
- cache = SharedPromptCache.get_instance()
157
- cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:md"
89
+ for json_file in AGENT_TEMPLATES_DIR.glob("*.json"):
90
+ if json_file.name == "agent_schema.json":
91
+ continue
92
+
93
+ try:
94
+ with open(json_file, 'r') as f:
95
+ agent_data = json.load(f)
96
+
97
+ # Validate against schema
98
+ validation_result = self.validator.validate_agent(agent_data)
99
+
100
+ if validation_result.is_valid:
101
+ agent_id = agent_data.get("id")
102
+ if agent_id:
103
+ self._agent_registry[agent_id] = agent_data
104
+ logger.debug(f"Loaded agent: {agent_id}")
105
+ else:
106
+ logger.warning(f"Invalid agent in {json_file.name}: {validation_result.errors}")
107
+
108
+ except Exception as e:
109
+ logger.error(f"Failed to load {json_file.name}: {e}")
110
+
111
+ def get_agent(self, agent_id: str) -> Optional[Dict[str, Any]]:
112
+ """Get agent data by ID."""
113
+ return self._agent_registry.get(agent_id)
114
+
115
+ def list_agents(self) -> List[Dict[str, Any]]:
116
+ """List all available agents."""
117
+ agents = []
118
+ for agent_id, agent_data in self._agent_registry.items():
119
+ agents.append({
120
+ "id": agent_id,
121
+ "name": agent_data.get("metadata", {}).get("name", agent_id),
122
+ "description": agent_data.get("metadata", {}).get("description", ""),
123
+ "category": agent_data.get("metadata", {}).get("category", ""),
124
+ "model": agent_data.get("capabilities", {}).get("model", ""),
125
+ "resource_tier": agent_data.get("capabilities", {}).get("resource_tier", "")
126
+ })
127
+ return sorted(agents, key=lambda x: x["id"])
128
+
129
+ def get_agent_prompt(self, agent_id: str, force_reload: bool = False) -> Optional[str]:
130
+ """Get agent instructions by ID."""
131
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_id}"
158
132
 
159
- # Check cache first (unless force reload)
133
+ # Check cache first
160
134
  if not force_reload:
161
- cached_content = cache.get(cache_key)
135
+ cached_content = self.cache.get(cache_key)
162
136
  if cached_content is not None:
163
- logger.debug(f"Agent prompt for '{agent_name}' loaded from cache")
137
+ logger.debug(f"Agent prompt for '{agent_id}' loaded from cache")
164
138
  return str(cached_content)
165
139
 
166
- # Get MD file path
167
- md_filename = AGENT_MAPPINGS.get(agent_name)
168
- if not md_filename:
169
- logger.warning(f"No MD file mapping found for agent: {agent_name}")
140
+ # Get agent data
141
+ agent_data = self.get_agent(agent_id)
142
+ if not agent_data:
143
+ logger.warning(f"Agent not found: {agent_id}")
170
144
  return None
171
145
 
172
- # Always get fresh framework directory path to ensure we're using the right location
173
- framework_agent_roles_dir = _get_framework_agent_roles_dir()
174
- md_path = framework_agent_roles_dir / md_filename
175
-
176
- # Check if file exists
177
- if not md_path.exists():
178
- logger.warning(f"Agent MD file not found: {md_path}")
146
+ # Extract instructions
147
+ instructions = agent_data.get("instructions", "")
148
+ if not instructions:
149
+ logger.warning(f"No instructions found for agent: {agent_id}")
179
150
  return None
180
-
181
- logger.debug(f"Loading agent prompt from: {md_path}")
182
- content = md_path.read_text(encoding='utf-8')
183
151
 
184
152
  # Cache the content with 1 hour TTL
185
- cache.set(cache_key, content, ttl=3600)
186
- logger.debug(f"Agent prompt for '{agent_name}' cached successfully")
153
+ self.cache.set(cache_key, instructions, ttl=3600)
154
+ logger.debug(f"Agent prompt for '{agent_id}' cached successfully")
187
155
 
188
- return content
156
+ return instructions
157
+
158
+ def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]:
159
+ """Get agent metadata including capabilities and configuration."""
160
+ agent_data = self.get_agent(agent_id)
161
+ if not agent_data:
162
+ return None
189
163
 
190
- except Exception as e:
191
- logger.error(f"Error loading agent prompt from MD for '{agent_name}': {e}")
192
- return None
164
+ return {
165
+ "id": agent_id,
166
+ "version": agent_data.get("version", "1.0.0"),
167
+ "metadata": agent_data.get("metadata", {}),
168
+ "capabilities": agent_data.get("capabilities", {}),
169
+ "knowledge": agent_data.get("knowledge", {}),
170
+ "interactions": agent_data.get("interactions", {})
171
+ }
193
172
 
194
173
 
174
+ # Global loader instance
175
+ _loader: Optional[AgentLoader] = None
176
+
177
+
178
+ def _get_loader() -> AgentLoader:
179
+ """Get or create the global agent loader instance."""
180
+ global _loader
181
+ if _loader is None:
182
+ _loader = AgentLoader()
183
+ return _loader
184
+
185
+
186
+ def load_agent_prompt_from_md(agent_name: str, force_reload: bool = False) -> Optional[str]:
187
+ """
188
+ Load agent prompt from new schema JSON template.
189
+
190
+ Args:
191
+ agent_name: Agent name (matches agent ID in new schema)
192
+ force_reload: Force reload from file, bypassing cache
193
+
194
+ Returns:
195
+ str: Agent instructions from JSON template, or None if not found
196
+ """
197
+ loader = _get_loader()
198
+ return loader.get_agent_prompt(agent_name, force_reload)
195
199
 
196
200
 
197
201
  def _analyze_task_complexity(task_description: str, context_size: int = 0, **kwargs: Any) -> Dict[str, Any]:
@@ -228,13 +232,19 @@ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, A
228
232
  Returns:
229
233
  Tuple of (selected_model, model_config)
230
234
  """
235
+ loader = _get_loader()
236
+ agent_data = loader.get_agent(agent_name)
237
+
238
+ if not agent_data:
239
+ # Fallback for unknown agents
240
+ return "claude-sonnet-4-20250514", {"selection_method": "default"}
241
+
242
+ # Get model from agent capabilities
243
+ default_model = agent_data.get("capabilities", {}).get("model", "claude-sonnet-4-20250514")
244
+
231
245
  # Check if dynamic model selection is enabled
232
246
  enable_dynamic_selection = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
233
247
 
234
- # Debug logging
235
- logger.debug(f"Environment ENABLE_DYNAMIC_MODEL_SELECTION: {os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION')}")
236
- logger.debug(f"Enable dynamic selection: {enable_dynamic_selection}")
237
-
238
248
  # Check for per-agent override in environment
239
249
  agent_override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
240
250
  agent_override = os.getenv(agent_override_key, '').lower()
@@ -244,44 +254,24 @@ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, A
244
254
  elif agent_override == 'false':
245
255
  enable_dynamic_selection = False
246
256
 
247
- # Log model selection decision
248
- logger.info(f"Model selection for {agent_name}: dynamic={enable_dynamic_selection}, "
249
- f"complexity_available={complexity_analysis is not None}")
250
-
251
257
  # Dynamic model selection based on complexity
252
258
  if enable_dynamic_selection and complexity_analysis:
253
259
  recommended_model = complexity_analysis.get('recommended_model', ModelType.SONNET)
254
- selected_model = MODEL_NAME_MAPPINGS.get(recommended_model, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'))
260
+ selected_model = MODEL_NAME_MAPPINGS.get(recommended_model, default_model)
255
261
 
256
262
  model_config = {
257
263
  "selection_method": "dynamic_complexity_based",
258
264
  "complexity_score": complexity_analysis.get('complexity_score', 50),
259
265
  "complexity_level": complexity_analysis.get('complexity_level', ComplexityLevel.MEDIUM).value,
260
266
  "optimal_prompt_size": complexity_analysis.get('optimal_prompt_size', (700, 1000)),
261
- "scoring_breakdown": complexity_analysis.get('scoring_breakdown', {}),
262
- "analysis_details": complexity_analysis.get('analysis_details', {})
267
+ "default_model": default_model
263
268
  }
264
-
265
- # Log metrics
266
- logger.info(f"Dynamic model selection for {agent_name}: "
267
- f"model={selected_model}, "
268
- f"complexity_score={model_config['complexity_score']}, "
269
- f"complexity_level={model_config['complexity_level']}")
270
-
271
- # Track model selection metrics
272
- log_model_selection(
273
- agent_name=agent_name,
274
- selected_model=selected_model,
275
- complexity_score=model_config['complexity_score'],
276
- selection_method=model_config['selection_method']
277
- )
278
-
279
269
  else:
280
- # Use default model mapping
281
- selected_model = DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
270
+ selected_model = default_model
282
271
  model_config = {
283
- "selection_method": "default_mapping",
284
- "reason": "dynamic_selection_disabled" if not enable_dynamic_selection else "no_complexity_analysis"
272
+ "selection_method": "agent_default",
273
+ "reason": "dynamic_selection_disabled" if not enable_dynamic_selection else "no_complexity_analysis",
274
+ "default_model": default_model
285
275
  }
286
276
 
287
277
  return selected_model, model_config
@@ -289,27 +279,26 @@ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, A
289
279
 
290
280
  def get_agent_prompt(agent_name: str, force_reload: bool = False, return_model_info: bool = False, **kwargs: Any) -> Union[str, Tuple[str, str, Dict[str, Any]]]:
291
281
  """
292
- Get agent prompt from MD file with optional dynamic model selection.
282
+ Get agent prompt from JSON template with optional dynamic model selection.
293
283
 
294
284
  Args:
295
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
285
+ agent_name: Agent name (agent ID in new schema)
296
286
  force_reload: Force reload from source, bypassing cache
297
287
  return_model_info: If True, returns tuple (prompt, model, config)
298
288
  **kwargs: Additional arguments including:
299
289
  - task_description: Description of the task for complexity analysis
300
290
  - context_size: Size of context for complexity analysis
301
291
  - enable_complexity_analysis: Override for complexity analysis
302
- - Additional complexity factors (file_count, integration_points, etc.)
303
292
 
304
293
  Returns:
305
294
  str or tuple: Complete agent prompt with base instructions prepended,
306
295
  or tuple of (prompt, selected_model, model_config) if return_model_info=True
307
296
  """
308
- # Load from MD file
297
+ # Load from new schema JSON template
309
298
  prompt = load_agent_prompt_from_md(agent_name, force_reload)
310
299
 
311
300
  if prompt is None:
312
- raise ValueError(f"No agent prompt MD file found for: {agent_name}")
301
+ raise ValueError(f"No agent found with ID: {agent_name}")
313
302
 
314
303
  # Analyze task complexity if task description is provided
315
304
  complexity_analysis = None
@@ -317,38 +306,15 @@ def get_agent_prompt(agent_name: str, force_reload: bool = False, return_model_i
317
306
  enable_analysis = kwargs.get('enable_complexity_analysis', True)
318
307
 
319
308
  if task_description and enable_analysis:
320
- # Remove already specified parameters from kwargs to avoid duplicates
321
- analysis_kwargs = {k: v for k, v in kwargs.items()
322
- if k not in ['task_description', 'context_size']}
323
309
  complexity_analysis = _analyze_task_complexity(
324
310
  task_description=task_description,
325
311
  context_size=kwargs.get('context_size', 0),
326
- **analysis_kwargs
312
+ **{k: v for k, v in kwargs.items() if k not in ['task_description', 'context_size']}
327
313
  )
328
314
 
329
- # Get model configuration (always happens, even without complexity analysis)
315
+ # Get model configuration
330
316
  selected_model, model_config = _get_model_config(agent_name, complexity_analysis)
331
317
 
332
- # Always store model selection info in kwargs for potential use by callers
333
- kwargs['_selected_model'] = selected_model
334
- kwargs['_model_config'] = model_config
335
-
336
- # Handle dynamic template formatting if needed
337
- if "{dynamic_help}" in prompt:
338
- try:
339
- # Import CLI helper module to get dynamic help
340
- from ..orchestration.ai_trackdown_tools import CLIHelpFormatter
341
-
342
- # Create a CLI helper instance
343
- cli_helper = CLIHelpFormatter()
344
- help_content, _ = cli_helper.get_cli_help()
345
- dynamic_help = cli_helper.format_help_for_prompt(help_content)
346
- prompt = prompt.format(dynamic_help=dynamic_help)
347
- except Exception as e:
348
- logger.warning(f"Could not format dynamic help for ticketing agent: {e}")
349
- # Remove the placeholder if we can't fill it
350
- prompt = prompt.replace("{dynamic_help}", "")
351
-
352
318
  # Add model selection metadata to prompt if dynamic selection is enabled
353
319
  if selected_model and model_config.get('selection_method') == 'dynamic_complexity_based':
354
320
  model_metadata = f"\n<!-- Model Selection: {selected_model} (Complexity: {model_config.get('complexity_level', 'UNKNOWN')}) -->\n"
@@ -373,7 +339,6 @@ def get_documentation_agent_prompt() -> str:
373
339
  return prompt
374
340
 
375
341
 
376
-
377
342
  def get_version_control_agent_prompt() -> str:
378
343
  """Get the complete Version Control Agent prompt with base instructions."""
379
344
  prompt = get_agent_prompt("version_control", return_model_info=False)
@@ -428,46 +393,54 @@ def get_agent_prompt_with_model_info(agent_name: str, force_reload: bool = False
428
393
  Get agent prompt with model selection information.
429
394
 
430
395
  Args:
431
- agent_name: Agent name (e.g., 'documentation', 'ticketing')
396
+ agent_name: Agent name (agent ID)
432
397
  force_reload: Force reload from source, bypassing cache
433
398
  **kwargs: Additional arguments for prompt generation and model selection
434
399
 
435
400
  Returns:
436
401
  Tuple of (prompt, selected_model, model_config)
437
402
  """
438
- # Use get_agent_prompt with return_model_info=True
439
403
  result = get_agent_prompt(agent_name, force_reload, return_model_info=True, **kwargs)
440
404
 
441
- # If result is a tuple, return it directly
405
+ # Ensure we have a tuple
442
406
  if isinstance(result, tuple):
443
407
  return result
444
408
 
445
409
  # Fallback (shouldn't happen)
446
- return result, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'), {"selection_method": "default"}
410
+ loader = _get_loader()
411
+ agent_data = loader.get_agent(agent_name)
412
+ default_model = "claude-sonnet-4-20250514"
413
+ if agent_data:
414
+ default_model = agent_data.get("capabilities", {}).get("model", default_model)
415
+
416
+ return result, default_model, {"selection_method": "default"}
447
417
 
448
418
 
449
419
  # Utility functions
450
420
  def list_available_agents() -> Dict[str, Dict[str, Any]]:
451
421
  """
452
- List all available agents with their sources.
422
+ List all available agents with their metadata.
453
423
 
454
424
  Returns:
455
- dict: Agent information including MD file path
425
+ dict: Agent information including capabilities and metadata
456
426
  """
427
+ loader = _get_loader()
457
428
  agents = {}
458
429
 
459
- # Get fresh framework directory path
460
- framework_agent_roles_dir = _get_framework_agent_roles_dir()
461
-
462
- for agent_name, md_filename in AGENT_MAPPINGS.items():
463
- md_path = framework_agent_roles_dir / md_filename
430
+ for agent_info in loader.list_agents():
431
+ agent_id = agent_info["id"]
432
+ metadata = loader.get_agent_metadata(agent_id)
464
433
 
465
- agents[agent_name] = {
466
- "md_file": md_filename if md_path.exists() else None,
467
- "md_path": str(md_path) if md_path.exists() else None,
468
- "has_md": md_path.exists(),
469
- "default_model": DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
470
- }
434
+ if metadata:
435
+ agents[agent_id] = {
436
+ "name": metadata["metadata"].get("name", agent_id),
437
+ "description": metadata["metadata"].get("description", ""),
438
+ "category": metadata["metadata"].get("category", ""),
439
+ "version": metadata["version"],
440
+ "model": metadata["capabilities"].get("model", ""),
441
+ "resource_tier": metadata["capabilities"].get("resource_tier", ""),
442
+ "tools": metadata["capabilities"].get("tools", [])
443
+ }
471
444
 
472
445
  return agents
473
446
 
@@ -483,13 +456,14 @@ def clear_agent_cache(agent_name: Optional[str] = None) -> None:
483
456
  cache = SharedPromptCache.get_instance()
484
457
 
485
458
  if agent_name:
486
- cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:md"
459
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}"
487
460
  cache.invalidate(cache_key)
488
461
  logger.debug(f"Cache cleared for agent: {agent_name}")
489
462
  else:
490
463
  # Clear all agent caches
491
- for name in AGENT_MAPPINGS:
492
- cache_key = f"{AGENT_CACHE_PREFIX}{name}:md"
464
+ loader = _get_loader()
465
+ for agent_id in loader._agent_registry.keys():
466
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_id}"
493
467
  cache.invalidate(cache_key)
494
468
  logger.debug("All agent caches cleared")
495
469
 
@@ -499,123 +473,31 @@ def clear_agent_cache(agent_name: Optional[str] = None) -> None:
499
473
 
500
474
  def validate_agent_files() -> Dict[str, Dict[str, Any]]:
501
475
  """
502
- Validate that all expected agent files exist.
476
+ Validate all agent files in the templates directory.
503
477
 
504
478
  Returns:
505
479
  dict: Validation results for each agent
506
480
  """
481
+ validator = AgentValidator()
507
482
  results = {}
508
483
 
509
- # Get fresh framework directory path
510
- framework_agent_roles_dir = _get_framework_agent_roles_dir()
511
-
512
- for agent_name, md_filename in AGENT_MAPPINGS.items():
513
- md_path = framework_agent_roles_dir / md_filename
514
- results[agent_name] = {
515
- "md_exists": md_path.exists(),
516
- "md_path": str(md_path)
484
+ for json_file in AGENT_TEMPLATES_DIR.glob("*.json"):
485
+ if json_file.name == "agent_schema.json":
486
+ continue
487
+
488
+ validation_result = validator.validate_file(json_file)
489
+ results[json_file.stem] = {
490
+ "valid": validation_result.is_valid,
491
+ "errors": validation_result.errors,
492
+ "warnings": validation_result.warnings,
493
+ "file_path": str(json_file)
517
494
  }
518
495
 
519
496
  return results
520
497
 
521
498
 
522
- def get_model_selection_metrics() -> Dict[str, Any]:
523
- """
524
- Get metrics about model selection usage.
525
-
526
- Returns:
527
- dict: Metrics including feature flag status and selection counts
528
- """
529
- # Check feature flag status
530
- global_enabled = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
531
-
532
- # Check per-agent overrides
533
- agent_overrides = {}
534
- for agent_name in AGENT_MAPPINGS.keys():
535
- override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
536
- override_value = os.getenv(override_key, '')
537
- if override_value:
538
- agent_overrides[agent_name] = override_value.lower() == 'true'
539
-
540
- # Get cache instance to check for cached metrics
541
- try:
542
- cache = SharedPromptCache.get_instance()
543
- selection_stats = cache.get("agent_loader:model_selection_stats") or {}
544
- except Exception:
545
- selection_stats = {}
546
-
547
- return {
548
- "feature_flag": {
549
- "global_enabled": global_enabled,
550
- "agent_overrides": agent_overrides
551
- },
552
- "model_thresholds": {
553
- model_type.value: thresholds
554
- for model_type, thresholds in MODEL_THRESHOLDS.items()
555
- },
556
- "default_models": DEFAULT_AGENT_MODELS,
557
- "selection_stats": selection_stats
558
- }
559
-
560
-
561
- def log_model_selection(agent_name: str, selected_model: str, complexity_score: int, selection_method: str) -> None:
562
- """
563
- Log model selection for metrics tracking.
564
-
565
- Args:
566
- agent_name: Name of the agent
567
- selected_model: Model that was selected
568
- complexity_score: Complexity score from analysis
569
- selection_method: Method used for selection
570
- """
571
- try:
572
- # Get cache instance
573
- cache = SharedPromptCache.get_instance()
574
-
575
- # Get existing stats
576
- stats_key = "agent_loader:model_selection_stats"
577
- stats = cache.get(stats_key) or {
578
- "total_selections": 0,
579
- "by_model": {},
580
- "by_agent": {},
581
- "by_method": {},
582
- "complexity_distribution": {
583
- "0-30": 0,
584
- "31-70": 0,
585
- "71-100": 0
586
- }
587
- }
588
-
589
- # Update stats
590
- stats["total_selections"] += 1
591
-
592
- # By model
593
- if selected_model not in stats["by_model"]:
594
- stats["by_model"][selected_model] = 0
595
- stats["by_model"][selected_model] += 1
596
-
597
- # By agent
598
- if agent_name not in stats["by_agent"]:
599
- stats["by_agent"][agent_name] = {}
600
- if selected_model not in stats["by_agent"][agent_name]:
601
- stats["by_agent"][agent_name][selected_model] = 0
602
- stats["by_agent"][agent_name][selected_model] += 1
603
-
604
- # By method
605
- if selection_method not in stats["by_method"]:
606
- stats["by_method"][selection_method] = 0
607
- stats["by_method"][selection_method] += 1
608
-
609
- # Complexity distribution
610
- if complexity_score <= 30:
611
- stats["complexity_distribution"]["0-30"] += 1
612
- elif complexity_score <= 70:
613
- stats["complexity_distribution"]["31-70"] += 1
614
- else:
615
- stats["complexity_distribution"]["71-100"] += 1
616
-
617
- # Store updated stats with 24 hour TTL
618
- cache.set(stats_key, stats, ttl=86400)
619
-
620
- except Exception as e:
621
- logger.warning(f"Failed to log model selection metrics: {e}")
499
+ def reload_agents() -> None:
500
+ """Force reload all agents from disk."""
501
+ global _loader
502
+ _loader = None
503
+ logger.info("Agent registry cleared, will reload on next access")