claude-mpm 3.4.27__py3-none-any.whl → 3.5.1__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 (123) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +182 -299
  3. claude_mpm/agents/agent_loader.py +283 -57
  4. claude_mpm/agents/agent_loader_integration.py +6 -9
  5. claude_mpm/agents/base_agent.json +2 -1
  6. claude_mpm/agents/base_agent_loader.py +1 -1
  7. claude_mpm/cli/__init__.py +5 -7
  8. claude_mpm/cli/commands/__init__.py +0 -2
  9. claude_mpm/cli/commands/agents.py +1 -1
  10. claude_mpm/cli/commands/memory.py +1 -1
  11. claude_mpm/cli/commands/run.py +12 -0
  12. claude_mpm/cli/parser.py +0 -13
  13. claude_mpm/cli/utils.py +1 -1
  14. claude_mpm/config/__init__.py +44 -2
  15. claude_mpm/config/agent_config.py +348 -0
  16. claude_mpm/config/paths.py +322 -0
  17. claude_mpm/constants.py +0 -1
  18. claude_mpm/core/__init__.py +2 -5
  19. claude_mpm/core/agent_registry.py +63 -17
  20. claude_mpm/core/claude_runner.py +354 -43
  21. claude_mpm/core/config.py +7 -1
  22. claude_mpm/core/config_aliases.py +4 -3
  23. claude_mpm/core/config_paths.py +151 -0
  24. claude_mpm/core/factories.py +4 -50
  25. claude_mpm/core/logger.py +11 -13
  26. claude_mpm/core/service_registry.py +2 -2
  27. claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
  28. claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
  30. claude_mpm/hooks/memory_integration_hook.py +1 -1
  31. claude_mpm/init.py +37 -6
  32. claude_mpm/scripts/socketio_daemon.py +6 -2
  33. claude_mpm/services/__init__.py +71 -3
  34. claude_mpm/services/agents/__init__.py +85 -0
  35. claude_mpm/services/agents/deployment/__init__.py +21 -0
  36. claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
  37. claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
  38. claude_mpm/services/agents/loading/__init__.py +11 -0
  39. claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
  40. claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
  41. claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
  42. claude_mpm/services/agents/management/__init__.py +9 -0
  43. claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
  44. claude_mpm/services/agents/memory/__init__.py +21 -0
  45. claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
  46. claude_mpm/services/agents/registry/__init__.py +29 -0
  47. claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
  48. claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
  49. claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
  50. claude_mpm/services/async_session_logger.py +584 -0
  51. claude_mpm/services/claude_session_logger.py +299 -0
  52. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  53. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
  54. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
  56. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
  57. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
  58. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
  59. claude_mpm/services/framework_claude_md_generator.py +4 -2
  60. claude_mpm/services/memory/__init__.py +17 -0
  61. claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
  62. claude_mpm/services/memory/cache/__init__.py +14 -0
  63. claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
  64. claude_mpm/services/memory/cache/simple_cache.py +317 -0
  65. claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
  66. claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
  67. claude_mpm/services/optimized_hook_service.py +542 -0
  68. claude_mpm/services/project_registry.py +14 -8
  69. claude_mpm/services/response_tracker.py +237 -0
  70. claude_mpm/services/ticketing_service_original.py +4 -2
  71. claude_mpm/services/version_control/branch_strategy.py +3 -1
  72. claude_mpm/utils/paths.py +12 -10
  73. claude_mpm/utils/session_logging.py +114 -0
  74. claude_mpm/validation/agent_validator.py +2 -1
  75. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/METADATA +28 -20
  76. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/RECORD +83 -106
  77. claude_mpm/cli/commands/ui.py +0 -57
  78. claude_mpm/core/simple_runner.py +0 -1046
  79. claude_mpm/hooks/builtin/__init__.py +0 -1
  80. claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
  81. claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
  82. claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
  83. claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
  84. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
  85. claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
  86. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
  87. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
  88. claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
  89. claude_mpm/orchestration/__init__.py +0 -6
  90. claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
  91. claude_mpm/orchestration/archive/factory.py +0 -215
  92. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
  93. claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
  94. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
  95. claude_mpm/orchestration/archive/orchestrator.py +0 -501
  96. claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
  97. claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
  98. claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
  99. claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
  100. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
  101. claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
  102. claude_mpm/schemas/workflow_validator.py +0 -411
  103. claude_mpm/services/parent_directory_manager/__init__.py +0 -577
  104. claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
  105. claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
  106. claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
  107. claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
  108. claude_mpm/services/parent_directory_manager/operations.py +0 -186
  109. claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
  110. claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
  111. claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
  112. claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
  113. claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
  114. claude_mpm/ui/__init__.py +0 -1
  115. claude_mpm/ui/rich_terminal_ui.py +0 -295
  116. claude_mpm/ui/terminal_ui.py +0 -328
  117. /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
  118. /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
  119. /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
  120. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/WHEEL +0 -0
  121. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/entry_points.txt +0 -0
  122. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.dist-info}/licenses/LICENSE +0 -0
  123. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.1.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')