claude-mpm 0.3.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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
content = content.strip()
|
|
145
|
+
# Check if content has a valid agent prefix
|
|
146
|
+
agent = agent_name_normalizer.extract_from_todo(content)
|
|
147
|
+
return agent is not None
|
|
148
|
+
|
|
149
|
+
def _suggest_agent(self, content: str) -> Optional[str]:
|
|
150
|
+
"""Suggest an appropriate agent based on content analysis."""
|
|
151
|
+
content_lower = content.lower()
|
|
152
|
+
|
|
153
|
+
# Check each agent's patterns
|
|
154
|
+
for agent, patterns in self.compiled_patterns.items():
|
|
155
|
+
for pattern in patterns:
|
|
156
|
+
if pattern.search(content_lower):
|
|
157
|
+
return agent_name_normalizer.normalize(agent)
|
|
158
|
+
|
|
159
|
+
# Default suggestions based on common keywords
|
|
160
|
+
if any(word in content_lower for word in ['code', 'implement', 'fix', 'bug']):
|
|
161
|
+
return 'engineer'
|
|
162
|
+
elif any(word in content_lower for word in ['test', 'validate', 'check']):
|
|
163
|
+
return 'qa'
|
|
164
|
+
elif any(word in content_lower for word in ['doc', 'readme', 'guide']):
|
|
165
|
+
return 'documentation'
|
|
166
|
+
elif any(word in content_lower for word in ['research', 'investigate']):
|
|
167
|
+
return 'research'
|
|
168
|
+
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def validate(self, context: HookContext) -> bool:
|
|
172
|
+
"""Validate if hook should run for given context."""
|
|
173
|
+
if not super().validate(context):
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
# This hook only runs for CUSTOM type with tool_name = TodoWrite
|
|
177
|
+
return (context.hook_type == HookType.CUSTOM and
|
|
178
|
+
context.data.get('tool_name') == 'TodoWrite')
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TodoAgentPrefixValidatorHook(BaseHook):
|
|
182
|
+
"""Alternative hook that only validates without auto-fixing."""
|
|
183
|
+
|
|
184
|
+
def __init__(self):
|
|
185
|
+
super().__init__(name="todo_agent_prefix_validator", priority=15)
|
|
186
|
+
# Get valid agents from normalizer
|
|
187
|
+
self.valid_agents = list(agent_name_normalizer.CANONICAL_NAMES.values())
|
|
188
|
+
|
|
189
|
+
def execute(self, context: HookContext) -> HookResult:
|
|
190
|
+
"""Validate agent prefix in TodoWrite calls without auto-fixing."""
|
|
191
|
+
try:
|
|
192
|
+
# Check if this is a TodoWrite tool call
|
|
193
|
+
if context.data.get('tool_name') != 'TodoWrite':
|
|
194
|
+
return HookResult(success=True, modified=False)
|
|
195
|
+
|
|
196
|
+
# Extract todos
|
|
197
|
+
tool_params = context.data.get('parameters', {})
|
|
198
|
+
todos = tool_params.get('todos', [])
|
|
199
|
+
|
|
200
|
+
validation_errors = []
|
|
201
|
+
|
|
202
|
+
for i, todo in enumerate(todos):
|
|
203
|
+
content = todo.get('content', '')
|
|
204
|
+
|
|
205
|
+
# Check for agent prefix using normalizer
|
|
206
|
+
if not agent_name_normalizer.extract_from_todo(content):
|
|
207
|
+
validation_errors.append(
|
|
208
|
+
f"Todo #{i+1} missing required agent prefix. "
|
|
209
|
+
f"Content: '{content[:50]}...'\n"
|
|
210
|
+
f"Please use format: '[Agent] Task description' where [Agent] is one of: "
|
|
211
|
+
f"{', '.join('[' + agent + ']' for agent in self.valid_agents)}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if validation_errors:
|
|
215
|
+
return HookResult(
|
|
216
|
+
success=False,
|
|
217
|
+
error="\n\n".join(validation_errors),
|
|
218
|
+
metadata={
|
|
219
|
+
'validation_type': 'agent_prefix',
|
|
220
|
+
'valid_agents': self.valid_agents
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return HookResult(success=True, modified=False)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Todo validation failed: {e}")
|
|
228
|
+
return HookResult(
|
|
229
|
+
success=False,
|
|
230
|
+
error=str(e)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def validate(self, context: HookContext) -> bool:
|
|
234
|
+
"""Validate if hook should run for given context."""
|
|
235
|
+
if not super().validate(context):
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
return (context.hook_type == HookType.CUSTOM and
|
|
239
|
+
context.data.get('tool_name') == 'TodoWrite')
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Hook to log workflow information at the start of execution."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from claude_mpm.hooks.base_hook import SubmitHook, HookContext, HookResult, HookType
|
|
8
|
+
from claude_mpm.core.logger import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkflowStartHook(SubmitHook):
|
|
14
|
+
"""Hook that logs workflow information including steps list at workflow start."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(name="workflow_start_logger", priority=5)
|
|
18
|
+
|
|
19
|
+
def execute(self, context: HookContext) -> HookResult:
|
|
20
|
+
"""Log workflow information when a workflow starts."""
|
|
21
|
+
try:
|
|
22
|
+
# Extract prompt and workflow data from context
|
|
23
|
+
prompt = context.data.get('prompt', '')
|
|
24
|
+
workflow_data = context.data.get('workflow', {})
|
|
25
|
+
|
|
26
|
+
# Check if this is a workflow start (either by explicit workflow data or detected pattern)
|
|
27
|
+
if workflow_data or self._is_workflow_prompt(prompt):
|
|
28
|
+
# Extract workflow information
|
|
29
|
+
workflow_name = workflow_data.get('name', 'Unnamed Workflow')
|
|
30
|
+
workflow_steps = workflow_data.get('steps', [])
|
|
31
|
+
|
|
32
|
+
# If no explicit steps provided, try to parse from prompt
|
|
33
|
+
if not workflow_steps and prompt:
|
|
34
|
+
workflow_steps = self._extract_steps_from_prompt(prompt)
|
|
35
|
+
|
|
36
|
+
# Log workflow start information
|
|
37
|
+
logger.info("="*60)
|
|
38
|
+
logger.info("WORKFLOW START")
|
|
39
|
+
logger.info("="*60)
|
|
40
|
+
logger.info(f"Workflow: {workflow_name}")
|
|
41
|
+
logger.info(f"Started at: {context.timestamp.isoformat()}")
|
|
42
|
+
logger.info(f"Session ID: {context.session_id or 'N/A'}")
|
|
43
|
+
logger.info(f"User ID: {context.user_id or 'N/A'}")
|
|
44
|
+
|
|
45
|
+
if workflow_steps:
|
|
46
|
+
logger.info(f"\nWorkflow Steps ({len(workflow_steps)} total):")
|
|
47
|
+
for i, step in enumerate(workflow_steps, 1):
|
|
48
|
+
if isinstance(step, dict):
|
|
49
|
+
step_name = step.get('name', step.get('description', 'Unnamed step'))
|
|
50
|
+
step_type = step.get('type', 'task')
|
|
51
|
+
logger.info(f" {i}. [{step_type}] {step_name}")
|
|
52
|
+
else:
|
|
53
|
+
# Handle simple string steps
|
|
54
|
+
logger.info(f" {i}. {step}")
|
|
55
|
+
else:
|
|
56
|
+
logger.info("\nNo explicit workflow steps defined")
|
|
57
|
+
|
|
58
|
+
# Log additional metadata if present
|
|
59
|
+
metadata = workflow_data.get('metadata', {})
|
|
60
|
+
if metadata:
|
|
61
|
+
logger.info(f"\nWorkflow Metadata:")
|
|
62
|
+
for key, value in metadata.items():
|
|
63
|
+
logger.info(f" {key}: {value}")
|
|
64
|
+
|
|
65
|
+
logger.info("="*60)
|
|
66
|
+
|
|
67
|
+
# Add workflow info to result metadata for downstream hooks
|
|
68
|
+
return HookResult(
|
|
69
|
+
success=True,
|
|
70
|
+
modified=False,
|
|
71
|
+
metadata={
|
|
72
|
+
'workflow_logged': True,
|
|
73
|
+
'workflow_name': workflow_name,
|
|
74
|
+
'step_count': len(workflow_steps),
|
|
75
|
+
'has_explicit_workflow': bool(workflow_data)
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Not a workflow start, pass through
|
|
80
|
+
return HookResult(success=True, modified=False)
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Workflow start logging failed: {e}")
|
|
84
|
+
# Don't block execution on logging errors
|
|
85
|
+
return HookResult(
|
|
86
|
+
success=True,
|
|
87
|
+
modified=False,
|
|
88
|
+
error=str(e)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _is_workflow_prompt(self, prompt: str) -> bool:
|
|
92
|
+
"""Detect if a prompt indicates a workflow start."""
|
|
93
|
+
if not prompt:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
prompt_lower = prompt.lower()
|
|
97
|
+
workflow_indicators = [
|
|
98
|
+
'workflow', 'steps:', 'step 1', 'first,', 'then,', 'finally,',
|
|
99
|
+
'process:', 'procedure:', 'sequence:', 'plan:'
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
return any(indicator in prompt_lower for indicator in workflow_indicators)
|
|
103
|
+
|
|
104
|
+
def _extract_steps_from_prompt(self, prompt: str) -> List[str]:
|
|
105
|
+
"""Try to extract workflow steps from a prompt text."""
|
|
106
|
+
steps = []
|
|
107
|
+
|
|
108
|
+
# Look for numbered steps (1. 2. 3. or 1) 2) 3))
|
|
109
|
+
import re
|
|
110
|
+
numbered_pattern = re.compile(r'^\s*\d+[\)\.]\s*(.+)$', re.MULTILINE)
|
|
111
|
+
matches = numbered_pattern.findall(prompt)
|
|
112
|
+
if matches:
|
|
113
|
+
steps.extend(matches)
|
|
114
|
+
return steps
|
|
115
|
+
|
|
116
|
+
# Look for bullet points
|
|
117
|
+
bullet_pattern = re.compile(r'^\s*[-*•]\s*(.+)$', re.MULTILINE)
|
|
118
|
+
matches = bullet_pattern.findall(prompt)
|
|
119
|
+
if matches:
|
|
120
|
+
steps.extend(matches)
|
|
121
|
+
return steps
|
|
122
|
+
|
|
123
|
+
# Look for sequential keywords
|
|
124
|
+
sequential_pattern = re.compile(
|
|
125
|
+
r'(?:first|then|next|after that|finally|lastly),?\s*(.+?)(?=(?:first|then|next|after that|finally|lastly|$))',
|
|
126
|
+
re.IGNORECASE | re.DOTALL
|
|
127
|
+
)
|
|
128
|
+
matches = sequential_pattern.findall(prompt)
|
|
129
|
+
if matches:
|
|
130
|
+
steps.extend([m.strip() for m in matches if m.strip()])
|
|
131
|
+
|
|
132
|
+
return steps
|
|
133
|
+
|
|
134
|
+
def validate(self, context: HookContext) -> bool:
|
|
135
|
+
"""Validate if hook should run for given context."""
|
|
136
|
+
if not super().validate(context):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# This hook runs for all submit contexts to check for workflows
|
|
140
|
+
return context.hook_type == HookType.SUBMIT
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WorkflowStepLogger(SubmitHook):
|
|
144
|
+
"""Alternative hook that logs individual workflow step execution."""
|
|
145
|
+
|
|
146
|
+
def __init__(self):
|
|
147
|
+
super().__init__(name="workflow_step_logger", priority=6)
|
|
148
|
+
|
|
149
|
+
def execute(self, context: HookContext) -> HookResult:
|
|
150
|
+
"""Log individual workflow step execution."""
|
|
151
|
+
try:
|
|
152
|
+
# Check if this is a workflow step execution
|
|
153
|
+
step_data = context.data.get('workflow_step', {})
|
|
154
|
+
if not step_data:
|
|
155
|
+
return HookResult(success=True, modified=False)
|
|
156
|
+
|
|
157
|
+
# Extract step information
|
|
158
|
+
step_number = step_data.get('number', 0)
|
|
159
|
+
step_name = step_data.get('name', 'Unnamed step')
|
|
160
|
+
step_type = step_data.get('type', 'task')
|
|
161
|
+
workflow_name = step_data.get('workflow_name', 'Unknown workflow')
|
|
162
|
+
|
|
163
|
+
# Log step execution
|
|
164
|
+
logger.info(f"\n→ Executing Step {step_number}: {step_name}")
|
|
165
|
+
logger.info(f" Type: {step_type}")
|
|
166
|
+
logger.info(f" Workflow: {workflow_name}")
|
|
167
|
+
logger.info(f" Started: {context.timestamp.isoformat()}")
|
|
168
|
+
|
|
169
|
+
return HookResult(
|
|
170
|
+
success=True,
|
|
171
|
+
modified=False,
|
|
172
|
+
metadata={
|
|
173
|
+
'step_logged': True,
|
|
174
|
+
'step_number': step_number,
|
|
175
|
+
'step_name': step_name
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"Workflow step logging failed: {e}")
|
|
181
|
+
return HookResult(success=True, modified=False, error=str(e))
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Client for interacting with the hook service.
|
|
2
|
+
|
|
3
|
+
DEPRECATED: This HTTP-based hook client is deprecated and will be removed in a future release.
|
|
4
|
+
Please use the JSON-RPC implementation from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
5
|
+
See /docs/hook_system_migration_guide.md for migration instructions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
from urllib.parse import urljoin
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from requests.adapters import HTTPAdapter
|
|
16
|
+
from requests.packages.urllib3.util.retry import Retry
|
|
17
|
+
|
|
18
|
+
from claude_mpm.hooks.base_hook import HookType
|
|
19
|
+
from claude_mpm.core.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HookServiceClient:
|
|
25
|
+
"""Client for interacting with the centralized hook service.
|
|
26
|
+
|
|
27
|
+
DEPRECATED: Use JSONRPCHookClient from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, base_url: str = "http://localhost:5001", timeout: int = 30):
|
|
31
|
+
"""Initialize hook service client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base_url: Base URL of hook service
|
|
35
|
+
timeout: Request timeout in seconds
|
|
36
|
+
"""
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"HookServiceClient is deprecated and will be removed in a future release. "
|
|
39
|
+
"Please use JSONRPCHookClient from claude_mpm.hooks.json_rpc_hook_client instead. "
|
|
40
|
+
"See /docs/hook_system_migration_guide.md for migration instructions.",
|
|
41
|
+
DeprecationWarning,
|
|
42
|
+
stacklevel=2
|
|
43
|
+
)
|
|
44
|
+
self.base_url = base_url.rstrip('/')
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
|
|
47
|
+
# Setup session with retry logic
|
|
48
|
+
self.session = requests.Session()
|
|
49
|
+
retry_strategy = Retry(
|
|
50
|
+
total=3,
|
|
51
|
+
backoff_factor=1,
|
|
52
|
+
status_forcelist=[429, 500, 502, 503, 504]
|
|
53
|
+
)
|
|
54
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
55
|
+
self.session.mount("http://", adapter)
|
|
56
|
+
self.session.mount("https://", adapter)
|
|
57
|
+
|
|
58
|
+
def health_check(self) -> Dict[str, Any]:
|
|
59
|
+
"""Check health of hook service.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Health status dictionary
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
response = self.session.get(
|
|
66
|
+
urljoin(self.base_url, '/health'),
|
|
67
|
+
timeout=self.timeout
|
|
68
|
+
)
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response.json()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Health check failed: {e}")
|
|
73
|
+
return {
|
|
74
|
+
'status': 'unhealthy',
|
|
75
|
+
'error': str(e)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
79
|
+
"""List all registered hooks.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary mapping hook types to hook info
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
response = self.session.get(
|
|
86
|
+
urljoin(self.base_url, '/hooks/list'),
|
|
87
|
+
timeout=self.timeout
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
data = response.json()
|
|
91
|
+
return data.get('hooks', {})
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Failed to list hooks: {e}")
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
def execute_hook(self, hook_type: HookType, context_data: Dict[str, Any],
|
|
97
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
98
|
+
specific_hook: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
99
|
+
"""Execute hooks of a given type.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
hook_type: Type of hooks to execute
|
|
103
|
+
context_data: Data to pass to hooks
|
|
104
|
+
metadata: Optional metadata
|
|
105
|
+
specific_hook: Optional specific hook name to execute
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of execution results
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
payload = {
|
|
112
|
+
'hook_type': hook_type.value,
|
|
113
|
+
'context': context_data,
|
|
114
|
+
'metadata': metadata or {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if specific_hook:
|
|
118
|
+
payload['hook_name'] = specific_hook
|
|
119
|
+
|
|
120
|
+
response = self.session.post(
|
|
121
|
+
urljoin(self.base_url, '/hooks/execute'),
|
|
122
|
+
json=payload,
|
|
123
|
+
timeout=self.timeout
|
|
124
|
+
)
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
data = response.json()
|
|
127
|
+
|
|
128
|
+
if data.get('status') == 'success':
|
|
129
|
+
return data.get('results', [])
|
|
130
|
+
else:
|
|
131
|
+
logger.error(f"Hook execution failed: {data.get('error')}")
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Failed to execute hooks: {e}")
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
def execute_submit_hook(self, prompt: str, **kwargs) -> List[Dict[str, Any]]:
|
|
139
|
+
"""Execute submit hooks on a user prompt.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
prompt: User prompt to process
|
|
143
|
+
**kwargs: Additional context data
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of execution results
|
|
147
|
+
"""
|
|
148
|
+
context_data = {'prompt': prompt}
|
|
149
|
+
context_data.update(kwargs)
|
|
150
|
+
return self.execute_hook(HookType.SUBMIT, context_data)
|
|
151
|
+
|
|
152
|
+
def execute_pre_delegation_hook(self, agent: str, context: Dict[str, Any],
|
|
153
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
154
|
+
"""Execute pre-delegation hooks.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
agent: Agent being delegated to
|
|
158
|
+
context: Context being passed to agent
|
|
159
|
+
**kwargs: Additional data
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of execution results
|
|
163
|
+
"""
|
|
164
|
+
context_data = {
|
|
165
|
+
'agent': agent,
|
|
166
|
+
'context': context
|
|
167
|
+
}
|
|
168
|
+
context_data.update(kwargs)
|
|
169
|
+
return self.execute_hook(HookType.PRE_DELEGATION, context_data)
|
|
170
|
+
|
|
171
|
+
def execute_post_delegation_hook(self, agent: str, result: Any,
|
|
172
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
173
|
+
"""Execute post-delegation hooks.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
agent: Agent that was delegated to
|
|
177
|
+
result: Result from agent
|
|
178
|
+
**kwargs: Additional data
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of execution results
|
|
182
|
+
"""
|
|
183
|
+
context_data = {
|
|
184
|
+
'agent': agent,
|
|
185
|
+
'result': result
|
|
186
|
+
}
|
|
187
|
+
context_data.update(kwargs)
|
|
188
|
+
return self.execute_hook(HookType.POST_DELEGATION, context_data)
|
|
189
|
+
|
|
190
|
+
def execute_ticket_extraction_hook(self, content: Any,
|
|
191
|
+
**kwargs) -> List[Dict[str, Any]]:
|
|
192
|
+
"""Execute ticket extraction hooks.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
content: Content to extract tickets from
|
|
196
|
+
**kwargs: Additional data
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of execution results
|
|
200
|
+
"""
|
|
201
|
+
context_data = {'content': content}
|
|
202
|
+
context_data.update(kwargs)
|
|
203
|
+
return self.execute_hook(HookType.TICKET_EXTRACTION, context_data)
|
|
204
|
+
|
|
205
|
+
def get_modified_data(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
206
|
+
"""Extract modified data from hook results.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
results: Hook execution results
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Combined modified data from all hooks
|
|
213
|
+
"""
|
|
214
|
+
modified_data = {}
|
|
215
|
+
|
|
216
|
+
for result in results:
|
|
217
|
+
if result.get('modified') and result.get('data'):
|
|
218
|
+
modified_data.update(result['data'])
|
|
219
|
+
|
|
220
|
+
return modified_data
|
|
221
|
+
|
|
222
|
+
def get_extracted_tickets(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
223
|
+
"""Extract tickets from hook results.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
results: Hook execution results
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of extracted tickets
|
|
230
|
+
"""
|
|
231
|
+
all_tickets = []
|
|
232
|
+
|
|
233
|
+
for result in results:
|
|
234
|
+
if result.get('success') and 'tickets' in result.get('data', {}):
|
|
235
|
+
tickets = result['data']['tickets']
|
|
236
|
+
if isinstance(tickets, list):
|
|
237
|
+
all_tickets.extend(tickets)
|
|
238
|
+
|
|
239
|
+
return all_tickets
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Convenience function for creating a default client
|
|
243
|
+
def get_hook_client(base_url: Optional[str] = None) -> 'JSONRPCHookClient':
|
|
244
|
+
"""Get a hook client instance.
|
|
245
|
+
|
|
246
|
+
DEPRECATED: This function now returns a JSONRPCHookClient for compatibility.
|
|
247
|
+
Import directly from claude_mpm.hooks.json_rpc_hook_client instead.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
base_url: Ignored (kept for backward compatibility)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
JSONRPCHookClient instance
|
|
254
|
+
"""
|
|
255
|
+
warnings.warn(
|
|
256
|
+
"get_hook_client from hook_client module is deprecated. "
|
|
257
|
+
"Import from claude_mpm.hooks.json_rpc_hook_client instead.",
|
|
258
|
+
DeprecationWarning,
|
|
259
|
+
stacklevel=2
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Import and return JSON-RPC client for compatibility
|
|
263
|
+
from claude_mpm.hooks.json_rpc_hook_client import JSONRPCHookClient
|
|
264
|
+
return JSONRPCHookClient()
|