claude-mpm 3.4.27__py3-none-any.whl → 3.5.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/INSTRUCTIONS.md +182 -299
- claude_mpm/agents/agent_loader.py +283 -57
- claude_mpm/agents/agent_loader_integration.py +6 -9
- claude_mpm/agents/base_agent.json +2 -1
- claude_mpm/agents/base_agent_loader.py +1 -1
- claude_mpm/cli/__init__.py +5 -7
- claude_mpm/cli/commands/__init__.py +0 -2
- claude_mpm/cli/commands/agents.py +1 -1
- claude_mpm/cli/commands/memory.py +1 -1
- claude_mpm/cli/commands/run.py +12 -0
- claude_mpm/cli/parser.py +0 -13
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/config/__init__.py +44 -2
- claude_mpm/config/agent_config.py +348 -0
- claude_mpm/config/paths.py +322 -0
- claude_mpm/constants.py +0 -1
- claude_mpm/core/__init__.py +2 -5
- claude_mpm/core/agent_registry.py +63 -17
- claude_mpm/core/claude_runner.py +354 -43
- claude_mpm/core/config.py +7 -1
- claude_mpm/core/config_aliases.py +4 -3
- claude_mpm/core/config_paths.py +151 -0
- claude_mpm/core/factories.py +4 -50
- claude_mpm/core/logger.py +11 -13
- claude_mpm/core/service_registry.py +2 -2
- claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
- claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/init.py +37 -6
- claude_mpm/scripts/socketio_daemon.py +6 -2
- claude_mpm/services/__init__.py +71 -3
- claude_mpm/services/agents/__init__.py +85 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
- claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
- claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
- claude_mpm/services/agents/memory/__init__.py +21 -0
- claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
- claude_mpm/services/agents/registry/__init__.py +29 -0
- claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
- claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
- claude_mpm/services/async_session_logger.py +584 -0
- claude_mpm/services/claude_session_logger.py +299 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
- claude_mpm/services/framework_claude_md_generator.py +4 -2
- claude_mpm/services/memory/__init__.py +17 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
- claude_mpm/services/memory/cache/simple_cache.py +317 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
- claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
- claude_mpm/services/optimized_hook_service.py +542 -0
- claude_mpm/services/project_registry.py +14 -8
- claude_mpm/services/response_tracker.py +237 -0
- claude_mpm/services/ticketing_service_original.py +4 -2
- claude_mpm/services/version_control/branch_strategy.py +3 -1
- claude_mpm/utils/paths.py +12 -10
- claude_mpm/utils/session_logging.py +114 -0
- claude_mpm/validation/agent_validator.py +2 -1
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
- /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
- /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""Example submit hook implementation."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import Dict, Any
|
|
5
|
-
|
|
6
|
-
from claude_mpm.hooks.base_hook import SubmitHook, HookContext, HookResult
|
|
7
|
-
from claude_mpm.core.logger import get_logger
|
|
8
|
-
|
|
9
|
-
logger = get_logger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TicketDetectionSubmitHook(SubmitHook):
|
|
13
|
-
"""Hook that detects ticket references in user prompts."""
|
|
14
|
-
|
|
15
|
-
def __init__(self):
|
|
16
|
-
super().__init__(name="ticket_detection", priority=10)
|
|
17
|
-
self.ticket_pattern = re.compile(r'\b(?:TSK|BUG|FEAT)-\d+\b', re.IGNORECASE)
|
|
18
|
-
|
|
19
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
20
|
-
"""Detect and extract ticket references from prompt."""
|
|
21
|
-
try:
|
|
22
|
-
prompt = context.data.get('prompt', '')
|
|
23
|
-
|
|
24
|
-
# Find all ticket references
|
|
25
|
-
tickets = self.ticket_pattern.findall(prompt)
|
|
26
|
-
|
|
27
|
-
if tickets:
|
|
28
|
-
logger.info(f"Found {len(tickets)} ticket references: {tickets}")
|
|
29
|
-
|
|
30
|
-
# Add ticket references to metadata
|
|
31
|
-
return HookResult(
|
|
32
|
-
success=True,
|
|
33
|
-
data={
|
|
34
|
-
'tickets': list(set(tickets)), # Unique tickets
|
|
35
|
-
'prompt': prompt
|
|
36
|
-
},
|
|
37
|
-
modified=True,
|
|
38
|
-
metadata={'ticket_count': len(set(tickets))}
|
|
39
|
-
)
|
|
40
|
-
else:
|
|
41
|
-
return HookResult(
|
|
42
|
-
success=True,
|
|
43
|
-
data={'prompt': prompt},
|
|
44
|
-
modified=False
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
except Exception as e:
|
|
48
|
-
logger.error(f"Ticket detection failed: {e}")
|
|
49
|
-
return HookResult(
|
|
50
|
-
success=False,
|
|
51
|
-
error=str(e)
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class PriorityDetectionSubmitHook(SubmitHook):
|
|
56
|
-
"""Hook that detects priority indicators in prompts."""
|
|
57
|
-
|
|
58
|
-
def __init__(self):
|
|
59
|
-
super().__init__(name="priority_detection", priority=20)
|
|
60
|
-
self.priority_keywords = {
|
|
61
|
-
'urgent': 'high',
|
|
62
|
-
'asap': 'high',
|
|
63
|
-
'critical': 'high',
|
|
64
|
-
'important': 'high',
|
|
65
|
-
'when you can': 'low',
|
|
66
|
-
'whenever': 'low',
|
|
67
|
-
'low priority': 'low'
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
71
|
-
"""Detect priority level from prompt."""
|
|
72
|
-
try:
|
|
73
|
-
prompt = context.data.get('prompt', '').lower()
|
|
74
|
-
|
|
75
|
-
# Check for priority keywords
|
|
76
|
-
detected_priority = 'normal'
|
|
77
|
-
for keyword, priority in self.priority_keywords.items():
|
|
78
|
-
if keyword in prompt:
|
|
79
|
-
detected_priority = priority
|
|
80
|
-
break
|
|
81
|
-
|
|
82
|
-
if detected_priority != 'normal':
|
|
83
|
-
logger.info(f"Detected priority: {detected_priority}")
|
|
84
|
-
|
|
85
|
-
return HookResult(
|
|
86
|
-
success=True,
|
|
87
|
-
data={
|
|
88
|
-
'prompt': context.data.get('prompt', ''),
|
|
89
|
-
'priority': detected_priority
|
|
90
|
-
},
|
|
91
|
-
modified=detected_priority != 'normal',
|
|
92
|
-
metadata={'priority_detected': detected_priority != 'normal'}
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
except Exception as e:
|
|
96
|
-
logger.error(f"Priority detection failed: {e}")
|
|
97
|
-
return HookResult(
|
|
98
|
-
success=False,
|
|
99
|
-
error=str(e)
|
|
100
|
-
)
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
"""Example ticket extraction hook implementation."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import Dict, Any, List, Optional
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
from claude_mpm.hooks.base_hook import TicketExtractionHook, HookContext, HookResult
|
|
8
|
-
from claude_mpm.core.logger import get_logger
|
|
9
|
-
|
|
10
|
-
logger = get_logger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class AutoTicketExtractionHook(TicketExtractionHook):
|
|
14
|
-
"""Hook that automatically extracts tickets from conversations."""
|
|
15
|
-
|
|
16
|
-
def __init__(self):
|
|
17
|
-
super().__init__(name="auto_ticket_extraction", priority=10)
|
|
18
|
-
|
|
19
|
-
# Patterns for detecting ticket-worthy content
|
|
20
|
-
self.ticket_patterns = [
|
|
21
|
-
# Action items: "TODO:", "FIXME:", "ACTION:"
|
|
22
|
-
(r'(?:TODO|FIXME|ACTION):\s*(.+?)(?:\n|$)', 'action'),
|
|
23
|
-
# Bug reports: "bug:", "issue:", "problem:"
|
|
24
|
-
(r'(?:bug|issue|problem):\s*(.+?)(?:\n|$)', 'bug'),
|
|
25
|
-
# Feature requests: "feature:", "enhancement:", "request:"
|
|
26
|
-
(r'(?:feature|enhancement|request):\s*(.+?)(?:\n|$)', 'feature'),
|
|
27
|
-
# Questions that need follow-up
|
|
28
|
-
(r'(?:question|Q):\s*(.+?)(?:\n|$)', 'question'),
|
|
29
|
-
# Explicit ticket creation: "create ticket:", "new ticket:"
|
|
30
|
-
(r'(?:create ticket|new ticket):\s*(.+?)(?:\n|$)', 'ticket')
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
34
|
-
"""Extract potential tickets from conversation."""
|
|
35
|
-
try:
|
|
36
|
-
# Get conversation content
|
|
37
|
-
content = context.data.get('content', '')
|
|
38
|
-
if isinstance(content, dict):
|
|
39
|
-
# Handle structured content
|
|
40
|
-
content = self._extract_text_content(content)
|
|
41
|
-
|
|
42
|
-
# Find all potential tickets
|
|
43
|
-
tickets = []
|
|
44
|
-
|
|
45
|
-
for pattern, ticket_type in self.ticket_patterns:
|
|
46
|
-
matches = re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE)
|
|
47
|
-
for match in matches:
|
|
48
|
-
description = match.group(1).strip()
|
|
49
|
-
if description:
|
|
50
|
-
ticket = self._create_ticket(
|
|
51
|
-
description=description,
|
|
52
|
-
ticket_type=ticket_type,
|
|
53
|
-
context=context
|
|
54
|
-
)
|
|
55
|
-
tickets.append(ticket)
|
|
56
|
-
|
|
57
|
-
# Also check for numbered lists that might be tasks
|
|
58
|
-
numbered_tasks = re.findall(r'^\d+\.\s*(.+?)$', content, re.MULTILINE)
|
|
59
|
-
for task in numbered_tasks:
|
|
60
|
-
if self._is_actionable(task):
|
|
61
|
-
ticket = self._create_ticket(
|
|
62
|
-
description=task.strip(),
|
|
63
|
-
ticket_type='task',
|
|
64
|
-
context=context
|
|
65
|
-
)
|
|
66
|
-
tickets.append(ticket)
|
|
67
|
-
|
|
68
|
-
if tickets:
|
|
69
|
-
logger.info(f"Extracted {len(tickets)} potential tickets")
|
|
70
|
-
return HookResult(
|
|
71
|
-
success=True,
|
|
72
|
-
data={
|
|
73
|
-
'tickets': tickets,
|
|
74
|
-
'original_content': content
|
|
75
|
-
},
|
|
76
|
-
modified=True,
|
|
77
|
-
metadata={'ticket_count': len(tickets)}
|
|
78
|
-
)
|
|
79
|
-
else:
|
|
80
|
-
return HookResult(
|
|
81
|
-
success=True,
|
|
82
|
-
data={'original_content': content},
|
|
83
|
-
modified=False
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
except Exception as e:
|
|
87
|
-
logger.error(f"Ticket extraction failed: {e}")
|
|
88
|
-
return HookResult(
|
|
89
|
-
success=False,
|
|
90
|
-
error=str(e)
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
def _extract_text_content(self, data: Any) -> str:
|
|
94
|
-
"""Extract text content from structured data."""
|
|
95
|
-
if isinstance(data, str):
|
|
96
|
-
return data
|
|
97
|
-
elif isinstance(data, dict):
|
|
98
|
-
# Try common keys
|
|
99
|
-
for key in ['content', 'text', 'message', 'result']:
|
|
100
|
-
if key in data:
|
|
101
|
-
return self._extract_text_content(data[key])
|
|
102
|
-
# Fallback to string representation
|
|
103
|
-
return str(data)
|
|
104
|
-
elif isinstance(data, list):
|
|
105
|
-
return '\n'.join(self._extract_text_content(item) for item in data)
|
|
106
|
-
else:
|
|
107
|
-
return str(data)
|
|
108
|
-
|
|
109
|
-
def _is_actionable(self, text: str) -> bool:
|
|
110
|
-
"""Determine if text represents an actionable item."""
|
|
111
|
-
actionable_verbs = [
|
|
112
|
-
'implement', 'create', 'add', 'fix', 'update', 'remove',
|
|
113
|
-
'test', 'verify', 'check', 'investigate', 'research',
|
|
114
|
-
'document', 'write', 'review', 'refactor', 'optimize'
|
|
115
|
-
]
|
|
116
|
-
|
|
117
|
-
text_lower = text.lower()
|
|
118
|
-
return any(verb in text_lower for verb in actionable_verbs)
|
|
119
|
-
|
|
120
|
-
def _create_ticket(self, description: str, ticket_type: str,
|
|
121
|
-
context: HookContext) -> Dict[str, Any]:
|
|
122
|
-
"""Create a ticket structure."""
|
|
123
|
-
return {
|
|
124
|
-
'id': None, # To be assigned by ticket system
|
|
125
|
-
'title': self._generate_title(description),
|
|
126
|
-
'description': description,
|
|
127
|
-
'type': ticket_type,
|
|
128
|
-
'priority': context.data.get('priority', 'normal'),
|
|
129
|
-
'status': 'pending',
|
|
130
|
-
'created_at': datetime.now().isoformat(),
|
|
131
|
-
'source': 'auto_extraction',
|
|
132
|
-
'metadata': {
|
|
133
|
-
'session_id': context.session_id,
|
|
134
|
-
'user_id': context.user_id,
|
|
135
|
-
'extraction_timestamp': context.timestamp.isoformat()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
def _generate_title(self, description: str) -> str:
|
|
140
|
-
"""Generate a concise title from description."""
|
|
141
|
-
# Take first 50 chars or up to first period/newline
|
|
142
|
-
title = description[:50]
|
|
143
|
-
|
|
144
|
-
# Try to cut at sentence boundary
|
|
145
|
-
for delimiter in ['.', '\n', '!', '?']:
|
|
146
|
-
if delimiter in title:
|
|
147
|
-
title = title.split(delimiter)[0]
|
|
148
|
-
break
|
|
149
|
-
|
|
150
|
-
# Clean up and ensure not too long
|
|
151
|
-
title = title.strip()
|
|
152
|
-
if len(title) > 50:
|
|
153
|
-
title = title[:47] + '...'
|
|
154
|
-
|
|
155
|
-
return title
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class TicketPriorityAnalyzerHook(TicketExtractionHook):
|
|
159
|
-
"""Hook that analyzes and assigns priority to extracted tickets."""
|
|
160
|
-
|
|
161
|
-
def __init__(self):
|
|
162
|
-
super().__init__(name="ticket_priority_analyzer", priority=50)
|
|
163
|
-
|
|
164
|
-
self.priority_indicators = {
|
|
165
|
-
'critical': ['critical', 'urgent', 'blocker', 'emergency', 'asap'],
|
|
166
|
-
'high': ['important', 'high priority', 'needed', 'required'],
|
|
167
|
-
'low': ['minor', 'nice to have', 'someday', 'optional']
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
171
|
-
"""Analyze and update ticket priorities."""
|
|
172
|
-
try:
|
|
173
|
-
tickets = context.data.get('tickets', [])
|
|
174
|
-
|
|
175
|
-
if not tickets:
|
|
176
|
-
return HookResult(
|
|
177
|
-
success=True,
|
|
178
|
-
data=context.data,
|
|
179
|
-
modified=False
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Analyze each ticket
|
|
183
|
-
updated_tickets = []
|
|
184
|
-
priorities_updated = 0
|
|
185
|
-
|
|
186
|
-
for ticket in tickets:
|
|
187
|
-
original_priority = ticket.get('priority', 'normal')
|
|
188
|
-
analyzed_priority = self._analyze_priority(ticket)
|
|
189
|
-
|
|
190
|
-
if analyzed_priority != original_priority:
|
|
191
|
-
ticket['priority'] = analyzed_priority
|
|
192
|
-
ticket['metadata']['priority_analyzed'] = True
|
|
193
|
-
priorities_updated += 1
|
|
194
|
-
|
|
195
|
-
updated_tickets.append(ticket)
|
|
196
|
-
|
|
197
|
-
if priorities_updated > 0:
|
|
198
|
-
logger.info(f"Updated priority for {priorities_updated} tickets")
|
|
199
|
-
return HookResult(
|
|
200
|
-
success=True,
|
|
201
|
-
data={
|
|
202
|
-
'tickets': updated_tickets
|
|
203
|
-
},
|
|
204
|
-
modified=True,
|
|
205
|
-
metadata={'priorities_updated': priorities_updated}
|
|
206
|
-
)
|
|
207
|
-
else:
|
|
208
|
-
return HookResult(
|
|
209
|
-
success=True,
|
|
210
|
-
data=context.data,
|
|
211
|
-
modified=False
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
except Exception as e:
|
|
215
|
-
logger.error(f"Priority analysis failed: {e}")
|
|
216
|
-
return HookResult(
|
|
217
|
-
success=False,
|
|
218
|
-
error=str(e)
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
def _analyze_priority(self, ticket: Dict[str, Any]) -> str:
|
|
222
|
-
"""Analyze ticket content to determine priority."""
|
|
223
|
-
content = f"{ticket.get('title', '')} {ticket.get('description', '')}".lower()
|
|
224
|
-
|
|
225
|
-
# Check for priority indicators
|
|
226
|
-
for priority, indicators in self.priority_indicators.items():
|
|
227
|
-
if any(indicator in content for indicator in indicators):
|
|
228
|
-
return priority
|
|
229
|
-
|
|
230
|
-
# Check ticket type
|
|
231
|
-
ticket_type = ticket.get('type', '')
|
|
232
|
-
if ticket_type == 'bug':
|
|
233
|
-
return 'high'
|
|
234
|
-
elif ticket_type == 'question':
|
|
235
|
-
return 'normal'
|
|
236
|
-
|
|
237
|
-
return 'normal'
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
"""Hook to enforce [Agent] prefix requirement for TodoWrite tool calls."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import Dict, Any, List, Optional
|
|
5
|
-
|
|
6
|
-
from claude_mpm.hooks.base_hook import BaseHook, HookContext, HookResult, HookType
|
|
7
|
-
from claude_mpm.core.logger import get_logger
|
|
8
|
-
from claude_mpm.core.agent_name_normalizer import agent_name_normalizer
|
|
9
|
-
|
|
10
|
-
logger = get_logger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TodoAgentPrefixHook(BaseHook):
|
|
14
|
-
"""Hook that enforces agent name prefixes in TodoWrite tool calls."""
|
|
15
|
-
|
|
16
|
-
def __init__(self):
|
|
17
|
-
super().__init__(name="todo_agent_prefix_enforcer", priority=20)
|
|
18
|
-
|
|
19
|
-
# Mapping of task content patterns to appropriate agent prefixes
|
|
20
|
-
self.agent_patterns = {
|
|
21
|
-
'engineer': [
|
|
22
|
-
r'implement', r'code', r'fix', r'refactor', r'debug', r'develop',
|
|
23
|
-
r'create.*function', r'write.*class', r'add.*feature', r'optimize.*code'
|
|
24
|
-
],
|
|
25
|
-
'research': [
|
|
26
|
-
r'research', r'investigate', r'analyze', r'explore', r'find.*best',
|
|
27
|
-
r'compare', r'evaluate', r'study', r'discover', r'understand'
|
|
28
|
-
],
|
|
29
|
-
'documentation': [
|
|
30
|
-
r'document', r'write.*doc', r'update.*readme', r'changelog',
|
|
31
|
-
r'create.*guide', r'explain', r'describe', r'write.*tutorial'
|
|
32
|
-
],
|
|
33
|
-
'qa': [
|
|
34
|
-
r'test', r'validate', r'verify', r'check', r'ensure.*quality',
|
|
35
|
-
r'run.*tests', r'coverage', r'lint', r'audit'
|
|
36
|
-
],
|
|
37
|
-
'security': [
|
|
38
|
-
r'security', r'vulnerability', r'protect', r'secure', r'audit.*security',
|
|
39
|
-
r'penetration', r'encrypt', r'authenticate', r'authorize'
|
|
40
|
-
],
|
|
41
|
-
'ops': [
|
|
42
|
-
r'deploy', r'configure', r'setup', r'install', r'provision',
|
|
43
|
-
r'infrastructure', r'ci/cd', r'pipeline', r'monitor'
|
|
44
|
-
],
|
|
45
|
-
'data_engineer': [
|
|
46
|
-
r'data.*pipeline', r'etl', r'database', r'schema', r'migrate',
|
|
47
|
-
r'transform.*data', r'api.*integration', r'data.*flow'
|
|
48
|
-
],
|
|
49
|
-
'version_control': [
|
|
50
|
-
r'version', r'release', r'tag', r'branch', r'merge',
|
|
51
|
-
r'git', r'commit', r'push', r'pull'
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
# Compile patterns for efficiency
|
|
56
|
-
self.compiled_patterns = {}
|
|
57
|
-
for agent, patterns in self.agent_patterns.items():
|
|
58
|
-
self.compiled_patterns[agent] = [
|
|
59
|
-
re.compile(pattern, re.IGNORECASE) for pattern in patterns
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
63
|
-
"""Check and enforce agent prefix in TodoWrite calls."""
|
|
64
|
-
try:
|
|
65
|
-
# This hook is designed to work with tool interception
|
|
66
|
-
# Check if this is a TodoWrite tool call
|
|
67
|
-
if context.hook_type != HookType.CUSTOM:
|
|
68
|
-
return HookResult(success=True, modified=False)
|
|
69
|
-
|
|
70
|
-
tool_name = context.data.get('tool_name', '')
|
|
71
|
-
if tool_name != 'TodoWrite':
|
|
72
|
-
return HookResult(success=True, modified=False)
|
|
73
|
-
|
|
74
|
-
# Extract todos from the tool parameters
|
|
75
|
-
tool_params = context.data.get('parameters', {})
|
|
76
|
-
todos = tool_params.get('todos', [])
|
|
77
|
-
|
|
78
|
-
if not todos:
|
|
79
|
-
return HookResult(success=True, modified=False)
|
|
80
|
-
|
|
81
|
-
# Check and fix each todo item
|
|
82
|
-
modified = False
|
|
83
|
-
validation_errors = []
|
|
84
|
-
updated_todos = []
|
|
85
|
-
|
|
86
|
-
for todo in todos:
|
|
87
|
-
content = todo.get('content', '')
|
|
88
|
-
|
|
89
|
-
# Check if content already has an agent prefix
|
|
90
|
-
if self._has_agent_prefix(content):
|
|
91
|
-
updated_todos.append(todo)
|
|
92
|
-
continue
|
|
93
|
-
|
|
94
|
-
# Try to determine appropriate agent
|
|
95
|
-
suggested_agent = self._suggest_agent(content)
|
|
96
|
-
|
|
97
|
-
if suggested_agent:
|
|
98
|
-
# Automatically add the prefix using normalized format
|
|
99
|
-
prefix = agent_name_normalizer.to_todo_prefix(suggested_agent)
|
|
100
|
-
todo['content'] = f"{prefix} {content}"
|
|
101
|
-
updated_todos.append(todo)
|
|
102
|
-
modified = True
|
|
103
|
-
logger.info(f"Added '{prefix}' prefix to todo: {content[:50]}...")
|
|
104
|
-
else:
|
|
105
|
-
# If we can't determine the agent, block the call
|
|
106
|
-
validation_errors.append(
|
|
107
|
-
f"Todo item missing required [Agent] prefix: '{content[:50]}...'. "
|
|
108
|
-
f"Please prefix with one of: [Research], [Engineer], [QA], "
|
|
109
|
-
f"[Security], [Documentation], [Ops], [Data Engineer], or [Version Control]."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# If there are validation errors, block the call
|
|
113
|
-
if validation_errors:
|
|
114
|
-
return HookResult(
|
|
115
|
-
success=False,
|
|
116
|
-
error="\n".join(validation_errors),
|
|
117
|
-
metadata={'validation_failed': True}
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# If we modified any todos, update the parameters
|
|
121
|
-
if modified:
|
|
122
|
-
tool_params['todos'] = updated_todos
|
|
123
|
-
return HookResult(
|
|
124
|
-
success=True,
|
|
125
|
-
data={
|
|
126
|
-
'tool_name': tool_name,
|
|
127
|
-
'parameters': tool_params
|
|
128
|
-
},
|
|
129
|
-
modified=True,
|
|
130
|
-
metadata={'prefixes_added': True}
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
return HookResult(success=True, modified=False)
|
|
134
|
-
|
|
135
|
-
except Exception as e:
|
|
136
|
-
logger.error(f"Todo agent prefix enforcement failed: {e}")
|
|
137
|
-
return HookResult(
|
|
138
|
-
success=False,
|
|
139
|
-
error=str(e)
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
def _has_agent_prefix(self, content: str) -> bool:
|
|
143
|
-
"""Check if content already has an agent prefix."""
|
|
144
|
-
import re
|
|
145
|
-
content = content.strip()
|
|
146
|
-
# Only check for [Agent] prefix at the beginning, not agent mentions in content
|
|
147
|
-
match = re.match(r'^\[([^\]]+)\]', content)
|
|
148
|
-
return match is not None
|
|
149
|
-
|
|
150
|
-
def _suggest_agent(self, content: str) -> Optional[str]:
|
|
151
|
-
"""Suggest an appropriate agent based on content analysis."""
|
|
152
|
-
content_lower = content.lower()
|
|
153
|
-
|
|
154
|
-
# Check each agent's patterns
|
|
155
|
-
for agent, patterns in self.compiled_patterns.items():
|
|
156
|
-
for pattern in patterns:
|
|
157
|
-
if pattern.search(content_lower):
|
|
158
|
-
return agent_name_normalizer.normalize(agent)
|
|
159
|
-
|
|
160
|
-
# Default suggestions based on common keywords
|
|
161
|
-
if any(word in content_lower for word in ['code', 'implement', 'fix', 'bug']):
|
|
162
|
-
return agent_name_normalizer.normalize('engineer')
|
|
163
|
-
elif any(word in content_lower for word in ['test', 'validate', 'check']):
|
|
164
|
-
return agent_name_normalizer.normalize('qa')
|
|
165
|
-
elif any(word in content_lower for word in ['doc', 'readme', 'guide']):
|
|
166
|
-
return agent_name_normalizer.normalize('documentation')
|
|
167
|
-
elif any(word in content_lower for word in ['research', 'investigate']):
|
|
168
|
-
return agent_name_normalizer.normalize('research')
|
|
169
|
-
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
def validate(self, context: HookContext) -> bool:
|
|
173
|
-
"""Validate if hook should run for given context."""
|
|
174
|
-
if not super().validate(context):
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
# This hook only runs for CUSTOM type with tool_name = TodoWrite
|
|
178
|
-
return (context.hook_type == HookType.CUSTOM and
|
|
179
|
-
context.data.get('tool_name') == 'TodoWrite')
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class TodoAgentPrefixValidatorHook(BaseHook):
|
|
183
|
-
"""Alternative hook that only validates without auto-fixing."""
|
|
184
|
-
|
|
185
|
-
def __init__(self):
|
|
186
|
-
super().__init__(name="todo_agent_prefix_validator", priority=15)
|
|
187
|
-
# Get valid agents from normalizer
|
|
188
|
-
self.valid_agents = list(agent_name_normalizer.CANONICAL_NAMES.values())
|
|
189
|
-
|
|
190
|
-
def execute(self, context: HookContext) -> HookResult:
|
|
191
|
-
"""Validate agent prefix in TodoWrite calls without auto-fixing."""
|
|
192
|
-
try:
|
|
193
|
-
# Check if this is a TodoWrite tool call
|
|
194
|
-
if context.data.get('tool_name') != 'TodoWrite':
|
|
195
|
-
return HookResult(success=True, modified=False)
|
|
196
|
-
|
|
197
|
-
# Extract todos
|
|
198
|
-
tool_params = context.data.get('parameters', {})
|
|
199
|
-
todos = tool_params.get('todos', [])
|
|
200
|
-
|
|
201
|
-
validation_errors = []
|
|
202
|
-
|
|
203
|
-
for i, todo in enumerate(todos):
|
|
204
|
-
content = todo.get('content', '')
|
|
205
|
-
|
|
206
|
-
# Check for agent prefix using normalizer
|
|
207
|
-
if not agent_name_normalizer.extract_from_todo(content):
|
|
208
|
-
validation_errors.append(
|
|
209
|
-
f"Todo #{i+1} missing required agent prefix. "
|
|
210
|
-
f"Content: '{content[:50]}...'\n"
|
|
211
|
-
f"Please use format: '[Agent] Task description' where [Agent] is one of: "
|
|
212
|
-
f"{', '.join('[' + agent + ']' for agent in self.valid_agents)}"
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if validation_errors:
|
|
216
|
-
return HookResult(
|
|
217
|
-
success=False,
|
|
218
|
-
error="\n\n".join(validation_errors),
|
|
219
|
-
metadata={
|
|
220
|
-
'validation_type': 'agent_prefix',
|
|
221
|
-
'valid_agents': self.valid_agents
|
|
222
|
-
}
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
return HookResult(success=True, modified=False)
|
|
226
|
-
|
|
227
|
-
except Exception as e:
|
|
228
|
-
logger.error(f"Todo validation failed: {e}")
|
|
229
|
-
return HookResult(
|
|
230
|
-
success=False,
|
|
231
|
-
error=str(e)
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
def validate(self, context: HookContext) -> bool:
|
|
235
|
-
"""Validate if hook should run for given context."""
|
|
236
|
-
if not super().validate(context):
|
|
237
|
-
return False
|
|
238
|
-
|
|
239
|
-
return (context.hook_type == HookType.CUSTOM and
|
|
240
|
-
context.data.get('tool_name') == 'TodoWrite')
|