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,801 +0,0 @@
1
- """Subprocess orchestrator that mimics Claude's Task tool for non-interactive mode."""
2
-
3
- import concurrent.futures
4
- import json
5
- import time
6
- import logging
7
- import subprocess
8
- from pathlib import Path
9
- from typing import Dict, List, Any, Optional, Tuple
10
- from datetime import datetime
11
- import re
12
-
13
- try:
14
- from ..core.logger import get_logger, setup_logging
15
- from ..utils.subprocess_runner import SubprocessRunner
16
- # TicketExtractor removed from project
17
- from ..core.framework_loader import FrameworkLoader
18
- from ..core.claude_launcher import ClaudeLauncher, LaunchMode
19
- from .agent_delegator import AgentDelegator
20
- from ..hooks.hook_client import HookServiceClient
21
- from .todo_hijacker import TodoHijacker
22
- from ..core.logger import get_project_logger
23
- from ..core.tool_access_control import tool_access_control
24
- from ..core.agent_name_normalizer import agent_name_normalizer
25
- except ImportError:
26
- from core.logger import get_logger, setup_logging
27
- from utils.subprocess_runner import SubprocessRunner
28
- # TicketExtractor removed from project
29
- from core.framework_loader import FrameworkLoader
30
- from core.claude_launcher import ClaudeLauncher, LaunchMode
31
- from orchestration.agent_delegator import AgentDelegator
32
- from orchestration.todo_hijacker import TodoHijacker
33
- from core.logger import get_project_logger
34
- from core.tool_access_control import tool_access_control
35
- from core.agent_name_normalizer import agent_name_normalizer
36
- try:
37
- from hooks.hook_client import HookServiceClient
38
- except ImportError:
39
- HookServiceClient = None
40
-
41
-
42
- class SubprocessOrchestrator:
43
- """
44
- Orchestrator that creates real subprocesses for agent delegations.
45
- Mimics Claude's built-in Task tool behavior for non-interactive mode.
46
- """
47
-
48
- def __init__(
49
- self,
50
- framework_path: Optional[Path] = None,
51
- agents_dir: Optional[Path] = None,
52
- log_level: str = "OFF",
53
- log_dir: Optional[Path] = None,
54
- hook_manager=None,
55
- enable_todo_hijacking: bool = False,
56
- ):
57
- """Initialize the subprocess orchestrator."""
58
- self.log_level = log_level
59
- self.log_dir = log_dir or (Path.home() / ".claude-mpm" / "logs")
60
- self.hook_manager = hook_manager
61
-
62
- # Initialize unified Claude launcher
63
- self.launcher = ClaudeLauncher(model="opus", skip_permissions=True, log_level=log_level)
64
- self.enable_todo_hijacking = enable_todo_hijacking
65
-
66
- # Set up logging
67
- if log_level != "OFF":
68
- self.logger = setup_logging(level=log_level, log_dir=log_dir)
69
- self.logger.info(f"Initializing Subprocess Orchestrator (log_level={log_level})")
70
- if hook_manager and hook_manager.is_available():
71
- self.logger.info(f"Hook service available on port {hook_manager.port}")
72
- else:
73
- self.logger = get_logger("subprocess_orchestrator")
74
- self.logger.setLevel(logging.WARNING)
75
-
76
- # Components
77
- self.framework_loader = FrameworkLoader(framework_path, agents_dir)
78
- # TicketExtractor removed from project
79
- self.agent_delegator = AgentDelegator(self.framework_loader.agent_registry)
80
-
81
- # Initialize TODO hijacker if enabled
82
- self.todo_hijacker = None
83
- if enable_todo_hijacking:
84
- self.todo_hijacker = TodoHijacker(
85
- log_level=log_level,
86
- on_delegation=self._handle_todo_delegation
87
- )
88
- self.logger.info("TODO hijacking enabled")
89
-
90
- # Initialize hook client if available
91
- self.hook_client = None
92
- if self.hook_manager and self.hook_manager.is_available() and HookServiceClient:
93
- try:
94
- hook_info = self.hook_manager.get_service_info()
95
- if hook_info and 'url' in hook_info:
96
- self.hook_client = HookServiceClient(base_url=hook_info['url'])
97
- # Test connection
98
- health = self.hook_client.health_check()
99
- if health.get('status') == 'healthy':
100
- self.logger.info(f"Connected to hook service with {health.get('hooks_count', 0)} hooks")
101
- else:
102
- self.logger.warning("Hook service not healthy, disabling hooks")
103
- self.hook_client = None
104
- else:
105
- self.logger.debug("Hook service info missing 'url' key, skipping hook client initialization")
106
- except Exception as e:
107
- self.logger.warning(f"Failed to initialize hook client: {e}")
108
- self.hook_client = None
109
-
110
- # State
111
- self.session_start = datetime.now()
112
- # Ticket creation removed from project
113
- self._pending_todo_delegations = []
114
-
115
- # Initialize subprocess runner
116
- self.subprocess_runner = SubprocessRunner(logger=self.logger)
117
-
118
- # Initialize project logger
119
- self.project_logger = get_project_logger(log_level)
120
- self.project_logger.log_system(
121
- f"Initializing Subprocess Orchestrator (log_level={log_level})",
122
- level="INFO",
123
- component="orchestrator"
124
- )
125
-
126
- def detect_delegations(self, response: str) -> List[Dict[str, str]]:
127
- """
128
- Detect delegation requests in PM response.
129
-
130
- Looks for patterns like:
131
- - **Engineer Agent**: Create a function...
132
- - Task(Engineer role summary)
133
- - Delegate to Engineer: implement...
134
-
135
- Returns:
136
- List of delegations with agent and task
137
- """
138
- delegations = []
139
-
140
- # Pattern 1: **Agent Name**: task
141
- pattern1 = r'\*\*([^*]+)(?:\s+Agent)?\*\*:\s*(.+?)(?=\n\n|\n\*\*|$)'
142
- for match in re.finditer(pattern1, response, re.MULTILINE | re.DOTALL):
143
- agent = agent_name_normalizer.normalize(match.group(1).strip())
144
- task = match.group(2).strip()
145
- delegations.append({
146
- 'agent': agent,
147
- 'task': task,
148
- 'format': 'markdown'
149
- })
150
-
151
- # Pattern 2: Task(description)
152
- pattern2 = r'Task\(([^)]+)\)'
153
- for match in re.finditer(pattern2, response):
154
- task_desc = match.group(1).strip()
155
- # Try to extract agent from task description
156
- agent = self._extract_agent_from_task(task_desc)
157
- delegations.append({
158
- 'agent': agent,
159
- 'task': task_desc,
160
- 'format': 'task_tool'
161
- })
162
-
163
- if self.log_level != "OFF":
164
- self.logger.info(f"Detected {len(delegations)} delegations")
165
- for d in delegations:
166
- self.logger.debug(f" {d['agent']}: {d['task'][:50]}...")
167
-
168
- return delegations
169
-
170
- def _extract_agent_from_task(self, task: str) -> str:
171
- """Extract agent name from task description."""
172
- # Try to extract from TODO format first
173
- agent = agent_name_normalizer.extract_from_todo(task)
174
- if agent:
175
- return agent
176
-
177
- # Otherwise, look for agent mentions in the task
178
- task_lower = task.lower()
179
-
180
- # Check for explicit agent names
181
- agents = ['engineer', 'qa', 'documentation', 'research',
182
- 'security', 'ops', 'version control', 'data engineer']
183
-
184
- for agent in agents:
185
- if agent in task_lower:
186
- return agent_name_normalizer.normalize(agent)
187
-
188
- # Use agent delegator to suggest based on task
189
- suggested = self.agent_delegator.suggest_agent_for_task(task)
190
- return agent_name_normalizer.normalize(suggested) if suggested else "Engineer"
191
-
192
- def create_agent_prompt(self, agent: str, task: str) -> str:
193
- """
194
- Create a prompt for an agent subprocess.
195
-
196
- Args:
197
- agent: Agent name
198
- task: Task description
199
-
200
- Returns:
201
- Complete prompt including agent-specific framework
202
- """
203
- # Normalize agent name
204
- normalized_agent = agent_name_normalizer.normalize(agent)
205
-
206
- # Get agent-specific content
207
- agent_content = ""
208
- agent_key = agent_name_normalizer.to_key(normalized_agent) + '_agent'
209
-
210
- if agent_key in self.framework_loader.framework_content.get('agents', {}):
211
- agent_content = self.framework_loader.framework_content['agents'][agent_key]
212
-
213
- # Get TODO guidance for this agent
214
- todo_guidance = tool_access_control.get_todo_guidance(agent)
215
-
216
- # Build focused agent prompt
217
- prompt = f"""You are the {normalized_agent} Agent in the Claude PM Framework.
218
-
219
- {agent_content}
220
-
221
- ## Tool Access and Task Tracking
222
- {todo_guidance}
223
-
224
- ## Current Task
225
- {task}
226
-
227
- ## Response Format
228
- Provide a clear, structured response that:
229
- 1. Confirms your role as {normalized_agent} Agent
230
- 2. Completes the requested task
231
- 3. Reports any issues or blockers
232
- 4. Summarizes deliverables
233
- 5. Lists any follow-up tasks using TODO format
234
-
235
- ## TODO Reporting Format
236
- When identifying tasks for other agents, use this format:
237
- TODO (Priority): [Agent] Task description
238
-
239
- Example:
240
- TODO (High Priority): [Research] Analyze authentication patterns
241
- TODO (Medium Priority): [QA] Write tests for new endpoints
242
-
243
- Remember: You are an autonomous agent. Complete the task independently and report results."""
244
-
245
- return prompt
246
-
247
- def run_subprocess(self, agent: str, task: str) -> Tuple[str, float, int]:
248
- """
249
- Run a single agent subprocess.
250
-
251
- Args:
252
- agent: Agent name
253
- task: Task description
254
-
255
- Returns:
256
- Tuple of (response, execution_time, token_count)
257
- """
258
- start_time = time.time()
259
-
260
- # Pre-delegation hook
261
- if self.hook_client:
262
- try:
263
- self.logger.info(f"Calling pre-delegation hook for {agent}")
264
- hook_results = self.hook_client.execute_pre_delegation_hook(
265
- agent=agent,
266
- context={"task": task}
267
- )
268
- if hook_results:
269
- self.logger.info(f"Pre-delegation hook executed: {len(hook_results)} hooks")
270
- # Check for any modifications
271
- modified = self.hook_client.get_modified_data(hook_results)
272
- if modified and 'task' in modified:
273
- task = modified['task']
274
- self.logger.info(f"Task modified by hook: {task[:50]}...")
275
- except Exception as e:
276
- self.logger.warning(f"Pre-delegation hook error: {e}")
277
-
278
- # Create agent prompt
279
- prompt = self.create_agent_prompt(agent, task)
280
-
281
- # Log prompt size
282
- token_estimate = len(prompt) // 4 # Rough estimate
283
- if self.log_level != "OFF":
284
- self.logger.info(f"Running subprocess for {agent} ({token_estimate} est. tokens)")
285
-
286
- # Log the agent invocation
287
- self.project_logger.log_system(
288
- f"Invoking {agent} agent for task: {task[:100]}",
289
- level="INFO",
290
- component="orchestrator"
291
- )
292
-
293
- try:
294
- # Normalize agent name for consistent tool access
295
- normalized_agent = agent_name_normalizer.normalize(agent)
296
-
297
- # Get allowed tools for this agent type
298
- allowed_tools = tool_access_control.format_allowed_tools_arg(normalized_agent, is_parent=False)
299
-
300
- if self.log_level != "OFF":
301
- self.logger.info(f"Applying tool restrictions for {normalized_agent}: {allowed_tools}")
302
-
303
- # Create command with tool restrictions
304
- cmd = [self.launcher.claude_path, "--dangerously-skip-permissions"]
305
- if self.launcher.model:
306
- cmd.extend(["--model", self.launcher.model])
307
-
308
- # Apply tool restrictions for the agent
309
- cmd.extend(["--allowedTools", allowed_tools])
310
-
311
- # Use subprocess directly for more control
312
- import subprocess
313
- process = subprocess.Popen(
314
- cmd,
315
- stdin=subprocess.PIPE,
316
- stdout=subprocess.PIPE,
317
- stderr=subprocess.PIPE,
318
- text=True
319
- )
320
-
321
- stdout, stderr = process.communicate(input=prompt, timeout=60)
322
- returncode = process.returncode
323
-
324
- execution_time = time.time() - start_time
325
-
326
- if returncode == 0:
327
- response = stdout.strip()
328
- # Estimate response tokens
329
- total_tokens = len(prompt + response) // 4
330
-
331
- # Post-delegation hook
332
- if self.hook_client:
333
- try:
334
- self.logger.info(f"Calling post-delegation hook for {agent}")
335
- hook_results = self.hook_client.execute_post_delegation_hook(
336
- agent=agent,
337
- result={
338
- "task": task,
339
- "response": response,
340
- "execution_time": execution_time,
341
- "tokens": total_tokens
342
- }
343
- )
344
- if hook_results:
345
- self.logger.info(f"Post-delegation hook executed: {len(hook_results)} hooks")
346
- # Ticket extraction removed from project
347
- except Exception as e:
348
- self.logger.warning(f"Post-delegation hook error: {e}")
349
-
350
- # Log agent invocation with full details
351
- self.project_logger.log_agent_invocation(
352
- agent=agent,
353
- task=task,
354
- prompt=prompt,
355
- response=response,
356
- execution_time=execution_time,
357
- tokens=total_tokens,
358
- success=True,
359
- metadata={
360
- "session_id": getattr(self, 'current_session_id', None),
361
- "delegation_format": "subprocess"
362
- }
363
- )
364
-
365
- return response, execution_time, total_tokens
366
- else:
367
- error_msg = f"Subprocess failed: {stderr}"
368
- if self.log_level != "OFF":
369
- self.logger.error(f"{agent} subprocess error: {error_msg}")
370
- return error_msg, execution_time, token_estimate
371
-
372
- except subprocess.TimeoutExpired:
373
- execution_time = time.time() - start_time
374
- error_msg = f"Subprocess timed out after {execution_time:.1f}s"
375
- if self.log_level != "OFF":
376
- self.logger.error(f"{agent} {error_msg}")
377
-
378
- # Log timeout error
379
- self.project_logger.log_agent_invocation(
380
- agent=agent,
381
- task=task,
382
- prompt=prompt,
383
- response=error_msg,
384
- execution_time=execution_time,
385
- tokens=token_estimate,
386
- success=False,
387
- metadata={
388
- "error": "timeout",
389
- "delegation_format": "subprocess"
390
- }
391
- )
392
-
393
- return error_msg, execution_time, token_estimate
394
- except Exception as e:
395
- execution_time = time.time() - start_time
396
- error_msg = f"Subprocess error: {str(e)}"
397
- if self.log_level != "OFF":
398
- self.logger.error(f"{agent} {error_msg}")
399
-
400
- # Log exception error
401
- self.project_logger.log_agent_invocation(
402
- agent=agent,
403
- task=task,
404
- prompt=prompt,
405
- response=error_msg,
406
- execution_time=execution_time,
407
- tokens=token_estimate,
408
- success=False,
409
- metadata={
410
- "error": "exception",
411
- "exception_type": type(e).__name__,
412
- "delegation_format": "subprocess"
413
- }
414
- )
415
-
416
- return error_msg, execution_time, token_estimate
417
-
418
- def _handle_todo_delegation(self, delegation: Dict[str, Any]):
419
- """
420
- Handle a delegation created from a TODO.
421
-
422
- Args:
423
- delegation: Delegation dict from TodoHijacker
424
- """
425
- self.logger.info(f"TODO delegation received: {delegation['agent']} - {delegation['task'][:50]}...")
426
- self._pending_todo_delegations.append(delegation)
427
-
428
- def _process_pending_todo_delegations(self) -> List[Dict[str, Any]]:
429
- """
430
- Process any pending TODO delegations.
431
-
432
- Returns:
433
- List of results from running the delegations
434
- """
435
- if not self._pending_todo_delegations:
436
- return []
437
-
438
- self.logger.info(f"Processing {len(self._pending_todo_delegations)} TODO delegations")
439
-
440
- # Run the delegations
441
- results = self.run_parallel_tasks(self._pending_todo_delegations)
442
-
443
- # Clear pending list
444
- self._pending_todo_delegations.clear()
445
-
446
- return results
447
-
448
- def run_parallel_tasks(self, delegations: List[Dict[str, str]]) -> List[Dict[str, Any]]:
449
- """
450
- Run multiple agent tasks in parallel.
451
-
452
- Args:
453
- delegations: List of delegation dicts with 'agent' and 'task'
454
-
455
- Returns:
456
- List of results with agent, response, timing, and tokens
457
- """
458
- results = []
459
-
460
- # Use ThreadPoolExecutor for parallel execution
461
- with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
462
- # Submit all tasks
463
- future_to_delegation = {}
464
- for delegation in delegations:
465
- future = executor.submit(
466
- self.run_subprocess,
467
- delegation['agent'],
468
- delegation['task']
469
- )
470
- future_to_delegation[future] = delegation
471
-
472
- # Collect results as they complete
473
- for future in concurrent.futures.as_completed(future_to_delegation):
474
- delegation = future_to_delegation[future]
475
- try:
476
- response, exec_time, tokens = future.result()
477
- results.append({
478
- 'agent': delegation['agent'],
479
- 'task': delegation['task'],
480
- 'response': response,
481
- 'execution_time': exec_time,
482
- 'tokens': tokens,
483
- 'status': 'completed'
484
- })
485
- except Exception as e:
486
- results.append({
487
- 'agent': delegation['agent'],
488
- 'task': delegation['task'],
489
- 'response': str(e),
490
- 'execution_time': 0,
491
- 'tokens': 0,
492
- 'status': 'failed'
493
- })
494
-
495
- return results
496
-
497
- def format_results(self, results: List[Dict[str, Any]]) -> str:
498
- """
499
- Format subprocess results in Claude Task tool style.
500
-
501
- Args:
502
- results: List of result dicts
503
-
504
- Returns:
505
- Formatted output mimicking Claude's Task tool
506
- """
507
- output = []
508
-
509
- # Show task executions
510
- for result in results:
511
- status_icon = "⏺" if result['status'] == 'completed' else "❌"
512
- tokens_k = result['tokens'] / 1000
513
-
514
- output.append(f"{status_icon} Task({result['task'][:50]}...)")
515
- output.append(f" ⎿ Done (0 tool uses · {tokens_k:.1f}k tokens · {result['execution_time']:.1f}s)")
516
- output.append("")
517
-
518
- # Aggregate responses
519
- output.append("## Agent Responses\n")
520
-
521
- for result in results:
522
- # Use colorized agent name for display
523
- agent_display = agent_name_normalizer.colorize(result['agent'], f"{agent_name_normalizer.normalize(result['agent'])} Agent")
524
- output.append(f"### {agent_display}")
525
- output.append(result['response'])
526
- output.append("")
527
-
528
- return "\n".join(output)
529
-
530
- def run_non_interactive(self, user_input: str):
531
- """
532
- Run non-interactive session with subprocess orchestration.
533
-
534
- This method:
535
- 1. Runs PM with user input
536
- 2. Detects delegations in PM response
537
- 3. Creates subprocesses for each delegation
538
- 4. Aggregates and displays results
539
- 5. Processes any TODO-based delegations if hijacking is enabled
540
- """
541
- try:
542
- # Log session start
543
- self.project_logger.log_system(
544
- f"Starting non-interactive session with input: {user_input[:100]}",
545
- level="INFO",
546
- component="orchestrator"
547
- )
548
-
549
- # Start TODO monitoring if enabled
550
- if self.todo_hijacker:
551
- self.todo_hijacker.start_monitoring()
552
- # Submit hook for user input
553
- if self.hook_client:
554
- try:
555
- self.logger.info("Calling submit hook for user input")
556
- hook_results = self.hook_client.execute_submit_hook(
557
- prompt=user_input,
558
- session_type="subprocess"
559
- )
560
- if hook_results:
561
- self.logger.info(f"Submit hook executed: {len(hook_results)} hooks")
562
- except Exception as e:
563
- self.logger.warning(f"Submit hook error: {e}")
564
- # Prepare PM prompt with minimal framework
565
- try:
566
- from ..core.minimal_framework_loader import MinimalFrameworkLoader
567
- except ImportError:
568
- from core.minimal_framework_loader import MinimalFrameworkLoader
569
-
570
- minimal_loader = MinimalFrameworkLoader(self.framework_loader.framework_path)
571
- framework = minimal_loader.get_framework_instructions()
572
-
573
- # Add instruction to use delegation format
574
- framework += """
575
- ## Delegation Format
576
- When delegating tasks, use this exact format:
577
- **[Agent Name]**: [Task description]
578
-
579
- Example:
580
- **Engineer**: Create a function that calculates factorial
581
- **QA**: Write tests for the factorial function
582
- """
583
-
584
- full_message = framework + "\n\nUser: " + user_input
585
-
586
- if self.log_level != "OFF":
587
- self.logger.info("Running PM with user input")
588
-
589
- # Get allowed tools for PM
590
- pm_allowed_tools = tool_access_control.format_allowed_tools_arg("pm", is_parent=True)
591
-
592
- if self.log_level != "OFF":
593
- self.logger.info(f"Applying PM tool restrictions: {pm_allowed_tools}")
594
-
595
- # Use unified launcher for PM execution with tool restrictions
596
- # Create command with tool restrictions for PM
597
- cmd = [self.launcher.claude_path, "--dangerously-skip-permissions"]
598
- if self.launcher.model:
599
- cmd.extend(["--model", self.launcher.model])
600
-
601
- # Restrict PM to only delegation and tracking tools
602
- cmd.extend(["--allowedTools", pm_allowed_tools])
603
-
604
- # Use subprocess directly for more control
605
- import subprocess
606
- process = subprocess.Popen(
607
- cmd,
608
- stdin=subprocess.PIPE,
609
- stdout=subprocess.PIPE,
610
- stderr=subprocess.PIPE,
611
- text=True
612
- )
613
-
614
- stdout, stderr = process.communicate(input=full_message, timeout=30)
615
- returncode = process.returncode
616
-
617
- if returncode != 0:
618
- print(f"Error: {stderr}")
619
- return
620
-
621
- pm_response = stdout
622
- print("PM Response:")
623
- print("-" * 50)
624
- print(pm_response)
625
- print("-" * 50)
626
-
627
- # Log PM response in DEBUG mode
628
- if self.log_level == "DEBUG":
629
- self.project_logger.log_system(
630
- f"PM Response (full): {pm_response}",
631
- level="DEBUG",
632
- component="orchestrator"
633
- )
634
- # Also save PM response to cache for analysis
635
- from pathlib import Path
636
- cache_dir = Path.cwd() / ".claude-mpm" / "cache"
637
- cache_dir.mkdir(parents=True, exist_ok=True)
638
- pm_cache_file = cache_dir / f"pm_response_{self.project_logger.session_id}.txt"
639
- pm_cache_file.write_text(pm_response)
640
-
641
- # Detect delegations
642
- delegations = self.detect_delegations(pm_response)
643
-
644
- # Log delegation detection
645
- if delegations:
646
- self.project_logger.log_delegation(
647
- pm_task=user_input,
648
- delegations=delegations,
649
- pm_response=pm_response
650
- )
651
-
652
- if delegations:
653
- print(f"\nDetected {len(delegations)} delegations. Running subprocesses...\n")
654
-
655
- # Run delegations in parallel
656
- results = self.run_parallel_tasks(delegations)
657
-
658
- # Format and display results
659
- formatted_results = self.format_results(results)
660
- print(formatted_results)
661
-
662
- # Ticket extraction removed from project
663
- else:
664
- print("\nNo delegations detected in PM response.")
665
-
666
- # Process any TODO-based delegations
667
- if self.todo_hijacker:
668
- # Give a moment for TODO files to be written
669
- time.sleep(0.5)
670
-
671
- # Process pending TODO delegations
672
- todo_results = self._process_pending_todo_delegations()
673
- if todo_results:
674
- print(f"\nProcessed {len(todo_results)} TODO-based delegations:")
675
- formatted_todo_results = self.format_results(todo_results)
676
- print(formatted_todo_results)
677
-
678
- # Ticket creation removed from project
679
-
680
- # Create session report
681
- self.project_logger.create_session_report()
682
-
683
- except Exception as e:
684
- print(f"Error: {e}")
685
- if self.log_level != "OFF":
686
- self.logger.error(f"Non-interactive error: {e}")
687
- finally:
688
- # Stop TODO monitoring
689
- if self.todo_hijacker:
690
- self.todo_hijacker.stop_monitoring()
691
-
692
- def run_interactive(self):
693
- """Run an interactive session with framework instructions."""
694
- try:
695
- from .._version import __version__
696
- except ImportError:
697
- from claude_mpm._version import __version__
698
-
699
- print(f"Claude MPM v{__version__} - Interactive Session")
700
- print("Starting Claude with framework instructions...")
701
- print("-" * 50)
702
-
703
- # Get framework instructions
704
- framework = self.framework_loader.get_framework_instructions()
705
-
706
- # Submit hook for framework initialization if available
707
- if self.hook_client:
708
- hook_response = self.hook_client.execute_submit_hook(
709
- "Starting interactive session with framework",
710
- hook_type='framework_init',
711
- framework=framework,
712
- session_mode='interactive'
713
- )
714
- if hook_response and 'framework' in hook_response:
715
- framework = hook_response['framework']
716
-
717
- if self.log_level != "OFF":
718
- self.logger.info("Starting Claude with framework instructions")
719
- self.logger.info(f"Framework size: {len(framework)} bytes")
720
-
721
- print(f"\nFramework version: {self.framework_loader.framework_content.get('version', 'unknown')}")
722
- print(f"Framework size: {len(framework):,} bytes")
723
-
724
- # Display agent versions
725
- print("\nAgent versions (base-agent):")
726
- try:
727
- from pathlib import Path
728
- import json
729
- agents_dir = Path.cwd() / ".claude" / "agents"
730
- if agents_dir.exists():
731
- agent_files = sorted(agents_dir.glob("*.yml"))
732
- if agent_files:
733
- for agent_file in agent_files[:8]: # Show up to 8 agents
734
- try:
735
- with open(agent_file, 'r') as f:
736
- for line in f:
737
- if line.startswith("version:"):
738
- version = line.split(":", 1)[1].strip().strip('"')
739
- agent_name = agent_file.stem
740
- print(f" - {agent_name:<20} {version}")
741
- break
742
- except:
743
- pass
744
- else:
745
- print(" No agents deployed")
746
- else:
747
- print(" No agents directory found")
748
- except Exception as e:
749
- print(f" Error reading agent versions: {e}")
750
-
751
- print("-" * 50)
752
-
753
- try:
754
- import subprocess
755
- import sys
756
-
757
- # Build command with --append-system-prompt
758
- cmd = [self.launcher.claude_path, "--dangerously-skip-permissions"]
759
-
760
- # Add model if specified
761
- if self.launcher.model:
762
- cmd.extend(["--model", self.launcher.model])
763
-
764
- # Add the framework as system prompt
765
- cmd.extend(["--append-system-prompt", framework])
766
-
767
- # Get PM tool restrictions
768
- pm_allowed_tools = tool_access_control.format_allowed_tools_arg("pm", is_parent=True)
769
-
770
- # Restrict PM to only delegation and tracking tools
771
- cmd.extend(["--allowedTools", pm_allowed_tools])
772
-
773
- if self.log_level != "OFF":
774
- self.logger.debug(f"Launching Claude with command: {' '.join(cmd[:4])}... (framework omitted)")
775
-
776
- # Launch Claude directly with inherited stdio
777
- process = subprocess.Popen(
778
- cmd,
779
- stdin=sys.stdin,
780
- stdout=sys.stdout,
781
- stderr=sys.stderr,
782
- text=True
783
- )
784
-
785
- # Wait for process to complete
786
- process.wait()
787
-
788
- # After session ends, extract and create tickets
789
- # Note: In interactive mode, we can't capture output directly
790
- # so ticket extraction would need to be handled differently
791
-
792
- except KeyboardInterrupt:
793
- print("\nSession interrupted by user")
794
- if self.log_level != "OFF":
795
- self.logger.info("Interactive session interrupted")
796
- except Exception as e:
797
- print(f"Error running Claude: {e}")
798
- if self.log_level != "OFF":
799
- self.logger.error(f"Interactive error: {e}")
800
-
801
- # _create_tickets method removed - TicketExtractor functionality removed from project