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,468 @@
|
|
|
1
|
+
"""Unified logging system for Claude MPM.
|
|
2
|
+
|
|
3
|
+
This module consolidates features from:
|
|
4
|
+
- utils/logger.py (simple console/file logging)
|
|
5
|
+
- core/logging_config.py (Rich formatting, JSON, streaming)
|
|
6
|
+
- core/project_logger.py (project-local logging, statistics)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import logging.handlers
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Optional, Dict, Any, List
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
import functools
|
|
20
|
+
|
|
21
|
+
# Optional Rich support
|
|
22
|
+
try:
|
|
23
|
+
from rich.logging import RichHandler
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
HAS_RICH = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_RICH = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LogLevel(Enum):
|
|
31
|
+
"""Log levels for different verbosity."""
|
|
32
|
+
OFF = "off"
|
|
33
|
+
DEBUG = "debug"
|
|
34
|
+
INFO = "info"
|
|
35
|
+
WARNING = "warning"
|
|
36
|
+
ERROR = "error"
|
|
37
|
+
CRITICAL = "critical"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StreamingHandler(logging.StreamHandler):
|
|
41
|
+
"""
|
|
42
|
+
Custom handler for single-line streaming INFO messages.
|
|
43
|
+
|
|
44
|
+
Shows progress indicators that update in place using carriage returns
|
|
45
|
+
while keeping ERROR and WARNING messages on separate lines.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, stream=None):
|
|
49
|
+
super().__init__(stream)
|
|
50
|
+
self._last_info_message = False
|
|
51
|
+
self._info_line_active = False
|
|
52
|
+
|
|
53
|
+
def emit(self, record):
|
|
54
|
+
"""Emit a log record with streaming support for INFO messages."""
|
|
55
|
+
try:
|
|
56
|
+
msg = self.format(record)
|
|
57
|
+
stream = self.stream
|
|
58
|
+
|
|
59
|
+
# Handle different log levels
|
|
60
|
+
if record.levelno == logging.INFO:
|
|
61
|
+
# For INFO messages, use carriage return for streaming
|
|
62
|
+
if self._info_line_active:
|
|
63
|
+
# Clear the previous line by overwriting with spaces
|
|
64
|
+
stream.write('\r' + ' ' * 100 + '\r')
|
|
65
|
+
|
|
66
|
+
# Write INFO message with carriage return (no newline)
|
|
67
|
+
stream.write(f'\r{msg}')
|
|
68
|
+
stream.flush()
|
|
69
|
+
self._info_line_active = True
|
|
70
|
+
self._last_info_message = True
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
# For WARNING, ERROR, CRITICAL - always on new lines
|
|
74
|
+
if self._info_line_active:
|
|
75
|
+
# Finish the INFO line first
|
|
76
|
+
stream.write('\n')
|
|
77
|
+
self._info_line_active = False
|
|
78
|
+
|
|
79
|
+
stream.write(f'{msg}\n')
|
|
80
|
+
stream.flush()
|
|
81
|
+
self._last_info_message = False
|
|
82
|
+
|
|
83
|
+
except (KeyboardInterrupt, SystemExit):
|
|
84
|
+
raise
|
|
85
|
+
except:
|
|
86
|
+
self.handleError(record)
|
|
87
|
+
|
|
88
|
+
def finalize_info_line(self):
|
|
89
|
+
"""
|
|
90
|
+
Finalize any active INFO line by adding a newline.
|
|
91
|
+
Call this when you want to ensure the final INFO message remains visible.
|
|
92
|
+
"""
|
|
93
|
+
if self._info_line_active:
|
|
94
|
+
self.stream.write('\n')
|
|
95
|
+
self.stream.flush()
|
|
96
|
+
self._info_line_active = False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class JsonFormatter(logging.Formatter):
|
|
100
|
+
"""JSON formatter for structured logging."""
|
|
101
|
+
|
|
102
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
103
|
+
"""Format log record as JSON."""
|
|
104
|
+
log_entry = {
|
|
105
|
+
"timestamp": self.formatTime(record, self.datefmt),
|
|
106
|
+
"level": record.levelname,
|
|
107
|
+
"logger": record.name,
|
|
108
|
+
"message": record.getMessage(),
|
|
109
|
+
"module": record.module,
|
|
110
|
+
"function": record.funcName,
|
|
111
|
+
"line": record.lineno,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Add exception info if present
|
|
115
|
+
if record.exc_info:
|
|
116
|
+
log_entry["exception"] = self.formatException(record.exc_info)
|
|
117
|
+
|
|
118
|
+
# Add extra fields
|
|
119
|
+
for key, value in record.__dict__.items():
|
|
120
|
+
if key not in {
|
|
121
|
+
"name", "msg", "args", "levelname", "levelno", "pathname",
|
|
122
|
+
"filename", "module", "lineno", "funcName", "created",
|
|
123
|
+
"msecs", "relativeCreated", "thread", "threadName",
|
|
124
|
+
"processName", "process", "message", "exc_info",
|
|
125
|
+
"exc_text", "stack_info"
|
|
126
|
+
}:
|
|
127
|
+
log_entry[key] = value
|
|
128
|
+
|
|
129
|
+
return json.dumps(log_entry, default=str)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def setup_logging(
|
|
133
|
+
name: str = "claude_mpm",
|
|
134
|
+
level: str = "INFO",
|
|
135
|
+
log_dir: Optional[Path] = None,
|
|
136
|
+
log_file: Optional[Path] = None,
|
|
137
|
+
console_output: bool = True,
|
|
138
|
+
file_output: bool = True,
|
|
139
|
+
use_rich: bool = False,
|
|
140
|
+
json_format: bool = False,
|
|
141
|
+
use_streaming: bool = False,
|
|
142
|
+
) -> logging.Logger:
|
|
143
|
+
"""
|
|
144
|
+
Set up logging with both console and file handlers.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Logger name
|
|
148
|
+
level: Logging level (OFF, DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
149
|
+
log_dir: Directory for log files (defaults to ~/.claude-mpm/logs)
|
|
150
|
+
log_file: Specific log file path (overrides log_dir)
|
|
151
|
+
console_output: Enable console output
|
|
152
|
+
file_output: Enable file output
|
|
153
|
+
use_rich: Use Rich handler for colored console output
|
|
154
|
+
json_format: Use JSON format for structured logging
|
|
155
|
+
use_streaming: Use streaming handler for single-line INFO messages
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Configured logger
|
|
159
|
+
"""
|
|
160
|
+
logger = logging.getLogger(name)
|
|
161
|
+
|
|
162
|
+
# Handle OFF level
|
|
163
|
+
if level.upper() == "OFF":
|
|
164
|
+
logger.setLevel(logging.CRITICAL + 1) # Higher than CRITICAL
|
|
165
|
+
logger.handlers.clear()
|
|
166
|
+
return logger
|
|
167
|
+
|
|
168
|
+
# Set log level
|
|
169
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
170
|
+
logger.setLevel(log_level)
|
|
171
|
+
|
|
172
|
+
# Prevent duplicate handlers
|
|
173
|
+
logger.handlers.clear()
|
|
174
|
+
|
|
175
|
+
# Create formatters
|
|
176
|
+
if json_format:
|
|
177
|
+
formatter = JsonFormatter()
|
|
178
|
+
else:
|
|
179
|
+
detailed_formatter = logging.Formatter(
|
|
180
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
|
181
|
+
)
|
|
182
|
+
simple_formatter = logging.Formatter('%(levelname)s: %(message)s')
|
|
183
|
+
|
|
184
|
+
# Console handler
|
|
185
|
+
if console_output:
|
|
186
|
+
if use_streaming:
|
|
187
|
+
# Use streaming handler for single-line INFO messages
|
|
188
|
+
console_handler = StreamingHandler(sys.stdout)
|
|
189
|
+
console_handler.setFormatter(simple_formatter)
|
|
190
|
+
elif use_rich and HAS_RICH and not json_format:
|
|
191
|
+
console = Console(stderr=True)
|
|
192
|
+
console_handler = RichHandler(console=console, show_time=True, show_path=True, markup=True)
|
|
193
|
+
console_handler.setFormatter(logging.Formatter(fmt="%(message)s", datefmt="[%X]"))
|
|
194
|
+
else:
|
|
195
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
196
|
+
console_handler.setFormatter(formatter if json_format else simple_formatter)
|
|
197
|
+
|
|
198
|
+
console_handler.setLevel(logging.INFO)
|
|
199
|
+
logger.addHandler(console_handler)
|
|
200
|
+
|
|
201
|
+
# File handler
|
|
202
|
+
if file_output and level.upper() in ["INFO", "DEBUG"]:
|
|
203
|
+
if log_file:
|
|
204
|
+
# Use specific log file
|
|
205
|
+
log_file = Path(log_file)
|
|
206
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
|
|
208
|
+
# Use rotating file handler
|
|
209
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
210
|
+
log_file, maxBytes=10 * 1024 * 1024, backupCount=5 # 10 MB
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Use default log directory
|
|
214
|
+
if log_dir is None:
|
|
215
|
+
log_dir = Path.home() / ".claude-mpm" / "logs"
|
|
216
|
+
|
|
217
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
|
|
219
|
+
# Create timestamped log file
|
|
220
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
221
|
+
log_file = log_dir / f"mpm_{timestamp}.log"
|
|
222
|
+
|
|
223
|
+
file_handler = logging.FileHandler(log_file)
|
|
224
|
+
|
|
225
|
+
# Also create a symlink to latest log
|
|
226
|
+
latest_link = log_dir / "latest.log"
|
|
227
|
+
if latest_link.exists() and latest_link.is_symlink():
|
|
228
|
+
latest_link.unlink()
|
|
229
|
+
if not latest_link.exists():
|
|
230
|
+
latest_link.symlink_to(log_file.name)
|
|
231
|
+
|
|
232
|
+
file_handler.setLevel(logging.DEBUG)
|
|
233
|
+
file_handler.setFormatter(formatter if json_format else detailed_formatter)
|
|
234
|
+
logger.addHandler(file_handler)
|
|
235
|
+
|
|
236
|
+
# Prevent propagation to root logger
|
|
237
|
+
logger.propagate = False
|
|
238
|
+
|
|
239
|
+
return logger
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_logger(name: str) -> logging.Logger:
|
|
243
|
+
"""Get a logger instance."""
|
|
244
|
+
return logging.getLogger(f"claude_mpm.{name}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def setup_streaming_logger(name: str, level: str = "INFO") -> logging.Logger:
|
|
248
|
+
"""
|
|
249
|
+
Convenience function to setup a logger with streaming INFO support.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
name: Logger name
|
|
253
|
+
level: Log level (default: INFO)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Logger configured with streaming handler
|
|
257
|
+
"""
|
|
258
|
+
return setup_logging(
|
|
259
|
+
name=name,
|
|
260
|
+
level=level,
|
|
261
|
+
use_rich=False,
|
|
262
|
+
use_streaming=True
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def finalize_streaming_logs(logger: logging.Logger):
|
|
267
|
+
"""
|
|
268
|
+
Finalize any active streaming INFO lines for a logger.
|
|
269
|
+
|
|
270
|
+
This ensures the final INFO message remains visible by adding
|
|
271
|
+
a newline to complete any streaming output.
|
|
272
|
+
"""
|
|
273
|
+
for handler in logger.handlers:
|
|
274
|
+
if isinstance(handler, StreamingHandler):
|
|
275
|
+
handler.finalize_info_line()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def log_performance(func):
|
|
279
|
+
"""Decorator to log function execution time."""
|
|
280
|
+
@functools.wraps(func)
|
|
281
|
+
def wrapper(*args, **kwargs):
|
|
282
|
+
logger = get_logger(func.__module__)
|
|
283
|
+
start_time = time.time()
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
result = func(*args, **kwargs)
|
|
287
|
+
execution_time = time.time() - start_time
|
|
288
|
+
logger.debug(f"{func.__name__} executed in {execution_time:.3f}s")
|
|
289
|
+
return result
|
|
290
|
+
except Exception as e:
|
|
291
|
+
execution_time = time.time() - start_time
|
|
292
|
+
logger.error(f"{func.__name__} failed after {execution_time:.3f}s: {e}")
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
return wrapper
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def log_async_performance(func):
|
|
299
|
+
"""Decorator to log async function execution time."""
|
|
300
|
+
@functools.wraps(func)
|
|
301
|
+
async def wrapper(*args, **kwargs):
|
|
302
|
+
logger = get_logger(func.__module__)
|
|
303
|
+
start_time = time.time()
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
result = await func(*args, **kwargs)
|
|
307
|
+
execution_time = time.time() - start_time
|
|
308
|
+
logger.debug(f"{func.__name__} executed in {execution_time:.3f}s")
|
|
309
|
+
return result
|
|
310
|
+
except Exception as e:
|
|
311
|
+
execution_time = time.time() - start_time
|
|
312
|
+
logger.error(f"{func.__name__} failed after {execution_time:.3f}s: {e}")
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
return wrapper
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class ProjectLogger:
|
|
319
|
+
"""
|
|
320
|
+
Manages project-local logging in ./claude-mpm directory.
|
|
321
|
+
|
|
322
|
+
This is a simplified version of the original ProjectLogger,
|
|
323
|
+
focused on essential features for backwards compatibility.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self,
|
|
327
|
+
project_dir: Optional[Path] = None,
|
|
328
|
+
log_level: str = "INFO",
|
|
329
|
+
create_structure: bool = True):
|
|
330
|
+
"""Initialize project logger.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
project_dir: Project directory (defaults to cwd)
|
|
334
|
+
log_level: Logging level (INFO, DEBUG, OFF)
|
|
335
|
+
create_structure: Whether to create directory structure
|
|
336
|
+
"""
|
|
337
|
+
self.project_dir = project_dir or Path.cwd()
|
|
338
|
+
self.claude_mpm_dir = self.project_dir / ".claude-mpm"
|
|
339
|
+
self.log_level = LogLevel(log_level.lower())
|
|
340
|
+
|
|
341
|
+
# Basic directory structure
|
|
342
|
+
self.dirs = {
|
|
343
|
+
"base": self.claude_mpm_dir,
|
|
344
|
+
"logs": self.claude_mpm_dir / "logs",
|
|
345
|
+
"logs_system": self.claude_mpm_dir / "logs" / "system",
|
|
346
|
+
"logs_agents": self.claude_mpm_dir / "logs" / "agents",
|
|
347
|
+
"logs_sessions": self.claude_mpm_dir / "logs" / "sessions",
|
|
348
|
+
"stats": self.claude_mpm_dir / "stats",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if create_structure:
|
|
352
|
+
for path in self.dirs.values():
|
|
353
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
|
|
355
|
+
# Create session directory
|
|
356
|
+
self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
357
|
+
self.session_start_time = datetime.now()
|
|
358
|
+
self.session_dir = self.dirs["logs_sessions"] / self.session_id
|
|
359
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
|
|
361
|
+
# Statistics tracking
|
|
362
|
+
self.stats = defaultdict(lambda: {
|
|
363
|
+
"total_calls": 0,
|
|
364
|
+
"total_tokens": 0,
|
|
365
|
+
"total_time_seconds": 0,
|
|
366
|
+
"by_agent": defaultdict(lambda: {
|
|
367
|
+
"calls": 0,
|
|
368
|
+
"tokens": 0,
|
|
369
|
+
"time_seconds": 0,
|
|
370
|
+
"success_rate": 0.0,
|
|
371
|
+
"tasks": []
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
def log_system(self, message: str, level: str = "INFO", component: str = "general"):
|
|
376
|
+
"""Log system-level message."""
|
|
377
|
+
if self.log_level == LogLevel.OFF:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
timestamp = datetime.now().isoformat()
|
|
381
|
+
log_entry = {
|
|
382
|
+
"timestamp": timestamp,
|
|
383
|
+
"level": level,
|
|
384
|
+
"component": component,
|
|
385
|
+
"message": message
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Write to daily log file
|
|
389
|
+
log_file = self.dirs["logs_system"] / f"{datetime.now().strftime('%Y%m%d')}.jsonl"
|
|
390
|
+
with open(log_file, 'a') as f:
|
|
391
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
392
|
+
|
|
393
|
+
def log_agent_invocation(self,
|
|
394
|
+
agent: str,
|
|
395
|
+
task: str,
|
|
396
|
+
prompt: str,
|
|
397
|
+
response: str,
|
|
398
|
+
execution_time: float,
|
|
399
|
+
tokens: int,
|
|
400
|
+
success: bool = True,
|
|
401
|
+
metadata: Optional[Dict[str, Any]] = None):
|
|
402
|
+
"""Log agent invocation with configurable detail level."""
|
|
403
|
+
if self.log_level == LogLevel.OFF:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
timestamp = datetime.now().isoformat()
|
|
407
|
+
|
|
408
|
+
# Update statistics
|
|
409
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
410
|
+
self.stats[today]["total_calls"] += 1
|
|
411
|
+
self.stats[today]["total_tokens"] += tokens
|
|
412
|
+
self.stats[today]["total_time_seconds"] += execution_time
|
|
413
|
+
|
|
414
|
+
agent_stats = self.stats[today]["by_agent"][agent.lower()]
|
|
415
|
+
agent_stats["calls"] += 1
|
|
416
|
+
agent_stats["tokens"] += tokens
|
|
417
|
+
agent_stats["time_seconds"] += execution_time
|
|
418
|
+
|
|
419
|
+
# Prepare log entry
|
|
420
|
+
log_entry = {
|
|
421
|
+
"timestamp": timestamp,
|
|
422
|
+
"agent": agent,
|
|
423
|
+
"task": task[:200],
|
|
424
|
+
"execution_time": execution_time,
|
|
425
|
+
"tokens": tokens,
|
|
426
|
+
"success": success,
|
|
427
|
+
"metadata": metadata or {}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Add full details in DEBUG mode
|
|
431
|
+
if self.log_level == LogLevel.DEBUG:
|
|
432
|
+
log_entry["prompt"] = prompt
|
|
433
|
+
log_entry["response"] = response
|
|
434
|
+
|
|
435
|
+
# Write to agent-specific log
|
|
436
|
+
agent_log_dir = self.dirs["logs_agents"] / agent.lower()
|
|
437
|
+
agent_log_dir.mkdir(exist_ok=True)
|
|
438
|
+
|
|
439
|
+
daily_log = agent_log_dir / f"{datetime.now().strftime('%Y%m%d')}.jsonl"
|
|
440
|
+
with open(daily_log, 'a') as f:
|
|
441
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
442
|
+
|
|
443
|
+
def get_session_summary(self) -> Dict[str, Any]:
|
|
444
|
+
"""Get summary of current session."""
|
|
445
|
+
return {
|
|
446
|
+
"session_id": self.session_id,
|
|
447
|
+
"session_dir": str(self.session_dir),
|
|
448
|
+
"start_time": self.session_id,
|
|
449
|
+
"stats": self.stats.get(datetime.now().strftime("%Y-%m-%d"), {})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Singleton instance for project logger
|
|
454
|
+
_project_logger = None
|
|
455
|
+
|
|
456
|
+
def get_project_logger(log_level: str = "INFO") -> ProjectLogger:
|
|
457
|
+
"""Get or create the project logger singleton.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
log_level: Log level (INFO, DEBUG, OFF)
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
ProjectLogger instance
|
|
464
|
+
"""
|
|
465
|
+
global _project_logger
|
|
466
|
+
if _project_logger is None:
|
|
467
|
+
_project_logger = ProjectLogger(log_level=log_level)
|
|
468
|
+
return _project_logger
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Minimal framework loader for better performance."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from ..core.logger import get_logger
|
|
9
|
+
except ImportError:
|
|
10
|
+
from core.logger import get_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MinimalFrameworkLoader:
|
|
14
|
+
"""Load a minimal framework for non-interactive mode."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
framework_path: Optional[Path] = None,
|
|
19
|
+
agents_dir: Optional[Path] = None,
|
|
20
|
+
):
|
|
21
|
+
"""Initialize the minimal loader."""
|
|
22
|
+
self.logger = get_logger("minimal_framework_loader")
|
|
23
|
+
self.framework_path = framework_path or self._detect_framework_path()
|
|
24
|
+
self.agents_dir = agents_dir
|
|
25
|
+
|
|
26
|
+
def _detect_framework_path(self) -> Optional[Path]:
|
|
27
|
+
"""Detect the claude-mpm framework path."""
|
|
28
|
+
# Same detection logic as main loader
|
|
29
|
+
candidates = [
|
|
30
|
+
Path.cwd() / "claude-mpm",
|
|
31
|
+
Path.cwd().parent / "claude-mpm",
|
|
32
|
+
Path.home() / "Projects" / "claude-mpm",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for candidate in candidates:
|
|
36
|
+
if candidate.exists() and (candidate / "src").exists():
|
|
37
|
+
return candidate
|
|
38
|
+
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def get_framework_instructions(self) -> str:
|
|
42
|
+
"""Get minimal framework instructions."""
|
|
43
|
+
# Load working directory INSTRUCTIONS.md (or legacy CLAUDE.md) if exists
|
|
44
|
+
working_claude = ""
|
|
45
|
+
working_instructions_path = Path.cwd() / "INSTRUCTIONS.md"
|
|
46
|
+
working_claude_path = Path.cwd() / "CLAUDE.md" # Legacy support
|
|
47
|
+
|
|
48
|
+
if working_instructions_path.exists():
|
|
49
|
+
try:
|
|
50
|
+
working_claude = working_instructions_path.read_text()
|
|
51
|
+
except:
|
|
52
|
+
pass
|
|
53
|
+
elif working_claude_path.exists():
|
|
54
|
+
try:
|
|
55
|
+
working_claude = working_claude_path.read_text()
|
|
56
|
+
except:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Build minimal framework
|
|
60
|
+
framework = f"""# Claude MPM Framework
|
|
61
|
+
|
|
62
|
+
You are a multi-agent orchestrator in the Claude MPM framework.
|
|
63
|
+
|
|
64
|
+
## Core Responsibilities
|
|
65
|
+
1. **Orchestrate** - Delegate ALL implementation work to specialized agents via Task Tool
|
|
66
|
+
2. **Coordinate** - Manage multi-agent workflows and cross-agent collaboration
|
|
67
|
+
3. **Track** - Extract TODO/BUG/FEATURE items for ticket creation
|
|
68
|
+
4. **Oversee** - Maintain project visibility and strategic oversight
|
|
69
|
+
5. **Never Implement** - You NEVER write code or perform direct implementation
|
|
70
|
+
|
|
71
|
+
## Available Agents
|
|
72
|
+
- **Engineer Agent**: Code implementation and development
|
|
73
|
+
- **QA Agent**: Testing and quality assurance
|
|
74
|
+
- **Documentation Agent**: Documentation and changelogs
|
|
75
|
+
- **Research Agent**: Investigation and analysis
|
|
76
|
+
- **Security Agent**: Security analysis and protection
|
|
77
|
+
- **Version Control Agent**: Git operations and version management
|
|
78
|
+
- **Ops Agent**: Deployment and operations
|
|
79
|
+
- **Data Engineer Agent**: Data management and AI API integration
|
|
80
|
+
|
|
81
|
+
## Task Tool Format
|
|
82
|
+
When delegating, use this format:
|
|
83
|
+
```
|
|
84
|
+
**[Agent Name]**: [Clear task description]
|
|
85
|
+
|
|
86
|
+
**Task**: [Detailed breakdown]
|
|
87
|
+
**Context**: [Relevant context]
|
|
88
|
+
**Expected Results**: [Specific deliverables]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Ticket Patterns
|
|
92
|
+
Extract from: TODO:, BUG:, FEATURE:, ISSUE:
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Add working directory instructions if present
|
|
97
|
+
if working_claude:
|
|
98
|
+
framework += f"\n## Working Directory Instructions\n{working_claude}\n"
|
|
99
|
+
|
|
100
|
+
return framework
|
|
101
|
+
|
|
102
|
+
def get_agent_list(self) -> list:
|
|
103
|
+
"""Get list of available agents."""
|
|
104
|
+
return [
|
|
105
|
+
"engineer", "qa", "documentation", "research",
|
|
106
|
+
"security", "version_control", "ops", "data_engineer"
|
|
107
|
+
]
|