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.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. 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()