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,511 @@
|
|
|
1
|
+
"""Simplified Claude runner replacing the complex orchestrator system."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from claude_mpm.services.agent_deployment import AgentDeploymentService
|
|
14
|
+
from claude_mpm.services.ticket_manager import TicketManager
|
|
15
|
+
from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
|
|
16
|
+
except ImportError:
|
|
17
|
+
from claude_mpm.services.agent_deployment import AgentDeploymentService
|
|
18
|
+
from claude_mpm.services.ticket_manager import TicketManager
|
|
19
|
+
from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SimpleClaudeRunner:
|
|
23
|
+
"""
|
|
24
|
+
Simplified Claude runner that replaces the entire orchestrator system.
|
|
25
|
+
|
|
26
|
+
This does exactly what we need:
|
|
27
|
+
1. Deploy native agents to .claude/agents/
|
|
28
|
+
2. Run Claude CLI with basic subprocess calls
|
|
29
|
+
3. Extract tickets if needed
|
|
30
|
+
4. Handle both interactive and non-interactive modes
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
enable_tickets: bool = True,
|
|
36
|
+
log_level: str = "OFF",
|
|
37
|
+
claude_args: Optional[list] = None
|
|
38
|
+
):
|
|
39
|
+
"""Initialize the simple runner."""
|
|
40
|
+
self.enable_tickets = enable_tickets
|
|
41
|
+
self.log_level = log_level
|
|
42
|
+
self.logger = get_logger("simple_runner")
|
|
43
|
+
self.claude_args = claude_args or []
|
|
44
|
+
|
|
45
|
+
# Initialize project logger for session logging
|
|
46
|
+
self.project_logger = None
|
|
47
|
+
if log_level != "OFF":
|
|
48
|
+
try:
|
|
49
|
+
self.project_logger = get_project_logger(log_level)
|
|
50
|
+
self.project_logger.log_system(
|
|
51
|
+
"Initializing SimpleClaudeRunner",
|
|
52
|
+
level="INFO",
|
|
53
|
+
component="runner"
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
self.logger.warning(f"Failed to initialize project logger: {e}")
|
|
57
|
+
|
|
58
|
+
# Initialize services
|
|
59
|
+
self.deployment_service = AgentDeploymentService()
|
|
60
|
+
if enable_tickets:
|
|
61
|
+
try:
|
|
62
|
+
self.ticket_manager = TicketManager()
|
|
63
|
+
except (ImportError, TypeError, Exception) as e:
|
|
64
|
+
self.logger.warning(f"Ticket manager not available: {e}")
|
|
65
|
+
self.ticket_manager = None
|
|
66
|
+
self.enable_tickets = False
|
|
67
|
+
|
|
68
|
+
# Load system instructions
|
|
69
|
+
self.system_instructions = self._load_system_instructions()
|
|
70
|
+
|
|
71
|
+
# Track if we need to create session logs
|
|
72
|
+
self.session_log_file = None
|
|
73
|
+
if self.project_logger and log_level != "OFF":
|
|
74
|
+
try:
|
|
75
|
+
# Create a system.jsonl file in the session directory
|
|
76
|
+
self.session_log_file = self.project_logger.session_dir / "system.jsonl"
|
|
77
|
+
self._log_session_event({
|
|
78
|
+
"event": "session_start",
|
|
79
|
+
"runner": "SimpleClaudeRunner",
|
|
80
|
+
"enable_tickets": enable_tickets,
|
|
81
|
+
"log_level": log_level
|
|
82
|
+
})
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.logger.debug(f"Failed to create session log file: {e}")
|
|
85
|
+
|
|
86
|
+
def setup_agents(self) -> bool:
|
|
87
|
+
"""Deploy native agents to .claude/agents/."""
|
|
88
|
+
try:
|
|
89
|
+
if self.project_logger:
|
|
90
|
+
self.project_logger.log_system(
|
|
91
|
+
"Starting agent deployment",
|
|
92
|
+
level="INFO",
|
|
93
|
+
component="deployment"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
results = self.deployment_service.deploy_agents()
|
|
97
|
+
|
|
98
|
+
if results["deployed"] or results.get("updated", []):
|
|
99
|
+
deployed_count = len(results['deployed'])
|
|
100
|
+
updated_count = len(results.get('updated', []))
|
|
101
|
+
|
|
102
|
+
if deployed_count > 0:
|
|
103
|
+
print(f"✓ Deployed {deployed_count} native agents")
|
|
104
|
+
if updated_count > 0:
|
|
105
|
+
print(f"✓ Updated {updated_count} agents")
|
|
106
|
+
|
|
107
|
+
if self.project_logger:
|
|
108
|
+
self.project_logger.log_system(
|
|
109
|
+
f"Agent deployment successful: {deployed_count} deployed, {updated_count} updated",
|
|
110
|
+
level="INFO",
|
|
111
|
+
component="deployment"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Set Claude environment
|
|
115
|
+
self.deployment_service.set_claude_environment()
|
|
116
|
+
return True
|
|
117
|
+
else:
|
|
118
|
+
self.logger.info("All agents already up to date")
|
|
119
|
+
if self.project_logger:
|
|
120
|
+
self.project_logger.log_system(
|
|
121
|
+
"All agents already up to date",
|
|
122
|
+
level="INFO",
|
|
123
|
+
component="deployment"
|
|
124
|
+
)
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.logger.error(f"Agent deployment failed: {e}")
|
|
129
|
+
print(f"⚠️ Agent deployment failed: {e}")
|
|
130
|
+
if self.project_logger:
|
|
131
|
+
self.project_logger.log_system(
|
|
132
|
+
f"Agent deployment failed: {e}",
|
|
133
|
+
level="ERROR",
|
|
134
|
+
component="deployment"
|
|
135
|
+
)
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def run_interactive(self, initial_context: Optional[str] = None):
|
|
139
|
+
"""Run Claude in interactive mode."""
|
|
140
|
+
# Get version
|
|
141
|
+
try:
|
|
142
|
+
from claude_mpm import __version__
|
|
143
|
+
version_str = f"v{__version__}"
|
|
144
|
+
except:
|
|
145
|
+
version_str = "v0.0.0"
|
|
146
|
+
|
|
147
|
+
# Print styled welcome box
|
|
148
|
+
print("\033[32m╭───────────────────────────────────────────────────╮\033[0m")
|
|
149
|
+
print("\033[32m│\033[0m ✻ Claude MPM - Interactive Session \033[32m│\033[0m")
|
|
150
|
+
print(f"\033[32m│\033[0m Version {version_str:<40}\033[32m│\033[0m")
|
|
151
|
+
print("\033[32m│ │\033[0m")
|
|
152
|
+
print("\033[32m│\033[0m Type '/agents' to see available agents \033[32m│\033[0m")
|
|
153
|
+
print("\033[32m╰───────────────────────────────────────────────────╯\033[0m")
|
|
154
|
+
print("") # Add blank line after box
|
|
155
|
+
|
|
156
|
+
if self.project_logger:
|
|
157
|
+
self.project_logger.log_system(
|
|
158
|
+
"Starting interactive session",
|
|
159
|
+
level="INFO",
|
|
160
|
+
component="session"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Setup agents
|
|
164
|
+
if not self.setup_agents():
|
|
165
|
+
print("Continuing without native agents...")
|
|
166
|
+
|
|
167
|
+
# Build command with system instructions
|
|
168
|
+
cmd = [
|
|
169
|
+
"claude",
|
|
170
|
+
"--model", "opus",
|
|
171
|
+
"--dangerously-skip-permissions"
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Add any custom Claude arguments
|
|
175
|
+
if self.claude_args:
|
|
176
|
+
cmd.extend(self.claude_args)
|
|
177
|
+
|
|
178
|
+
# Add system instructions if available
|
|
179
|
+
system_prompt = self._create_system_prompt()
|
|
180
|
+
if system_prompt and system_prompt != create_simple_context():
|
|
181
|
+
cmd.extend(["--append-system-prompt", system_prompt])
|
|
182
|
+
|
|
183
|
+
# Run interactive Claude directly
|
|
184
|
+
try:
|
|
185
|
+
# Use execvp to replace the current process with Claude
|
|
186
|
+
# This should avoid any subprocess issues
|
|
187
|
+
|
|
188
|
+
# Clean environment
|
|
189
|
+
clean_env = os.environ.copy()
|
|
190
|
+
claude_vars_to_remove = [
|
|
191
|
+
'CLAUDE_CODE_ENTRYPOINT', 'CLAUDECODE', 'CLAUDE_CONFIG_DIR',
|
|
192
|
+
'CLAUDE_MAX_PARALLEL_SUBAGENTS', 'CLAUDE_TIMEOUT'
|
|
193
|
+
]
|
|
194
|
+
for var in claude_vars_to_remove:
|
|
195
|
+
clean_env.pop(var, None)
|
|
196
|
+
|
|
197
|
+
print("Launching Claude...")
|
|
198
|
+
|
|
199
|
+
if self.project_logger:
|
|
200
|
+
self.project_logger.log_system(
|
|
201
|
+
"Launching Claude interactive mode",
|
|
202
|
+
level="INFO",
|
|
203
|
+
component="session"
|
|
204
|
+
)
|
|
205
|
+
self._log_session_event({
|
|
206
|
+
"event": "launching_claude_interactive",
|
|
207
|
+
"command": " ".join(cmd)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
# Replace current process with Claude
|
|
211
|
+
os.execvpe(cmd[0], cmd, clean_env)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"Failed to launch Claude: {e}")
|
|
215
|
+
if self.project_logger:
|
|
216
|
+
self.project_logger.log_system(
|
|
217
|
+
f"Failed to launch Claude: {e}",
|
|
218
|
+
level="ERROR",
|
|
219
|
+
component="session"
|
|
220
|
+
)
|
|
221
|
+
self._log_session_event({
|
|
222
|
+
"event": "interactive_launch_failed",
|
|
223
|
+
"error": str(e),
|
|
224
|
+
"exception_type": type(e).__name__
|
|
225
|
+
})
|
|
226
|
+
# Fallback to subprocess
|
|
227
|
+
try:
|
|
228
|
+
subprocess.run(cmd, stdin=None, stdout=None, stderr=None)
|
|
229
|
+
if self.project_logger:
|
|
230
|
+
self.project_logger.log_system(
|
|
231
|
+
"Interactive session completed (subprocess fallback)",
|
|
232
|
+
level="INFO",
|
|
233
|
+
component="session"
|
|
234
|
+
)
|
|
235
|
+
self._log_session_event({
|
|
236
|
+
"event": "interactive_session_complete",
|
|
237
|
+
"fallback": True
|
|
238
|
+
})
|
|
239
|
+
except Exception as fallback_error:
|
|
240
|
+
print(f"Fallback also failed: {fallback_error}")
|
|
241
|
+
if self.project_logger:
|
|
242
|
+
self.project_logger.log_system(
|
|
243
|
+
f"Fallback launch failed: {fallback_error}",
|
|
244
|
+
level="ERROR",
|
|
245
|
+
component="session"
|
|
246
|
+
)
|
|
247
|
+
self._log_session_event({
|
|
248
|
+
"event": "interactive_fallback_failed",
|
|
249
|
+
"error": str(fallback_error),
|
|
250
|
+
"exception_type": type(fallback_error).__name__
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
def run_oneshot(self, prompt: str, context: Optional[str] = None) -> bool:
|
|
254
|
+
"""Run Claude with a single prompt and return success status."""
|
|
255
|
+
start_time = time.time()
|
|
256
|
+
|
|
257
|
+
if self.project_logger:
|
|
258
|
+
self.project_logger.log_system(
|
|
259
|
+
f"Starting non-interactive session with prompt: {prompt[:100]}",
|
|
260
|
+
level="INFO",
|
|
261
|
+
component="session"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Setup agents
|
|
265
|
+
if not self.setup_agents():
|
|
266
|
+
print("Continuing without native agents...")
|
|
267
|
+
|
|
268
|
+
# Combine context and prompt
|
|
269
|
+
full_prompt = prompt
|
|
270
|
+
if context:
|
|
271
|
+
full_prompt = f"{context}\n\n{prompt}"
|
|
272
|
+
|
|
273
|
+
# Build command with system instructions
|
|
274
|
+
cmd = [
|
|
275
|
+
"claude",
|
|
276
|
+
"--model", "opus",
|
|
277
|
+
"--dangerously-skip-permissions"
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
# Add any custom Claude arguments
|
|
281
|
+
if self.claude_args:
|
|
282
|
+
cmd.extend(self.claude_args)
|
|
283
|
+
|
|
284
|
+
# Add print and prompt
|
|
285
|
+
cmd.extend(["--print", full_prompt])
|
|
286
|
+
|
|
287
|
+
# Add system instructions if available
|
|
288
|
+
system_prompt = self._create_system_prompt()
|
|
289
|
+
if system_prompt and system_prompt != create_simple_context():
|
|
290
|
+
# Insert system prompt before the user prompt
|
|
291
|
+
cmd.insert(-2, "--append-system-prompt")
|
|
292
|
+
cmd.insert(-2, system_prompt)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Run Claude
|
|
296
|
+
if self.project_logger:
|
|
297
|
+
self.project_logger.log_system(
|
|
298
|
+
"Executing Claude subprocess",
|
|
299
|
+
level="INFO",
|
|
300
|
+
component="session"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
304
|
+
execution_time = time.time() - start_time
|
|
305
|
+
|
|
306
|
+
if result.returncode == 0:
|
|
307
|
+
response = result.stdout.strip()
|
|
308
|
+
print(response)
|
|
309
|
+
|
|
310
|
+
if self.project_logger:
|
|
311
|
+
# Log successful completion
|
|
312
|
+
self.project_logger.log_system(
|
|
313
|
+
f"Non-interactive session completed successfully in {execution_time:.2f}s",
|
|
314
|
+
level="INFO",
|
|
315
|
+
component="session"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Log session event
|
|
319
|
+
self._log_session_event({
|
|
320
|
+
"event": "session_complete",
|
|
321
|
+
"success": True,
|
|
322
|
+
"execution_time": execution_time,
|
|
323
|
+
"response_length": len(response)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
# Log agent invocation if we detect delegation patterns
|
|
327
|
+
if self._contains_delegation(response):
|
|
328
|
+
self.project_logger.log_system(
|
|
329
|
+
"Detected potential agent delegation in response",
|
|
330
|
+
level="INFO",
|
|
331
|
+
component="delegation"
|
|
332
|
+
)
|
|
333
|
+
self._log_session_event({
|
|
334
|
+
"event": "delegation_detected",
|
|
335
|
+
"prompt": prompt[:200],
|
|
336
|
+
"indicators": [p for p in ["Task(", "subagent_type=", "engineer agent", "qa agent"]
|
|
337
|
+
if p.lower() in response.lower()]
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
# Extract tickets if enabled
|
|
341
|
+
if self.enable_tickets and self.ticket_manager and response:
|
|
342
|
+
self._extract_tickets(response)
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
else:
|
|
346
|
+
error_msg = result.stderr or "Unknown error"
|
|
347
|
+
print(f"Error: {error_msg}")
|
|
348
|
+
|
|
349
|
+
if self.project_logger:
|
|
350
|
+
self.project_logger.log_system(
|
|
351
|
+
f"Non-interactive session failed: {error_msg}",
|
|
352
|
+
level="ERROR",
|
|
353
|
+
component="session"
|
|
354
|
+
)
|
|
355
|
+
self._log_session_event({
|
|
356
|
+
"event": "session_failed",
|
|
357
|
+
"success": False,
|
|
358
|
+
"error": error_msg,
|
|
359
|
+
"return_code": result.returncode
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
print(f"Error: {e}")
|
|
366
|
+
|
|
367
|
+
if self.project_logger:
|
|
368
|
+
self.project_logger.log_system(
|
|
369
|
+
f"Exception during non-interactive session: {e}",
|
|
370
|
+
level="ERROR",
|
|
371
|
+
component="session"
|
|
372
|
+
)
|
|
373
|
+
self._log_session_event({
|
|
374
|
+
"event": "session_exception",
|
|
375
|
+
"success": False,
|
|
376
|
+
"exception": str(e),
|
|
377
|
+
"exception_type": type(e).__name__
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
return False
|
|
381
|
+
finally:
|
|
382
|
+
# Ensure logs are flushed
|
|
383
|
+
if self.project_logger:
|
|
384
|
+
try:
|
|
385
|
+
# Log session summary
|
|
386
|
+
summary = self.project_logger.get_session_summary()
|
|
387
|
+
self.project_logger.log_system(
|
|
388
|
+
f"Session {summary['session_id']} completed",
|
|
389
|
+
level="INFO",
|
|
390
|
+
component="session"
|
|
391
|
+
)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.logger.debug(f"Failed to log session summary: {e}")
|
|
394
|
+
|
|
395
|
+
def _extract_tickets(self, text: str):
|
|
396
|
+
"""Extract tickets from Claude's response."""
|
|
397
|
+
if not self.ticket_manager:
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
# Use the ticket manager's extraction logic if available
|
|
402
|
+
if hasattr(self.ticket_manager, 'extract_tickets_from_text'):
|
|
403
|
+
tickets = self.ticket_manager.extract_tickets_from_text(text)
|
|
404
|
+
if tickets:
|
|
405
|
+
print(f"\n📋 Extracted {len(tickets)} tickets")
|
|
406
|
+
for ticket in tickets[:3]: # Show first 3
|
|
407
|
+
print(f" - [{ticket.get('id', 'N/A')}] {ticket.get('title', 'No title')}")
|
|
408
|
+
if len(tickets) > 3:
|
|
409
|
+
print(f" ... and {len(tickets) - 3} more")
|
|
410
|
+
else:
|
|
411
|
+
self.logger.debug("Ticket extraction method not available")
|
|
412
|
+
except Exception as e:
|
|
413
|
+
self.logger.debug(f"Ticket extraction failed: {e}")
|
|
414
|
+
|
|
415
|
+
def _load_system_instructions(self) -> Optional[str]:
|
|
416
|
+
"""Load system instructions from agents/INSTRUCTIONS.md."""
|
|
417
|
+
try:
|
|
418
|
+
# Find the INSTRUCTIONS.md file
|
|
419
|
+
module_path = Path(__file__).parent.parent
|
|
420
|
+
instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
|
|
421
|
+
|
|
422
|
+
if not instructions_path.exists():
|
|
423
|
+
self.logger.warning(f"System instructions not found: {instructions_path}")
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
instructions = instructions_path.read_text()
|
|
427
|
+
self.logger.info("Loaded PM framework system instructions")
|
|
428
|
+
return instructions
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
self.logger.error(f"Failed to load system instructions: {e}")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
def _create_system_prompt(self) -> str:
|
|
435
|
+
"""Create the complete system prompt including instructions."""
|
|
436
|
+
if self.system_instructions:
|
|
437
|
+
return self.system_instructions
|
|
438
|
+
else:
|
|
439
|
+
# Fallback to basic context
|
|
440
|
+
return create_simple_context()
|
|
441
|
+
|
|
442
|
+
def _contains_delegation(self, text: str) -> bool:
|
|
443
|
+
"""Check if text contains signs of agent delegation."""
|
|
444
|
+
# Look for common delegation patterns
|
|
445
|
+
delegation_patterns = [
|
|
446
|
+
"Task(",
|
|
447
|
+
"subagent_type=",
|
|
448
|
+
"delegating to",
|
|
449
|
+
"asking the",
|
|
450
|
+
"engineer agent",
|
|
451
|
+
"qa agent",
|
|
452
|
+
"documentation agent",
|
|
453
|
+
"research agent",
|
|
454
|
+
"security agent",
|
|
455
|
+
"ops agent",
|
|
456
|
+
"version_control agent",
|
|
457
|
+
"data_engineer agent"
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
text_lower = text.lower()
|
|
461
|
+
return any(pattern.lower() in text_lower for pattern in delegation_patterns)
|
|
462
|
+
|
|
463
|
+
def _log_session_event(self, event_data: dict):
|
|
464
|
+
"""Log an event to the session log file."""
|
|
465
|
+
if self.session_log_file:
|
|
466
|
+
try:
|
|
467
|
+
log_entry = {
|
|
468
|
+
"timestamp": datetime.now().isoformat(),
|
|
469
|
+
**event_data
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
with open(self.session_log_file, 'a') as f:
|
|
473
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self.logger.debug(f"Failed to log session event: {e}")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def create_simple_context() -> str:
|
|
479
|
+
"""Create basic context for Claude."""
|
|
480
|
+
return """You are Claude Code running in Claude MPM (Multi-Agent Project Manager).
|
|
481
|
+
|
|
482
|
+
You have access to native subagents via the Task tool with subagent_type parameter:
|
|
483
|
+
- engineer: For coding, implementation, and technical tasks
|
|
484
|
+
- qa: For testing, validation, and quality assurance
|
|
485
|
+
- documentation: For docs, guides, and explanations
|
|
486
|
+
- research: For investigation and analysis
|
|
487
|
+
- security: For security-related tasks
|
|
488
|
+
- ops: For deployment and infrastructure
|
|
489
|
+
- version-control: For git and version management
|
|
490
|
+
- data-engineer: For data processing and APIs
|
|
491
|
+
|
|
492
|
+
Use these agents by calling: Task(description="task description", subagent_type="agent_name")
|
|
493
|
+
|
|
494
|
+
Work efficiently and delegate appropriately to subagents when needed."""
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# Convenience functions for backward compatibility
|
|
498
|
+
def run_claude_interactive(context: Optional[str] = None):
|
|
499
|
+
"""Run Claude interactively with optional context."""
|
|
500
|
+
runner = SimpleClaudeRunner()
|
|
501
|
+
if context is None:
|
|
502
|
+
context = create_simple_context()
|
|
503
|
+
runner.run_interactive(context)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def run_claude_oneshot(prompt: str, context: Optional[str] = None) -> bool:
|
|
507
|
+
"""Run Claude with a single prompt."""
|
|
508
|
+
runner = SimpleClaudeRunner()
|
|
509
|
+
if context is None:
|
|
510
|
+
context = create_simple_context()
|
|
511
|
+
return runner.run_oneshot(prompt, context)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tool access control system for managing which tools agents can use."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Set, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolAccessControl:
|
|
10
|
+
"""
|
|
11
|
+
Manages tool access permissions for different agent types.
|
|
12
|
+
|
|
13
|
+
This ensures that only authorized agents can access specific tools,
|
|
14
|
+
particularly restricting TodoWrite to the PM (parent process) only.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Default tool sets for different contexts
|
|
18
|
+
PM_TOOLS = {
|
|
19
|
+
"Task", # For delegating to agents
|
|
20
|
+
"TodoWrite", # For tracking tasks (PM ONLY)
|
|
21
|
+
"WebSearch", # For understanding requirements
|
|
22
|
+
"WebFetch" # For fetching external resources
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
AGENT_TOOLS = {
|
|
26
|
+
"Read", # Read files
|
|
27
|
+
"Write", # Write files
|
|
28
|
+
"Edit", # Edit files
|
|
29
|
+
"MultiEdit", # Multiple edits
|
|
30
|
+
"Bash", # Execute commands
|
|
31
|
+
"Grep", # Search in files
|
|
32
|
+
"Glob", # File pattern matching
|
|
33
|
+
"LS", # List directory
|
|
34
|
+
"WebSearch", # Search the web
|
|
35
|
+
"WebFetch", # Fetch web content
|
|
36
|
+
"NotebookRead", # Read Jupyter notebooks
|
|
37
|
+
"NotebookEdit" # Edit Jupyter notebooks
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Tool restrictions by agent type
|
|
41
|
+
AGENT_RESTRICTIONS: Dict[str, Set[str]] = {
|
|
42
|
+
# PM has very limited tools - delegation only
|
|
43
|
+
"pm": PM_TOOLS,
|
|
44
|
+
|
|
45
|
+
# All other agents get standard tools WITHOUT TodoWrite
|
|
46
|
+
"engineer": AGENT_TOOLS,
|
|
47
|
+
"research": AGENT_TOOLS,
|
|
48
|
+
"qa": AGENT_TOOLS,
|
|
49
|
+
"security": AGENT_TOOLS,
|
|
50
|
+
"documentation": AGENT_TOOLS,
|
|
51
|
+
"ops": AGENT_TOOLS,
|
|
52
|
+
"version_control": AGENT_TOOLS,
|
|
53
|
+
"data_engineer": AGENT_TOOLS,
|
|
54
|
+
"architect": AGENT_TOOLS
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
"""Initialize the tool access control system."""
|
|
59
|
+
self.custom_restrictions: Dict[str, Set[str]] = {}
|
|
60
|
+
logger.info("Tool access control system initialized")
|
|
61
|
+
|
|
62
|
+
def get_allowed_tools(self, agent_type: str, is_parent: bool = False) -> List[str]:
|
|
63
|
+
"""
|
|
64
|
+
Get the list of allowed tools for an agent type.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
agent_type: The type of agent (pm, engineer, research, etc.)
|
|
68
|
+
is_parent: Whether this is the parent process (PM)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of allowed tool names
|
|
72
|
+
"""
|
|
73
|
+
# Normalize agent type
|
|
74
|
+
agent_type = agent_type.lower().replace(" ", "_").replace("-", "_")
|
|
75
|
+
|
|
76
|
+
# Parent process (PM) gets PM tools
|
|
77
|
+
if is_parent or agent_type == "pm":
|
|
78
|
+
allowed = self.PM_TOOLS.copy()
|
|
79
|
+
logger.debug(f"PM/Parent process allowed tools: {allowed}")
|
|
80
|
+
return sorted(list(allowed))
|
|
81
|
+
|
|
82
|
+
# Check custom restrictions first
|
|
83
|
+
if agent_type in self.custom_restrictions:
|
|
84
|
+
allowed = self.custom_restrictions[agent_type]
|
|
85
|
+
elif agent_type in self.AGENT_RESTRICTIONS:
|
|
86
|
+
allowed = self.AGENT_RESTRICTIONS[agent_type]
|
|
87
|
+
else:
|
|
88
|
+
# Default to agent tools for unknown types
|
|
89
|
+
logger.warning(f"Unknown agent type '{agent_type}', using default agent tools")
|
|
90
|
+
allowed = self.AGENT_TOOLS.copy()
|
|
91
|
+
|
|
92
|
+
# Ensure TodoWrite is NEVER in child agent tools
|
|
93
|
+
allowed = allowed - {"TodoWrite"}
|
|
94
|
+
|
|
95
|
+
logger.debug(f"Agent '{agent_type}' allowed tools: {allowed}")
|
|
96
|
+
return sorted(list(allowed))
|
|
97
|
+
|
|
98
|
+
def format_allowed_tools_arg(self, agent_type: str, is_parent: bool = False) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Format the allowed tools as a comma-separated string for --allowedTools argument.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent_type: The type of agent
|
|
104
|
+
is_parent: Whether this is the parent process
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Comma-separated string of allowed tools
|
|
108
|
+
"""
|
|
109
|
+
allowed_tools = self.get_allowed_tools(agent_type, is_parent)
|
|
110
|
+
return ",".join(allowed_tools)
|
|
111
|
+
|
|
112
|
+
def set_custom_restrictions(self, agent_type: str, allowed_tools: Set[str]):
|
|
113
|
+
"""
|
|
114
|
+
Set custom tool restrictions for a specific agent type.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
agent_type: The agent type to customize
|
|
118
|
+
allowed_tools: Set of allowed tool names
|
|
119
|
+
"""
|
|
120
|
+
agent_type = agent_type.lower().replace(" ", "_").replace("-", "_")
|
|
121
|
+
|
|
122
|
+
# Ensure TodoWrite is not in custom restrictions for non-PM agents
|
|
123
|
+
if agent_type != "pm" and "TodoWrite" in allowed_tools:
|
|
124
|
+
logger.warning(f"Removing TodoWrite from custom restrictions for {agent_type}")
|
|
125
|
+
allowed_tools = allowed_tools - {"TodoWrite"}
|
|
126
|
+
|
|
127
|
+
self.custom_restrictions[agent_type] = allowed_tools
|
|
128
|
+
logger.info(f"Set custom tool restrictions for {agent_type}: {allowed_tools}")
|
|
129
|
+
|
|
130
|
+
def validate_tool_usage(self, agent_type: str, tool_name: str, is_parent: bool = False) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Validate if an agent is allowed to use a specific tool.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
agent_type: The type of agent
|
|
136
|
+
tool_name: The name of the tool
|
|
137
|
+
is_parent: Whether this is the parent process
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if allowed, False otherwise
|
|
141
|
+
"""
|
|
142
|
+
allowed_tools = self.get_allowed_tools(agent_type, is_parent)
|
|
143
|
+
is_allowed = tool_name in allowed_tools
|
|
144
|
+
|
|
145
|
+
if not is_allowed:
|
|
146
|
+
logger.warning(f"Agent '{agent_type}' attempted to use forbidden tool '{tool_name}'")
|
|
147
|
+
|
|
148
|
+
return is_allowed
|
|
149
|
+
|
|
150
|
+
def get_todo_guidance(self, agent_type: str) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Get guidance text for agents on how to handle TODOs.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
agent_type: The type of agent
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Guidance text for handling TODOs
|
|
159
|
+
"""
|
|
160
|
+
if agent_type.lower() == "pm":
|
|
161
|
+
return """You have access to the TodoWrite tool for tracking tasks. Always prefix todos with [Agent] to indicate delegation target."""
|
|
162
|
+
else:
|
|
163
|
+
return """You do NOT have access to TodoWrite. Instead, when you identify tasks that need tracking:
|
|
164
|
+
1. Include them in your response with clear markers like "TODO:" or "TASK:"
|
|
165
|
+
2. Format them as a structured list with priority and description
|
|
166
|
+
3. The PM will extract and track these in the central todo list
|
|
167
|
+
4. Example format:
|
|
168
|
+
TODO (High Priority): [Research] Analyze authentication patterns
|
|
169
|
+
TODO (Medium Priority): [QA] Write tests for new endpoints"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Global instance for easy access
|
|
173
|
+
tool_access_control = ToolAccessControl()
|