claude-mpm 1.0.0__py3-none-any.whl → 1.1.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 (38) hide show
  1. claude_mpm/_version.py +3 -2
  2. claude_mpm/agents/__init__.py +2 -2
  3. claude_mpm/agents/agent-template.yaml +83 -0
  4. claude_mpm/agents/agent_loader.py +66 -90
  5. claude_mpm/agents/base_agent_loader.py +10 -15
  6. claude_mpm/cli.py +41 -47
  7. claude_mpm/cli_enhancements.py +297 -0
  8. claude_mpm/core/factories.py +1 -46
  9. claude_mpm/core/service_registry.py +0 -8
  10. claude_mpm/core/simple_runner.py +43 -0
  11. claude_mpm/generators/__init__.py +5 -0
  12. claude_mpm/generators/agent_profile_generator.py +137 -0
  13. claude_mpm/hooks/README.md +75 -221
  14. claude_mpm/hooks/builtin/mpm_command_hook.py +125 -0
  15. claude_mpm/hooks/claude_hooks/__init__.py +5 -0
  16. claude_mpm/hooks/claude_hooks/hook_handler.py +399 -0
  17. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +47 -0
  18. claude_mpm/hooks/validation_hooks.py +181 -0
  19. claude_mpm/services/agent_management_service.py +4 -4
  20. claude_mpm/services/agent_profile_loader.py +1 -1
  21. claude_mpm/services/agent_registry.py +0 -1
  22. claude_mpm/services/base_agent_manager.py +3 -3
  23. claude_mpm/utils/error_handler.py +247 -0
  24. claude_mpm/validation/__init__.py +5 -0
  25. claude_mpm/validation/agent_validator.py +175 -0
  26. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/METADATA +44 -7
  27. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/RECORD +30 -26
  28. claude_mpm/config/hook_config.py +0 -42
  29. claude_mpm/hooks/hook_client.py +0 -264
  30. claude_mpm/hooks/hook_runner.py +0 -370
  31. claude_mpm/hooks/json_rpc_executor.py +0 -259
  32. claude_mpm/hooks/json_rpc_hook_client.py +0 -319
  33. claude_mpm/services/hook_service.py +0 -388
  34. claude_mpm/services/hook_service_manager.py +0 -223
  35. claude_mpm/services/json_rpc_hook_manager.py +0 -92
  36. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/WHEEL +0 -0
  37. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/entry_points.txt +0 -0
  38. {claude_mpm-1.0.0.dist-info → claude_mpm-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env python3
2
+ """Unified hook handler for Claude Code integration.
3
+
4
+ This script is called by hook_wrapper.sh, which is the shell script
5
+ that gets installed in ~/.claude/settings.json. The wrapper handles
6
+ environment setup and then executes this Python handler.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ import os
12
+ import re
13
+ import logging
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ # Add src to path for imports
18
+ project_root = Path(__file__).parent.parent.parent.parent
19
+ sys.path.insert(0, str(project_root / 'src'))
20
+
21
+ from claude_mpm.core.logger import get_logger, setup_logging, LogLevel
22
+
23
+ # Don't initialize global logging here - we'll do it per-project
24
+ logger = None
25
+
26
+
27
+ class ClaudeHookHandler:
28
+ """Handler for all Claude Code hook events."""
29
+
30
+ def __init__(self):
31
+ self.event = None
32
+ self.hook_type = None
33
+
34
+ # Available MPM arguments
35
+ self.mpm_args = {
36
+ 'status': 'Show claude-mpm system status',
37
+ # Add more arguments here as they're implemented
38
+ # 'config': 'Configure claude-mpm settings',
39
+ # 'debug': 'Toggle debug mode',
40
+ }
41
+
42
+ def handle(self):
43
+ """Main entry point for hook handling."""
44
+ global logger
45
+ try:
46
+ # Quick debug log to file
47
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
48
+ f.write(f"[{datetime.now().isoformat()}] Hook called\n")
49
+
50
+ # Read event from stdin
51
+ event_data = sys.stdin.read()
52
+ self.event = json.loads(event_data)
53
+ self.hook_type = self.event.get('hook_event_name', 'unknown')
54
+
55
+ # Get the working directory from the event
56
+ cwd = self.event.get('cwd', os.getcwd())
57
+ project_dir = Path(cwd)
58
+
59
+ # Initialize project-specific logging
60
+ log_dir = project_dir / '.claude-mpm' / 'logs'
61
+ log_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ # Set up logging for this specific project
64
+ # Use a single log file per day per project
65
+ log_level = os.environ.get('CLAUDE_MPM_LOG_LEVEL', 'INFO')
66
+ log_file = log_dir / f"hooks_{datetime.now().strftime('%Y%m%d')}.log"
67
+
68
+ # Only set up logging if we haven't already for this project
69
+ logger_name = f"claude_mpm_hooks_{project_dir.name}"
70
+ if not logging.getLogger(logger_name).handlers:
71
+ logger = setup_logging(
72
+ name=logger_name,
73
+ level=log_level,
74
+ log_dir=log_dir,
75
+ log_file=log_file
76
+ )
77
+ else:
78
+ logger = logging.getLogger(logger_name)
79
+
80
+ # Log more details about the hook type
81
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
82
+ f.write(f"[{datetime.now().isoformat()}] Hook type: {self.hook_type}\n")
83
+ f.write(f"[{datetime.now().isoformat()}] Project: {project_dir}\n")
84
+
85
+ # Log the prompt if it's UserPromptSubmit
86
+ if self.hook_type == 'UserPromptSubmit':
87
+ prompt = self.event.get('prompt', '')
88
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
89
+ f.write(f"[{datetime.now().isoformat()}] Prompt: {prompt}\n")
90
+
91
+ # Log the event if DEBUG logging is enabled
92
+ self._log_event()
93
+
94
+ # Route to appropriate handler
95
+ if self.hook_type == 'UserPromptSubmit':
96
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
97
+ f.write(f"[{datetime.now().isoformat()}] About to call _handle_user_prompt_submit\n")
98
+ return self._handle_user_prompt_submit()
99
+ elif self.hook_type == 'PreToolUse':
100
+ return self._handle_pre_tool_use()
101
+ elif self.hook_type == 'PostToolUse':
102
+ return self._handle_post_tool_use()
103
+ elif self.hook_type == 'Stop':
104
+ return self._handle_stop()
105
+ elif self.hook_type == 'SubagentStop':
106
+ return self._handle_subagent_stop()
107
+ else:
108
+ logger.debug(f"Unknown hook type: {self.hook_type}")
109
+ return self._continue()
110
+
111
+ except Exception as e:
112
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
113
+ f.write(f"[{datetime.now().isoformat()}] Hook handler error: {e}\n")
114
+ import traceback
115
+ f.write(traceback.format_exc())
116
+ if logger:
117
+ logger.error(f"Hook handler error: {e}")
118
+ return self._continue()
119
+
120
+ def _log_event(self):
121
+ """Log the event details if DEBUG logging is enabled."""
122
+ global logger
123
+ if not logger:
124
+ return
125
+
126
+ # Check if DEBUG logging is enabled
127
+ # logger.level might be an int or LogLevel enum
128
+ try:
129
+ if hasattr(logger.level, 'value'):
130
+ debug_enabled = logger.level.value <= LogLevel.DEBUG.value
131
+ else:
132
+ # It's an int, compare with the DEBUG level value (10)
133
+ debug_enabled = logger.level <= 10
134
+ except:
135
+ # If comparison fails, assume debug is disabled
136
+ debug_enabled = False
137
+
138
+ # Always log hook events at INFO level so they appear in the logs
139
+ session_id = self.event.get('session_id', 'unknown')
140
+ cwd = self.event.get('cwd', 'unknown')
141
+
142
+ logger.info(f"Claude Code hook event: {self.hook_type} (session: {session_id[:8] if session_id != 'unknown' else 'unknown'})")
143
+
144
+ if debug_enabled:
145
+ logger.debug(f"Event in directory: {cwd}")
146
+ logger.debug(f"Event data: {json.dumps(self.event, indent=2)}")
147
+
148
+ # Log specific details based on hook type
149
+ if self.hook_type == 'UserPromptSubmit':
150
+ prompt = self.event.get('prompt', '')
151
+ # Don't log full agent system prompts
152
+ if prompt.startswith('You are Claude Code running in Claude MPM'):
153
+ logger.info("UserPromptSubmit: System prompt for agent delegation")
154
+ else:
155
+ logger.info(f"UserPromptSubmit: {prompt[:100]}..." if len(prompt) > 100 else f"UserPromptSubmit: {prompt}")
156
+ elif self.hook_type == 'PreToolUse':
157
+ tool_name = self.event.get('tool_name', '')
158
+ logger.info(f"PreToolUse: {tool_name}")
159
+ if debug_enabled:
160
+ tool_input = self.event.get('tool_input', {})
161
+ logger.debug(f"Tool input: {json.dumps(tool_input, indent=2)}")
162
+ elif self.hook_type == 'PostToolUse':
163
+ tool_name = self.event.get('tool_name', '')
164
+ exit_code = self.event.get('exit_code', 'N/A')
165
+ logger.info(f"PostToolUse: {tool_name} (exit code: {exit_code})")
166
+ if debug_enabled:
167
+ tool_output = self.event.get('tool_output', '')
168
+ logger.debug(f"Tool output: {tool_output[:200]}..." if len(str(tool_output)) > 200 else f"Tool output: {tool_output}")
169
+ elif self.hook_type == 'Stop':
170
+ reason = self.event.get('reason', 'unknown')
171
+ timestamp = datetime.now().isoformat()
172
+ logger.info(f"Stop event: reason={reason} at {timestamp}")
173
+ elif self.hook_type == 'SubagentStop':
174
+ agent_type = self.event.get('agent_type', 'unknown')
175
+ agent_id = self.event.get('agent_id', 'unknown')
176
+ reason = self.event.get('reason', 'unknown')
177
+ timestamp = datetime.now().isoformat()
178
+ logger.info(f"SubagentStop: agent_type={agent_type}, agent_id={agent_id}, reason={reason} at {timestamp}")
179
+
180
+ def _handle_user_prompt_submit(self):
181
+ """Handle UserPromptSubmit events."""
182
+ try:
183
+ prompt = self.event.get('prompt', '').strip()
184
+
185
+ # Debug log
186
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
187
+ f.write(f"[{datetime.now().isoformat()}] UserPromptSubmit - Checking prompt: '{prompt}'\n")
188
+
189
+ # Check if this is the /mpm command
190
+ if prompt == '/mpm' or prompt.startswith('/mpm '):
191
+ # Parse arguments
192
+ parts = prompt.split(maxsplit=1)
193
+ arg = parts[1] if len(parts) > 1 else ''
194
+
195
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
196
+ f.write(f"[{datetime.now().isoformat()}] MPM command detected, arg: '{arg}'\n")
197
+
198
+ # Route based on argument
199
+ if arg == 'status' or arg.startswith('status '):
200
+ # Extract status args if any
201
+ status_args = arg[6:].strip() if arg.startswith('status ') else ''
202
+ return self._handle_mpm_status(status_args)
203
+ else:
204
+ # Show help for empty or unknown argument
205
+ return self._handle_mpm_help(arg)
206
+
207
+ except Exception as e:
208
+ with open('/tmp/claude-mpm-hook.log', 'a') as f:
209
+ f.write(f"[{datetime.now().isoformat()}] Error in _handle_user_prompt_submit: {e}\n")
210
+ import traceback
211
+ f.write(traceback.format_exc())
212
+
213
+ # For now, let everything else pass through
214
+ return self._continue()
215
+
216
+ def _handle_pre_tool_use(self):
217
+ """Handle PreToolUse events."""
218
+ # For now, just log and continue
219
+ return self._continue()
220
+
221
+ def _handle_post_tool_use(self):
222
+ """Handle PostToolUse events."""
223
+ # For now, just log and continue
224
+ return self._continue()
225
+
226
+ def _handle_stop(self):
227
+ """Handle Stop events."""
228
+ # Log the stop event and continue
229
+ return self._continue()
230
+
231
+ def _handle_subagent_stop(self):
232
+ """Handle SubagentStop events."""
233
+ # Log the subagent stop event and continue
234
+ return self._continue()
235
+
236
+ def _handle_mpm_status(self, args=None):
237
+ """Handle the /mpm:status command."""
238
+ # Parse arguments if provided
239
+ verbose = False
240
+ if args:
241
+ verbose = '--verbose' in args or '-v' in args
242
+
243
+ # Gather system information
244
+ # Handle logger.level which might be int or LogLevel enum
245
+ if hasattr(logger.level, 'name'):
246
+ log_level_name = logger.level.name
247
+ else:
248
+ # It's an int, map it to name
249
+ level_map = {
250
+ 0: 'NOTSET',
251
+ 10: 'DEBUG',
252
+ 20: 'INFO',
253
+ 30: 'WARNING',
254
+ 40: 'ERROR',
255
+ 50: 'CRITICAL'
256
+ }
257
+ log_level_name = level_map.get(logger.level, f"CUSTOM({logger.level})")
258
+
259
+ status_info = {
260
+ 'claude_mpm_version': self._get_version(),
261
+ 'python_version': sys.version.split()[0],
262
+ 'project_root': str(project_root) if project_root.name != 'src' else str(project_root.parent),
263
+ 'logging_level': log_level_name,
264
+ 'hook_handler': 'claude_mpm.hooks.claude_hooks.hook_handler',
265
+ 'environment': {
266
+ 'CLAUDE_PROJECT_DIR': os.environ.get('CLAUDE_PROJECT_DIR', 'not set'),
267
+ 'PYTHONPATH': os.environ.get('PYTHONPATH', 'not set'),
268
+ }
269
+ }
270
+
271
+ # Add verbose information if requested
272
+ if verbose:
273
+ status_info['hooks_configured'] = {
274
+ 'UserPromptSubmit': 'Active',
275
+ 'PreToolUse': 'Active',
276
+ 'PostToolUse': 'Active'
277
+ }
278
+ status_info['available_arguments'] = list(self.mpm_args.keys())
279
+
280
+ # Format output
281
+ output = self._format_status_output(status_info, verbose)
282
+
283
+ # Block LLM processing and return our output
284
+ print(output, file=sys.stderr)
285
+ sys.exit(2)
286
+
287
+ def _get_version(self):
288
+ """Get claude-mpm version."""
289
+ try:
290
+ # First try to read from VERSION file in project root
291
+ version_file = project_root.parent / 'VERSION'
292
+ if not version_file.exists():
293
+ # Try one more level up
294
+ version_file = project_root.parent.parent / 'VERSION'
295
+
296
+ if version_file.exists():
297
+ with open(version_file, 'r') as f:
298
+ version = f.read().strip()
299
+ # Return just the base version for cleaner display
300
+ # e.g., "1.0.2.dev1+g4ecadd4.d20250726" -> "1.0.2.dev1"
301
+ if '+' in version:
302
+ version = version.split('+')[0]
303
+ return version
304
+ except Exception:
305
+ pass
306
+
307
+ try:
308
+ # Fallback to trying import
309
+ from claude_mpm import __version__
310
+ return __version__
311
+ except:
312
+ pass
313
+
314
+ return 'unknown'
315
+
316
+ def _format_status_output(self, info, verbose=False):
317
+ """Format status information for display."""
318
+ # Use same colors as help screen
319
+ CYAN = '\033[96m' # Bright cyan
320
+ GREEN = '\033[92m' # Green (works in help)
321
+ BOLD = '\033[1m'
322
+ RESET = '\033[0m'
323
+ DIM = '\033[2m'
324
+
325
+ output = f"\n{DIM}{'─' * 60}{RESET}\n"
326
+ output += f"{CYAN}{BOLD}🔧 Claude MPM Status{RESET}\n"
327
+ output += f"{DIM}{'─' * 60}{RESET}\n\n"
328
+
329
+ output += f"{GREEN}Version:{RESET} {info['claude_mpm_version']}\n"
330
+ output += f"{GREEN}Python:{RESET} {info['python_version']}\n"
331
+ output += f"{GREEN}Project Root:{RESET} {info['project_root']}\n"
332
+ output += f"{GREEN}Logging Level:{RESET} {info['logging_level']}\n"
333
+ output += f"{GREEN}Hook Handler:{RESET} {info['hook_handler']}\n"
334
+
335
+ output += f"\n{CYAN}{BOLD}Environment:{RESET}\n"
336
+ for key, value in info['environment'].items():
337
+ output += f"{GREEN} {key}: {value}{RESET}\n"
338
+
339
+ if verbose:
340
+ output += f"\n{CYAN}{BOLD}Hooks Configured:{RESET}\n"
341
+ for hook, status in info.get('hooks_configured', {}).items():
342
+ output += f"{GREEN} {hook}: {status}{RESET}\n"
343
+
344
+ output += f"\n{CYAN}{BOLD}Available Arguments:{RESET}\n"
345
+ for arg in info.get('available_arguments', []):
346
+ output += f"{GREEN} /mpm {arg}{RESET}\n"
347
+
348
+ output += f"\n{DIM}{'─' * 60}{RESET}"
349
+
350
+ return output
351
+
352
+ def _handle_mpm_help(self, unknown_arg=None):
353
+ """Show help for MPM commands."""
354
+ # ANSI colors
355
+ CYAN = '\033[96m'
356
+ RED = '\033[91m'
357
+ GREEN = '\033[92m'
358
+ DIM = '\033[2m'
359
+ RESET = '\033[0m'
360
+ BOLD = '\033[1m'
361
+
362
+ output = f"\n{DIM}{'─' * 60}{RESET}\n"
363
+ output += f"{CYAN}{BOLD}🔧 Claude MPM Management{RESET}\n"
364
+ output += f"{DIM}{'─' * 60}{RESET}\n\n"
365
+
366
+ if unknown_arg:
367
+ output += f"{RED}Unknown argument: {unknown_arg}{RESET}\n\n"
368
+
369
+ output += f"{GREEN}Usage:{RESET} /mpm [argument]\n\n"
370
+ output += f"{GREEN}Available arguments:{RESET}\n"
371
+ for arg, desc in self.mpm_args.items():
372
+ output += f" {arg:<12} - {desc}\n"
373
+
374
+ output += f"\n{GREEN}Examples:{RESET}\n"
375
+ output += f" /mpm - Show this help\n"
376
+ output += f" /mpm status - Show system status\n"
377
+ output += f" /mpm status --verbose - Show detailed status\n"
378
+
379
+ output += f"\n{DIM}{'─' * 60}{RESET}"
380
+
381
+ # Block LLM processing and return our output
382
+ print(output, file=sys.stderr)
383
+ sys.exit(2)
384
+
385
+ def _continue(self):
386
+ """Return continue response to let prompt pass through."""
387
+ response = {"action": "continue"}
388
+ print(json.dumps(response))
389
+ sys.exit(0)
390
+
391
+
392
+ def main():
393
+ """Main entry point."""
394
+ handler = ClaudeHookHandler()
395
+ handler.handle()
396
+
397
+
398
+ if __name__ == "__main__":
399
+ main()
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # Claude Code hook wrapper for claude-mpm
3
+
4
+ # Debug log (optional - comment out in production)
5
+ # echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Wrapper called with args: $@" >> /tmp/hook-wrapper.log
6
+
7
+ # Get the directory where this script is located
8
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9
+
10
+ # Detect if we're in a development environment or installed package
11
+ if [ -d "$SCRIPT_DIR/../../../../venv" ]; then
12
+ # Development environment - script is in src/claude_mpm/hooks/claude_hooks/
13
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../../../.." && pwd )"
14
+ PYTHON_CMD="python"
15
+
16
+ # Activate the virtual environment if it exists
17
+ if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then
18
+ source "$PROJECT_ROOT/venv/bin/activate"
19
+ fi
20
+
21
+ # Set PYTHONPATH for development
22
+ export PYTHONPATH="$PROJECT_ROOT/src:$PYTHONPATH"
23
+ else
24
+ # Installed package - use system Python and installed claude_mpm
25
+ PYTHON_CMD="python3"
26
+
27
+ # Try to detect if we're in a virtual environment
28
+ if [ -n "$VIRTUAL_ENV" ]; then
29
+ PYTHON_CMD="$VIRTUAL_ENV/bin/python"
30
+ elif command -v python3 &> /dev/null; then
31
+ PYTHON_CMD="python3"
32
+ elif command -v python &> /dev/null; then
33
+ PYTHON_CMD="python"
34
+ fi
35
+ fi
36
+
37
+ # Check if we should use DEBUG logging
38
+ if [[ " $* " =~ " --logging DEBUG " ]] || [[ " $* " =~ " --debug " ]]; then
39
+ export CLAUDE_MPM_LOG_LEVEL="DEBUG"
40
+ fi
41
+
42
+ # Debug log (optional)
43
+ # echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/hook-wrapper.log
44
+ # echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Running: $PYTHON_CMD $SCRIPT_DIR/hook_handler.py" >> /tmp/hook-wrapper.log
45
+
46
+ # Run the Python hook handler
47
+ exec "$PYTHON_CMD" "$SCRIPT_DIR/hook_handler.py" "$@"
@@ -0,0 +1,181 @@
1
+ """
2
+ Validation hooks for claude-mpm operations.
3
+
4
+ Inspired by awesome-claude-code's pre-push validation approach.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, Any, List, Optional, Callable
9
+ from pathlib import Path
10
+ import yaml
11
+
12
+ from claude_mpm.validation import AgentValidator, ValidationResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ValidationHooks:
18
+ """Manages validation hooks for various operations."""
19
+
20
+ def __init__(self):
21
+ """Initialize validation hooks."""
22
+ self.pre_load_hooks: List[Callable] = []
23
+ self.post_load_hooks: List[Callable] = []
24
+ self.pre_execute_hooks: List[Callable] = []
25
+ self.validator = AgentValidator()
26
+
27
+ def register_pre_load_hook(self, hook: Callable) -> None:
28
+ """Register a hook to run before loading an agent."""
29
+ self.pre_load_hooks.append(hook)
30
+
31
+ def register_post_load_hook(self, hook: Callable) -> None:
32
+ """Register a hook to run after loading an agent."""
33
+ self.post_load_hooks.append(hook)
34
+
35
+ def register_pre_execute_hook(self, hook: Callable) -> None:
36
+ """Register a hook to run before executing an agent."""
37
+ self.pre_execute_hooks.append(hook)
38
+
39
+ async def run_pre_load_validation(self, profile_path: Path) -> ValidationResult:
40
+ """Run validation before loading an agent profile."""
41
+ logger.info(f"Running pre-load validation for {profile_path}")
42
+
43
+ # Run basic file validation
44
+ if not profile_path.exists():
45
+ result = ValidationResult(is_valid=False)
46
+ result.errors.append(f"Profile file not found: {profile_path}")
47
+ return result
48
+
49
+ # Validate profile structure
50
+ result = self.validator.validate_profile(profile_path)
51
+
52
+ # Run custom pre-load hooks
53
+ for hook in self.pre_load_hooks:
54
+ try:
55
+ hook_result = await hook(profile_path)
56
+ if hasattr(hook_result, 'errors'):
57
+ result.errors.extend(hook_result.errors)
58
+ if hook_result.errors:
59
+ result.is_valid = False
60
+ except Exception as e:
61
+ logger.error(f"Pre-load hook failed: {e}")
62
+ result.warnings.append(f"Pre-load hook failed: {str(e)}")
63
+
64
+ return result
65
+
66
+ async def run_post_load_validation(self, agent_config: Dict[str, Any]) -> ValidationResult:
67
+ """Run validation after loading an agent."""
68
+ result = ValidationResult(is_valid=True)
69
+
70
+ # Run custom post-load hooks
71
+ for hook in self.post_load_hooks:
72
+ try:
73
+ hook_result = await hook(agent_config)
74
+ if hasattr(hook_result, 'warnings'):
75
+ result.warnings.extend(hook_result.warnings)
76
+ except Exception as e:
77
+ logger.error(f"Post-load hook failed: {e}")
78
+ result.warnings.append(f"Post-load hook failed: {str(e)}")
79
+
80
+ return result
81
+
82
+ async def run_pre_execute_validation(self, agent_name: str, task: str) -> ValidationResult:
83
+ """Run validation before executing an agent task."""
84
+ result = ValidationResult(is_valid=True)
85
+
86
+ # Validate task format
87
+ if not task or not task.strip():
88
+ result.errors.append("Task cannot be empty")
89
+ result.is_valid = False
90
+
91
+ # Check task length
92
+ if len(task) > 10000:
93
+ result.warnings.append("Task is very long, consider breaking it down")
94
+
95
+ # Run custom pre-execute hooks
96
+ for hook in self.pre_execute_hooks:
97
+ try:
98
+ hook_result = await hook(agent_name, task)
99
+ if hasattr(hook_result, 'errors') and hook_result.errors:
100
+ result.errors.extend(hook_result.errors)
101
+ result.is_valid = False
102
+ except Exception as e:
103
+ logger.error(f"Pre-execute hook failed: {e}")
104
+ result.warnings.append(f"Pre-execute hook failed: {str(e)}")
105
+
106
+ return result
107
+
108
+
109
+ # Built-in validation hooks
110
+ async def validate_agent_dependencies(profile_path: Path) -> ValidationResult:
111
+ """Validate that agent dependencies are available."""
112
+ result = ValidationResult(is_valid=True)
113
+
114
+ try:
115
+ with open(profile_path, 'r') as f:
116
+ profile_data = yaml.safe_load(f)
117
+
118
+ # Check for circular dependencies
119
+ agents = profile_data.get('agents', [])
120
+ for agent in agents:
121
+ dependencies = agent.get('dependencies', [])
122
+ # Simple check - in real implementation would need graph analysis
123
+ if agent['name'] in dependencies:
124
+ result.errors.append(f"Agent '{agent['name']}' has circular dependency")
125
+ result.is_valid = False
126
+
127
+ except Exception as e:
128
+ result.warnings.append(f"Could not check dependencies: {e}")
129
+
130
+ return result
131
+
132
+
133
+ async def validate_security_constraints(agent_name: str, task: str) -> ValidationResult:
134
+ """Validate security constraints before execution."""
135
+ result = ValidationResult(is_valid=True)
136
+
137
+ # Check for potential security issues in task
138
+ dangerous_patterns = [
139
+ 'rm -rf /',
140
+ 'sudo rm',
141
+ 'format c:',
142
+ '__import__',
143
+ 'eval(',
144
+ 'exec(',
145
+ ]
146
+
147
+ task_lower = task.lower()
148
+ for pattern in dangerous_patterns:
149
+ if pattern in task_lower:
150
+ result.errors.append(f"Potentially dangerous pattern detected: {pattern}")
151
+ result.is_valid = False
152
+
153
+ return result
154
+
155
+
156
+ class ValidationError(Exception):
157
+ """Raised when validation fails."""
158
+
159
+ def __init__(self, message: str, validation_result: Optional[ValidationResult] = None):
160
+ """Initialize validation error."""
161
+ super().__init__(message)
162
+ self.validation_result = validation_result
163
+
164
+ def get_detailed_message(self) -> str:
165
+ """Get detailed error message including validation results."""
166
+ if not self.validation_result:
167
+ return str(self)
168
+
169
+ lines = [str(self)]
170
+
171
+ if self.validation_result.errors:
172
+ lines.append("\nErrors:")
173
+ for error in self.validation_result.errors:
174
+ lines.append(f" - {error}")
175
+
176
+ if self.validation_result.warnings:
177
+ lines.append("\nWarnings:")
178
+ for warning in self.validation_result.warnings:
179
+ lines.append(f" - {warning}")
180
+
181
+ return '\n'.join(lines)
@@ -40,15 +40,15 @@ class AgentManager:
40
40
  Initialize AgentManager.
41
41
 
42
42
  Args:
43
- framework_dir: Path to framework agent-roles directory
43
+ framework_dir: Path to agents templates directory
44
44
  project_dir: Path to project-specific agents directory
45
45
  """
46
46
  # Use PathResolver for consistent path discovery
47
47
  if framework_dir is None:
48
48
  try:
49
- framework_root = PathResolver.get_framework_root()
50
- self.framework_dir = framework_root / "framework" / "agent-roles"
51
- except FileNotFoundError:
49
+ # Use agents templates directory
50
+ self.framework_dir = Path(__file__).parent.parent / "agents" / "templates"
51
+ except Exception:
52
52
  # Fallback to agents directory
53
53
  self.framework_dir = PathResolver.get_agents_dir()
54
54
  else:
@@ -118,7 +118,7 @@ class AgentProfileLoader(BaseService):
118
118
  self.tier_paths = {
119
119
  ProfileTier.PROJECT: self.working_directory / 'agents',
120
120
  ProfileTier.USER: self.user_home / '.claude-pm' / 'agents',
121
- ProfileTier.SYSTEM: self.framework_path / 'agent-roles' if self.framework_path else None
121
+ ProfileTier.SYSTEM: Path(__file__).parent.parent / 'agents' / 'templates'
122
122
  }
123
123
 
124
124
  # Remove None values
@@ -155,7 +155,6 @@ class AgentRegistry:
155
155
  # System-level agents - multiple possible locations
156
156
  system_paths = [
157
157
  Path(__file__).parent.parent / 'agents' / 'templates',
158
- Path(__file__).parent.parent / 'framework' / 'agent-roles',
159
158
  Path('/opt/claude-pm/agents'),
160
159
  Path('/usr/local/claude-pm/agents')
161
160
  ]
@@ -64,10 +64,10 @@ class BaseAgentStructure:
64
64
  class BaseAgentManager:
65
65
  """Manages base_agent.md with structured updates and validation."""
66
66
 
67
- def __init__(self, framework_dir: Optional[Path] = None):
67
+ def __init__(self, agents_dir: Optional[Path] = None):
68
68
  """Initialize BaseAgentManager."""
69
- self.framework_dir = framework_dir or Path(__file__).parent.parent.parent / "framework" / "agent-roles"
70
- self.base_agent_path = self.framework_dir / "base_agent.md"
69
+ self.agents_dir = agents_dir or Path(__file__).parent.parent / "agents"
70
+ self.base_agent_path = self.agents_dir / "BASE_AGENT_TEMPLATE.md"
71
71
  self.cache = SharedPromptCache.get_instance()
72
72
 
73
73
  def read_base_agent(self) -> Optional[BaseAgentStructure]: