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,841 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agent Modification Tracker - Consolidated Service
|
|
4
|
+
=================================================
|
|
5
|
+
|
|
6
|
+
Comprehensive agent modification tracking and persistence system for monitoring
|
|
7
|
+
agent changes across the three-tier hierarchy with real-time detection and
|
|
8
|
+
intelligent persistence management.
|
|
9
|
+
|
|
10
|
+
Key Features:
|
|
11
|
+
- Real-time file system monitoring for agent changes
|
|
12
|
+
- Modification history and version tracking
|
|
13
|
+
- Agent backup and restore functionality
|
|
14
|
+
- Modification validation and conflict detection
|
|
15
|
+
- SharedPromptCache invalidation integration
|
|
16
|
+
- Persistence storage in hierarchy-appropriate locations
|
|
17
|
+
- Change classification (create, modify, delete, move)
|
|
18
|
+
|
|
19
|
+
This is a consolidated version of the agent_modification_tracker service,
|
|
20
|
+
combining all functionality into a single module for better maintainability.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import time
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import dataclass, field, asdict
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List, Optional, Any, Callable, Tuple, Set
|
|
36
|
+
|
|
37
|
+
from watchdog.observers import Observer
|
|
38
|
+
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
39
|
+
|
|
40
|
+
from claude_mpm.core.base_service import BaseService
|
|
41
|
+
from claude_mpm.services.shared_prompt_cache import SharedPromptCache
|
|
42
|
+
from claude_mpm.services.agent_registry import AgentRegistry
|
|
43
|
+
from claude_mpm.utils.path_operations import path_ops
|
|
44
|
+
from claude_mpm.utils.config_manager import ConfigurationManager
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# Data Models
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
class ModificationType(Enum):
|
|
52
|
+
"""Types of agent modifications."""
|
|
53
|
+
CREATE = "create"
|
|
54
|
+
MODIFY = "modify"
|
|
55
|
+
DELETE = "delete"
|
|
56
|
+
MOVE = "move"
|
|
57
|
+
RESTORE = "restore"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ModificationTier(Enum):
|
|
61
|
+
"""Agent hierarchy tiers for modification tracking."""
|
|
62
|
+
PROJECT = "project"
|
|
63
|
+
USER = "user"
|
|
64
|
+
SYSTEM = "system"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class AgentModification:
|
|
69
|
+
"""Agent modification record with comprehensive metadata."""
|
|
70
|
+
|
|
71
|
+
modification_id: str
|
|
72
|
+
agent_name: str
|
|
73
|
+
modification_type: ModificationType
|
|
74
|
+
tier: ModificationTier
|
|
75
|
+
file_path: str
|
|
76
|
+
timestamp: float
|
|
77
|
+
user_id: Optional[str] = None
|
|
78
|
+
modification_details: Dict[str, Any] = field(default_factory=dict)
|
|
79
|
+
file_hash_before: Optional[str] = None
|
|
80
|
+
file_hash_after: Optional[str] = None
|
|
81
|
+
file_size_before: Optional[int] = None
|
|
82
|
+
file_size_after: Optional[int] = None
|
|
83
|
+
backup_path: Optional[str] = None
|
|
84
|
+
validation_status: str = "pending"
|
|
85
|
+
validation_errors: List[str] = field(default_factory=list)
|
|
86
|
+
related_modifications: List[str] = field(default_factory=list)
|
|
87
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def modification_datetime(self) -> datetime:
|
|
91
|
+
"""Get modification timestamp as datetime."""
|
|
92
|
+
return datetime.fromtimestamp(self.timestamp)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def age_seconds(self) -> float:
|
|
96
|
+
"""Get age of modification in seconds."""
|
|
97
|
+
return time.time() - self.timestamp
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
100
|
+
"""Convert to dictionary for serialization."""
|
|
101
|
+
data = asdict(self)
|
|
102
|
+
data['modification_type'] = self.modification_type.value
|
|
103
|
+
data['tier'] = self.tier.value
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'AgentModification':
|
|
108
|
+
"""Create from dictionary."""
|
|
109
|
+
data['modification_type'] = ModificationType(data['modification_type'])
|
|
110
|
+
data['tier'] = ModificationTier(data['tier'])
|
|
111
|
+
return cls(**data)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ModificationHistory:
|
|
116
|
+
"""Complete modification history for an agent."""
|
|
117
|
+
|
|
118
|
+
agent_name: str
|
|
119
|
+
modifications: List[AgentModification] = field(default_factory=list)
|
|
120
|
+
current_version: Optional[str] = None
|
|
121
|
+
total_modifications: int = 0
|
|
122
|
+
first_seen: Optional[float] = None
|
|
123
|
+
last_modified: Optional[float] = None
|
|
124
|
+
|
|
125
|
+
def add_modification(self, modification: AgentModification) -> None:
|
|
126
|
+
"""Add a modification to history."""
|
|
127
|
+
self.modifications.append(modification)
|
|
128
|
+
self.total_modifications += 1
|
|
129
|
+
self.last_modified = modification.timestamp
|
|
130
|
+
|
|
131
|
+
if self.first_seen is None:
|
|
132
|
+
self.first_seen = modification.timestamp
|
|
133
|
+
|
|
134
|
+
def get_recent_modifications(self, hours: int = 24) -> List[AgentModification]:
|
|
135
|
+
"""Get modifications within specified hours."""
|
|
136
|
+
cutoff = time.time() - (hours * 3600)
|
|
137
|
+
return [mod for mod in self.modifications if mod.timestamp >= cutoff]
|
|
138
|
+
|
|
139
|
+
def get_modifications_by_type(self, mod_type: ModificationType) -> List[AgentModification]:
|
|
140
|
+
"""Get modifications by type."""
|
|
141
|
+
return [mod for mod in self.modifications if mod.modification_type == mod_type]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ============================================================================
|
|
145
|
+
# File System Monitoring
|
|
146
|
+
# ============================================================================
|
|
147
|
+
|
|
148
|
+
class AgentFileSystemHandler(FileSystemEventHandler):
|
|
149
|
+
"""Handles file system events for agent files."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, tracker: 'AgentModificationTracker'):
|
|
152
|
+
self.tracker = tracker
|
|
153
|
+
self.logger = logging.getLogger(__name__)
|
|
154
|
+
|
|
155
|
+
def on_created(self, event: FileSystemEvent) -> None:
|
|
156
|
+
"""Handle file creation events."""
|
|
157
|
+
if not event.is_directory and event.src_path.endswith(('.md', '.json', '.yaml')):
|
|
158
|
+
asyncio.create_task(
|
|
159
|
+
self.tracker._handle_file_modification(event.src_path, ModificationType.CREATE)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
163
|
+
"""Handle file modification events."""
|
|
164
|
+
if not event.is_directory and event.src_path.endswith(('.md', '.json', '.yaml')):
|
|
165
|
+
asyncio.create_task(
|
|
166
|
+
self.tracker._handle_file_modification(event.src_path, ModificationType.MODIFY)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def on_deleted(self, event: FileSystemEvent) -> None:
|
|
170
|
+
"""Handle file deletion events."""
|
|
171
|
+
if not event.is_directory and event.src_path.endswith(('.md', '.json', '.yaml')):
|
|
172
|
+
asyncio.create_task(
|
|
173
|
+
self.tracker._handle_file_modification(event.src_path, ModificationType.DELETE)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def on_moved(self, event: FileSystemEvent) -> None:
|
|
177
|
+
"""Handle file move events."""
|
|
178
|
+
if not event.is_directory and event.src_path.endswith(('.md', '.json', '.yaml')):
|
|
179
|
+
asyncio.create_task(
|
|
180
|
+
self.tracker._handle_file_move(event.src_path, event.dest_path)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ============================================================================
|
|
185
|
+
# Main Service Class
|
|
186
|
+
# ============================================================================
|
|
187
|
+
|
|
188
|
+
class AgentModificationTracker(BaseService):
|
|
189
|
+
"""
|
|
190
|
+
Agent Modification Tracker - Comprehensive modification tracking and persistence system.
|
|
191
|
+
|
|
192
|
+
This consolidated service combines all functionality from the previous multi-file
|
|
193
|
+
implementation into a single, maintainable module.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
197
|
+
"""Initialize the agent modification tracker."""
|
|
198
|
+
super().__init__("agent_modification_tracker", config)
|
|
199
|
+
|
|
200
|
+
# Configuration
|
|
201
|
+
self.enable_monitoring = self.get_config("enable_monitoring", True)
|
|
202
|
+
self.backup_enabled = self.get_config("backup_enabled", True)
|
|
203
|
+
self.max_history_days = self.get_config("max_history_days", 30)
|
|
204
|
+
self.validation_enabled = self.get_config("validation_enabled", True)
|
|
205
|
+
self.persistence_interval = self.get_config("persistence_interval", 300)
|
|
206
|
+
|
|
207
|
+
# Core components
|
|
208
|
+
self.shared_cache: Optional[SharedPromptCache] = None
|
|
209
|
+
self.agent_registry: Optional[AgentRegistry] = None
|
|
210
|
+
|
|
211
|
+
# Tracking data structures
|
|
212
|
+
self.modification_history: Dict[str, ModificationHistory] = {}
|
|
213
|
+
self.active_modifications: Dict[str, AgentModification] = {}
|
|
214
|
+
|
|
215
|
+
# File monitoring
|
|
216
|
+
self.file_observer: Optional[Observer] = None
|
|
217
|
+
self.watched_paths: Set[Path] = set()
|
|
218
|
+
|
|
219
|
+
# Persistence paths
|
|
220
|
+
self.persistence_root = Path.home() / '.claude-pm' / 'agent_tracking'
|
|
221
|
+
self.backup_root = self.persistence_root / 'backups'
|
|
222
|
+
self.history_root = self.persistence_root / 'history'
|
|
223
|
+
|
|
224
|
+
# Create directories
|
|
225
|
+
self.persistence_root.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
self.backup_root.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
self.history_root.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
|
|
229
|
+
# Background tasks
|
|
230
|
+
self._persistence_task: Optional[asyncio.Task] = None
|
|
231
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
232
|
+
|
|
233
|
+
# Callbacks
|
|
234
|
+
self.modification_callbacks: List[Callable[[AgentModification], None]] = []
|
|
235
|
+
|
|
236
|
+
self.logger.info(
|
|
237
|
+
f"AgentModificationTracker initialized with monitoring="
|
|
238
|
+
f"{'enabled' if self.enable_monitoring else 'disabled'}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def _initialize(self) -> None:
|
|
242
|
+
"""Initialize the modification tracker service."""
|
|
243
|
+
self.logger.info("Initializing AgentModificationTracker service...")
|
|
244
|
+
|
|
245
|
+
# Initialize cache and registry integration
|
|
246
|
+
await self._initialize_integrations()
|
|
247
|
+
|
|
248
|
+
# Load existing modification history
|
|
249
|
+
await self._load_modification_history()
|
|
250
|
+
|
|
251
|
+
# Set up file system monitoring
|
|
252
|
+
if self.enable_monitoring:
|
|
253
|
+
await self._setup_file_monitoring()
|
|
254
|
+
|
|
255
|
+
# Start background tasks
|
|
256
|
+
self._persistence_task = asyncio.create_task(self._persistence_loop())
|
|
257
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
258
|
+
|
|
259
|
+
self.logger.info("AgentModificationTracker service initialized successfully")
|
|
260
|
+
|
|
261
|
+
async def _cleanup(self) -> None:
|
|
262
|
+
"""Cleanup modification tracker resources."""
|
|
263
|
+
self.logger.info("Cleaning up AgentModificationTracker service...")
|
|
264
|
+
|
|
265
|
+
# Stop file system monitoring
|
|
266
|
+
if self.enable_monitoring and self.file_observer:
|
|
267
|
+
self.file_observer.stop()
|
|
268
|
+
self.file_observer.join()
|
|
269
|
+
|
|
270
|
+
# Cancel background tasks
|
|
271
|
+
if self._persistence_task:
|
|
272
|
+
self._persistence_task.cancel()
|
|
273
|
+
if self._cleanup_task:
|
|
274
|
+
self._cleanup_task.cancel()
|
|
275
|
+
|
|
276
|
+
# Save final state
|
|
277
|
+
await self._save_modification_history()
|
|
278
|
+
|
|
279
|
+
self.logger.info("AgentModificationTracker service cleaned up")
|
|
280
|
+
|
|
281
|
+
async def _health_check(self) -> Dict[str, bool]:
|
|
282
|
+
"""Perform modification tracker health checks."""
|
|
283
|
+
checks = {}
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Check persistence directories
|
|
287
|
+
checks["persistence_directories"] = all([
|
|
288
|
+
self.persistence_root.exists(),
|
|
289
|
+
self.backup_root.exists(),
|
|
290
|
+
self.history_root.exists()
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
# Check file system monitoring
|
|
294
|
+
checks["file_monitoring"] = (
|
|
295
|
+
self.file_observer is not None and
|
|
296
|
+
self.file_observer.is_alive()
|
|
297
|
+
) if self.enable_monitoring else True
|
|
298
|
+
|
|
299
|
+
# Check integration components
|
|
300
|
+
checks["cache_integration"] = self.shared_cache is not None
|
|
301
|
+
checks["registry_integration"] = self.agent_registry is not None
|
|
302
|
+
|
|
303
|
+
# Check background tasks
|
|
304
|
+
checks["persistence_task"] = (
|
|
305
|
+
self._persistence_task is not None and
|
|
306
|
+
not self._persistence_task.done()
|
|
307
|
+
)
|
|
308
|
+
checks["cleanup_task"] = (
|
|
309
|
+
self._cleanup_task is not None and
|
|
310
|
+
not self._cleanup_task.done()
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
checks["modification_tracking"] = True
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
self.logger.error(f"Modification tracker health check failed: {e}")
|
|
317
|
+
checks["health_check_error"] = False
|
|
318
|
+
|
|
319
|
+
return checks
|
|
320
|
+
|
|
321
|
+
# ========================================================================
|
|
322
|
+
# Core Functionality
|
|
323
|
+
# ========================================================================
|
|
324
|
+
|
|
325
|
+
async def track_modification(self,
|
|
326
|
+
agent_name: str,
|
|
327
|
+
modification_type: ModificationType,
|
|
328
|
+
file_path: str,
|
|
329
|
+
tier: ModificationTier,
|
|
330
|
+
**kwargs) -> AgentModification:
|
|
331
|
+
"""Track an agent modification with comprehensive metadata collection."""
|
|
332
|
+
# Generate modification ID
|
|
333
|
+
modification_id = f"{agent_name}_{modification_type.value}_{uuid.uuid4().hex[:8]}"
|
|
334
|
+
|
|
335
|
+
# Collect file metadata
|
|
336
|
+
file_metadata = await self._collect_file_metadata(file_path, modification_type)
|
|
337
|
+
|
|
338
|
+
# Create backup if enabled
|
|
339
|
+
backup_path = None
|
|
340
|
+
if self.backup_enabled and modification_type in [ModificationType.MODIFY, ModificationType.DELETE]:
|
|
341
|
+
backup_path = await self._create_backup(file_path, modification_id)
|
|
342
|
+
|
|
343
|
+
# Create modification record
|
|
344
|
+
modification = AgentModification(
|
|
345
|
+
modification_id=modification_id,
|
|
346
|
+
agent_name=agent_name,
|
|
347
|
+
modification_type=modification_type,
|
|
348
|
+
tier=tier,
|
|
349
|
+
file_path=file_path,
|
|
350
|
+
timestamp=time.time(),
|
|
351
|
+
backup_path=backup_path,
|
|
352
|
+
**file_metadata,
|
|
353
|
+
**kwargs
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Validate modification if enabled
|
|
357
|
+
if self.validation_enabled:
|
|
358
|
+
await self._validate_modification(modification)
|
|
359
|
+
|
|
360
|
+
# Store in active modifications
|
|
361
|
+
self.active_modifications[modification_id] = modification
|
|
362
|
+
|
|
363
|
+
# Add to history
|
|
364
|
+
if agent_name not in self.modification_history:
|
|
365
|
+
self.modification_history[agent_name] = ModificationHistory(agent_name=agent_name)
|
|
366
|
+
|
|
367
|
+
self.modification_history[agent_name].add_modification(modification)
|
|
368
|
+
|
|
369
|
+
# Invalidate cache
|
|
370
|
+
if self.shared_cache:
|
|
371
|
+
await self._invalidate_agent_cache(agent_name)
|
|
372
|
+
|
|
373
|
+
# Trigger callbacks
|
|
374
|
+
await self._trigger_modification_callbacks(modification)
|
|
375
|
+
|
|
376
|
+
self.logger.info(
|
|
377
|
+
f"Tracked {modification_type.value} modification for agent '{agent_name}': {modification_id}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return modification
|
|
381
|
+
|
|
382
|
+
# ========================================================================
|
|
383
|
+
# Helper Methods
|
|
384
|
+
# ========================================================================
|
|
385
|
+
|
|
386
|
+
async def _initialize_integrations(self) -> None:
|
|
387
|
+
"""Initialize cache and registry integrations."""
|
|
388
|
+
try:
|
|
389
|
+
self.shared_cache = SharedPromptCache.get_instance()
|
|
390
|
+
self.agent_registry = AgentRegistry(cache_service=self.shared_cache)
|
|
391
|
+
self.logger.info("Successfully initialized cache and registry integrations")
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.logger.warning(f"Failed to initialize integrations: {e}")
|
|
394
|
+
|
|
395
|
+
async def _setup_file_monitoring(self) -> None:
|
|
396
|
+
"""Set up file system monitoring for agent files."""
|
|
397
|
+
try:
|
|
398
|
+
self.file_observer = Observer()
|
|
399
|
+
event_handler = AgentFileSystemHandler(self)
|
|
400
|
+
|
|
401
|
+
# Monitor standard agent directories
|
|
402
|
+
agent_dirs = [
|
|
403
|
+
Path("agents"),
|
|
404
|
+
Path("src/claude_mpm/agents"),
|
|
405
|
+
Path.home() / ".claude-pm" / "agents"
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
for agent_dir in agent_dirs:
|
|
409
|
+
if agent_dir.exists():
|
|
410
|
+
self.file_observer.schedule(event_handler, str(agent_dir), recursive=True)
|
|
411
|
+
self.watched_paths.add(agent_dir)
|
|
412
|
+
self.logger.info(f"Monitoring agent directory: {agent_dir}")
|
|
413
|
+
|
|
414
|
+
self.file_observer.start()
|
|
415
|
+
|
|
416
|
+
except Exception as e:
|
|
417
|
+
self.logger.error(f"Failed to setup file monitoring: {e}")
|
|
418
|
+
|
|
419
|
+
async def _collect_file_metadata(self, file_path: str, modification_type: ModificationType) -> Dict[str, Any]:
|
|
420
|
+
"""Collect comprehensive file metadata."""
|
|
421
|
+
metadata = {}
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
path = Path(file_path)
|
|
425
|
+
|
|
426
|
+
if path.exists() and modification_type != ModificationType.DELETE:
|
|
427
|
+
# File size
|
|
428
|
+
metadata['file_size_after'] = path.stat().st_size
|
|
429
|
+
|
|
430
|
+
# File hash
|
|
431
|
+
with open(path, 'rb') as f:
|
|
432
|
+
metadata['file_hash_after'] = hashlib.sha256(f.read()).hexdigest()
|
|
433
|
+
|
|
434
|
+
# File type
|
|
435
|
+
metadata['file_type'] = path.suffix
|
|
436
|
+
|
|
437
|
+
# Modification time
|
|
438
|
+
metadata['mtime'] = path.stat().st_mtime
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
self.logger.error(f"Error collecting file metadata: {e}")
|
|
442
|
+
|
|
443
|
+
return metadata
|
|
444
|
+
|
|
445
|
+
async def _create_backup(self, file_path: str, modification_id: str) -> Optional[str]:
|
|
446
|
+
"""Create backup of agent file."""
|
|
447
|
+
try:
|
|
448
|
+
source = Path(file_path)
|
|
449
|
+
if not source.exists():
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
# Create backup directory for this modification
|
|
453
|
+
backup_dir = self.backup_root / modification_id
|
|
454
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
455
|
+
|
|
456
|
+
# Create backup file
|
|
457
|
+
backup_path = backup_dir / source.name
|
|
458
|
+
shutil.copy2(source, backup_path)
|
|
459
|
+
|
|
460
|
+
# Save backup metadata
|
|
461
|
+
metadata = {
|
|
462
|
+
'original_path': str(file_path),
|
|
463
|
+
'backup_time': time.time(),
|
|
464
|
+
'modification_id': modification_id
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
metadata_path = backup_dir / 'metadata.json'
|
|
468
|
+
with open(metadata_path, 'w') as f:
|
|
469
|
+
json.dump(metadata, f, indent=2)
|
|
470
|
+
|
|
471
|
+
return str(backup_path)
|
|
472
|
+
|
|
473
|
+
except Exception as e:
|
|
474
|
+
self.logger.error(f"Failed to create backup: {e}")
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
async def _validate_modification(self, modification: AgentModification) -> None:
|
|
478
|
+
"""Validate agent modification."""
|
|
479
|
+
errors = []
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
# Check for conflicts
|
|
483
|
+
for mod_id, active_mod in self.active_modifications.items():
|
|
484
|
+
if (active_mod.agent_name == modification.agent_name and
|
|
485
|
+
active_mod.modification_id != modification.modification_id and
|
|
486
|
+
active_mod.age_seconds < 60): # Recent modification
|
|
487
|
+
errors.append(f"Potential conflict with recent modification: {mod_id}")
|
|
488
|
+
|
|
489
|
+
# Validate file path
|
|
490
|
+
if modification.modification_type != ModificationType.DELETE:
|
|
491
|
+
if not Path(modification.file_path).exists():
|
|
492
|
+
errors.append(f"File does not exist: {modification.file_path}")
|
|
493
|
+
|
|
494
|
+
# Update validation status
|
|
495
|
+
modification.validation_status = "failed" if errors else "passed"
|
|
496
|
+
modification.validation_errors = errors
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
self.logger.error(f"Validation error: {e}")
|
|
500
|
+
modification.validation_status = "error"
|
|
501
|
+
modification.validation_errors.append(str(e))
|
|
502
|
+
|
|
503
|
+
async def _invalidate_agent_cache(self, agent_name: str) -> None:
|
|
504
|
+
"""Invalidate cache entries for modified agent."""
|
|
505
|
+
if self.shared_cache:
|
|
506
|
+
try:
|
|
507
|
+
# Invalidate all cache entries for this agent
|
|
508
|
+
await self.shared_cache.invalidate_pattern(f"*{agent_name}*")
|
|
509
|
+
self.logger.debug(f"Invalidated cache for agent: {agent_name}")
|
|
510
|
+
except Exception as e:
|
|
511
|
+
self.logger.error(f"Failed to invalidate cache: {e}")
|
|
512
|
+
|
|
513
|
+
async def _handle_file_modification(self, file_path: str, modification_type: ModificationType) -> None:
|
|
514
|
+
"""Handle file system modification events."""
|
|
515
|
+
try:
|
|
516
|
+
# Extract agent information from path
|
|
517
|
+
agent_info = self._extract_agent_info_from_path(file_path)
|
|
518
|
+
if not agent_info:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
agent_name, tier = agent_info
|
|
522
|
+
|
|
523
|
+
# Track the modification
|
|
524
|
+
await self.track_modification(
|
|
525
|
+
agent_name=agent_name,
|
|
526
|
+
modification_type=modification_type,
|
|
527
|
+
file_path=file_path,
|
|
528
|
+
tier=tier,
|
|
529
|
+
source="file_system_monitor"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
except Exception as e:
|
|
533
|
+
self.logger.error(f"Error handling file modification {file_path}: {e}")
|
|
534
|
+
|
|
535
|
+
async def _handle_file_move(self, src_path: str, dest_path: str) -> None:
|
|
536
|
+
"""Handle file move events."""
|
|
537
|
+
try:
|
|
538
|
+
src_info = self._extract_agent_info_from_path(src_path)
|
|
539
|
+
|
|
540
|
+
if src_info:
|
|
541
|
+
agent_name, tier = src_info
|
|
542
|
+
await self.track_modification(
|
|
543
|
+
agent_name=agent_name,
|
|
544
|
+
modification_type=ModificationType.MOVE,
|
|
545
|
+
file_path=dest_path,
|
|
546
|
+
tier=tier,
|
|
547
|
+
source="file_system_monitor",
|
|
548
|
+
move_source=src_path,
|
|
549
|
+
move_destination=dest_path
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
self.logger.error(f"Error handling file move {src_path} -> {dest_path}: {e}")
|
|
554
|
+
|
|
555
|
+
def _extract_agent_info_from_path(self, file_path: str) -> Optional[Tuple[str, ModificationTier]]:
|
|
556
|
+
"""Extract agent name and tier from file path."""
|
|
557
|
+
try:
|
|
558
|
+
path = Path(file_path)
|
|
559
|
+
|
|
560
|
+
# Extract agent name from filename
|
|
561
|
+
agent_name = path.stem
|
|
562
|
+
if agent_name.endswith('_agent'):
|
|
563
|
+
agent_name = agent_name[:-6] # Remove _agent suffix
|
|
564
|
+
elif agent_name.endswith('-agent'):
|
|
565
|
+
agent_name = agent_name[:-6] # Remove -agent suffix
|
|
566
|
+
|
|
567
|
+
# Determine tier based on path
|
|
568
|
+
path_str = str(path).lower()
|
|
569
|
+
if 'system' in path_str or '/claude_mpm/agents/' in path_str:
|
|
570
|
+
tier = ModificationTier.SYSTEM
|
|
571
|
+
elif '.claude-pm' in path_str or str(Path.home()) in path_str:
|
|
572
|
+
tier = ModificationTier.USER
|
|
573
|
+
else:
|
|
574
|
+
tier = ModificationTier.PROJECT
|
|
575
|
+
|
|
576
|
+
return agent_name, tier
|
|
577
|
+
|
|
578
|
+
except Exception:
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
async def _trigger_modification_callbacks(self, modification: AgentModification) -> None:
|
|
582
|
+
"""Trigger registered modification callbacks."""
|
|
583
|
+
for callback in self.modification_callbacks:
|
|
584
|
+
try:
|
|
585
|
+
if asyncio.iscoroutinefunction(callback):
|
|
586
|
+
await callback(modification)
|
|
587
|
+
else:
|
|
588
|
+
callback(modification)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
self.logger.error(f"Modification callback failed: {e}")
|
|
591
|
+
|
|
592
|
+
# ========================================================================
|
|
593
|
+
# Persistence Methods
|
|
594
|
+
# ========================================================================
|
|
595
|
+
|
|
596
|
+
async def _load_modification_history(self) -> None:
|
|
597
|
+
"""Load modification history from disk."""
|
|
598
|
+
try:
|
|
599
|
+
# Load active modifications
|
|
600
|
+
active_path = self.persistence_root / 'active_modifications.json'
|
|
601
|
+
if active_path.exists():
|
|
602
|
+
with open(active_path, 'r') as f:
|
|
603
|
+
data = json.load(f)
|
|
604
|
+
self.active_modifications = {
|
|
605
|
+
k: AgentModification.from_dict(v) for k, v in data.items()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# Load modification history
|
|
609
|
+
for history_file in self.history_root.glob('*.json'):
|
|
610
|
+
with open(history_file, 'r') as f:
|
|
611
|
+
data = json.load(f)
|
|
612
|
+
agent_name = data['agent_name']
|
|
613
|
+
history = ModificationHistory(agent_name=agent_name)
|
|
614
|
+
|
|
615
|
+
# Recreate modifications
|
|
616
|
+
for mod_data in data.get('modifications', []):
|
|
617
|
+
history.add_modification(AgentModification.from_dict(mod_data))
|
|
618
|
+
|
|
619
|
+
history.current_version = data.get('current_version')
|
|
620
|
+
history.first_seen = data.get('first_seen')
|
|
621
|
+
history.last_modified = data.get('last_modified')
|
|
622
|
+
|
|
623
|
+
self.modification_history[agent_name] = history
|
|
624
|
+
|
|
625
|
+
self.logger.info(
|
|
626
|
+
f"Loaded {len(self.active_modifications)} active modifications and "
|
|
627
|
+
f"{len(self.modification_history)} agent histories"
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
self.logger.error(f"Failed to load modification history: {e}")
|
|
632
|
+
|
|
633
|
+
async def _save_modification_history(self) -> None:
|
|
634
|
+
"""Save modification history to disk."""
|
|
635
|
+
try:
|
|
636
|
+
# Save active modifications
|
|
637
|
+
active_data = {k: v.to_dict() for k, v in self.active_modifications.items()}
|
|
638
|
+
active_path = self.persistence_root / 'active_modifications.json'
|
|
639
|
+
|
|
640
|
+
with open(active_path, 'w') as f:
|
|
641
|
+
json.dump(active_data, f, indent=2)
|
|
642
|
+
|
|
643
|
+
# Save modification history
|
|
644
|
+
for agent_name, history in self.modification_history.items():
|
|
645
|
+
history_data = {
|
|
646
|
+
'agent_name': history.agent_name,
|
|
647
|
+
'modifications': [mod.to_dict() for mod in history.modifications],
|
|
648
|
+
'current_version': history.current_version,
|
|
649
|
+
'total_modifications': history.total_modifications,
|
|
650
|
+
'first_seen': history.first_seen,
|
|
651
|
+
'last_modified': history.last_modified
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
history_path = self.history_root / f"{agent_name}_history.json"
|
|
655
|
+
with open(history_path, 'w') as f:
|
|
656
|
+
json.dump(history_data, f, indent=2)
|
|
657
|
+
|
|
658
|
+
self.logger.debug("Saved modification history to disk")
|
|
659
|
+
|
|
660
|
+
except Exception as e:
|
|
661
|
+
self.logger.error(f"Failed to save modification history: {e}")
|
|
662
|
+
|
|
663
|
+
async def _persistence_loop(self) -> None:
|
|
664
|
+
"""Background task to persist modification history."""
|
|
665
|
+
while not self._stop_event.is_set():
|
|
666
|
+
try:
|
|
667
|
+
await self._save_modification_history()
|
|
668
|
+
await asyncio.sleep(self.persistence_interval)
|
|
669
|
+
except asyncio.CancelledError:
|
|
670
|
+
break
|
|
671
|
+
except Exception as e:
|
|
672
|
+
self.logger.error(f"Persistence loop error: {e}")
|
|
673
|
+
await asyncio.sleep(self.persistence_interval)
|
|
674
|
+
|
|
675
|
+
async def _cleanup_loop(self) -> None:
|
|
676
|
+
"""Background task to cleanup old modifications and backups."""
|
|
677
|
+
while not self._stop_event.is_set():
|
|
678
|
+
try:
|
|
679
|
+
await self._cleanup_old_data()
|
|
680
|
+
await asyncio.sleep(3600) # Run every hour
|
|
681
|
+
except asyncio.CancelledError:
|
|
682
|
+
break
|
|
683
|
+
except Exception as e:
|
|
684
|
+
self.logger.error(f"Cleanup loop error: {e}")
|
|
685
|
+
await asyncio.sleep(3600)
|
|
686
|
+
|
|
687
|
+
async def _cleanup_old_data(self) -> None:
|
|
688
|
+
"""Clean up old modifications and backups."""
|
|
689
|
+
try:
|
|
690
|
+
cutoff_time = time.time() - (self.max_history_days * 24 * 3600)
|
|
691
|
+
|
|
692
|
+
# Clean up old modifications from active list
|
|
693
|
+
old_active = [
|
|
694
|
+
mod_id for mod_id, mod in self.active_modifications.items()
|
|
695
|
+
if mod.timestamp < cutoff_time
|
|
696
|
+
]
|
|
697
|
+
|
|
698
|
+
for mod_id in old_active:
|
|
699
|
+
del self.active_modifications[mod_id]
|
|
700
|
+
|
|
701
|
+
# Clean up old backups
|
|
702
|
+
backup_count = 0
|
|
703
|
+
for backup_dir in self.backup_root.iterdir():
|
|
704
|
+
if backup_dir.is_dir():
|
|
705
|
+
metadata_path = backup_dir / 'metadata.json'
|
|
706
|
+
if metadata_path.exists():
|
|
707
|
+
with open(metadata_path, 'r') as f:
|
|
708
|
+
metadata = json.load(f)
|
|
709
|
+
if metadata.get('backup_time', 0) < cutoff_time:
|
|
710
|
+
shutil.rmtree(backup_dir)
|
|
711
|
+
backup_count += 1
|
|
712
|
+
|
|
713
|
+
if old_active or backup_count > 0:
|
|
714
|
+
self.logger.info(
|
|
715
|
+
f"Cleaned up {len(old_active)} old modifications and {backup_count} old backups"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
except Exception as e:
|
|
719
|
+
self.logger.error(f"Failed to cleanup old data: {e}")
|
|
720
|
+
|
|
721
|
+
# ========================================================================
|
|
722
|
+
# Public API Methods
|
|
723
|
+
# ========================================================================
|
|
724
|
+
|
|
725
|
+
async def get_modification_history(self, agent_name: str) -> Optional[ModificationHistory]:
|
|
726
|
+
"""Get modification history for specific agent."""
|
|
727
|
+
return self.modification_history.get(agent_name)
|
|
728
|
+
|
|
729
|
+
async def get_recent_modifications(self, hours: int = 24) -> List[AgentModification]:
|
|
730
|
+
"""Get all recent modifications across all agents."""
|
|
731
|
+
cutoff = time.time() - (hours * 3600)
|
|
732
|
+
recent = []
|
|
733
|
+
|
|
734
|
+
for history in self.modification_history.values():
|
|
735
|
+
recent.extend([
|
|
736
|
+
mod for mod in history.modifications
|
|
737
|
+
if mod.timestamp >= cutoff
|
|
738
|
+
])
|
|
739
|
+
|
|
740
|
+
return sorted(recent, key=lambda x: x.timestamp, reverse=True)
|
|
741
|
+
|
|
742
|
+
async def restore_agent_backup(self, modification_id: str) -> bool:
|
|
743
|
+
"""Restore agent from backup."""
|
|
744
|
+
try:
|
|
745
|
+
modification = self.active_modifications.get(modification_id)
|
|
746
|
+
if not modification or not modification.backup_path:
|
|
747
|
+
return False
|
|
748
|
+
|
|
749
|
+
# Restore from backup
|
|
750
|
+
backup_path = Path(modification.backup_path)
|
|
751
|
+
if not backup_path.exists():
|
|
752
|
+
return False
|
|
753
|
+
|
|
754
|
+
original_path = Path(modification.file_path)
|
|
755
|
+
shutil.copy2(backup_path, original_path)
|
|
756
|
+
|
|
757
|
+
# Track restore operation
|
|
758
|
+
await self.track_modification(
|
|
759
|
+
agent_name=modification.agent_name,
|
|
760
|
+
modification_type=ModificationType.RESTORE,
|
|
761
|
+
file_path=modification.file_path,
|
|
762
|
+
tier=modification.tier,
|
|
763
|
+
restored_from=modification_id
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
return True
|
|
767
|
+
|
|
768
|
+
except Exception as e:
|
|
769
|
+
self.logger.error(f"Failed to restore agent backup: {e}")
|
|
770
|
+
return False
|
|
771
|
+
|
|
772
|
+
async def get_modification_stats(self) -> Dict[str, Any]:
|
|
773
|
+
"""Get comprehensive modification statistics."""
|
|
774
|
+
stats = {
|
|
775
|
+
'total_agents_tracked': len(self.modification_history),
|
|
776
|
+
'total_modifications': sum(h.total_modifications for h in self.modification_history.values()),
|
|
777
|
+
'active_modifications': len(self.active_modifications),
|
|
778
|
+
'watched_paths': len(self.watched_paths),
|
|
779
|
+
'monitoring_enabled': self.enable_monitoring,
|
|
780
|
+
'backup_enabled': self.backup_enabled,
|
|
781
|
+
'validation_enabled': self.validation_enabled
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
# Modification type breakdown
|
|
785
|
+
type_counts = {}
|
|
786
|
+
for history in self.modification_history.values():
|
|
787
|
+
for mod in history.modifications:
|
|
788
|
+
type_counts[mod.modification_type.value] = type_counts.get(mod.modification_type.value, 0) + 1
|
|
789
|
+
|
|
790
|
+
stats['modifications_by_type'] = type_counts
|
|
791
|
+
|
|
792
|
+
# Tier breakdown
|
|
793
|
+
tier_counts = {}
|
|
794
|
+
for history in self.modification_history.values():
|
|
795
|
+
for mod in history.modifications:
|
|
796
|
+
tier_counts[mod.tier.value] = tier_counts.get(mod.tier.value, 0) + 1
|
|
797
|
+
|
|
798
|
+
stats['modifications_by_tier'] = tier_counts
|
|
799
|
+
|
|
800
|
+
# Recent activity
|
|
801
|
+
recent_24h = await self.get_recent_modifications(24)
|
|
802
|
+
recent_7d = await self.get_recent_modifications(24 * 7)
|
|
803
|
+
|
|
804
|
+
stats['recent_activity'] = {
|
|
805
|
+
'last_24_hours': len(recent_24h),
|
|
806
|
+
'last_7_days': len(recent_7d)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
# Validation stats
|
|
810
|
+
validation_stats = {
|
|
811
|
+
'passed': 0,
|
|
812
|
+
'failed': 0,
|
|
813
|
+
'pending': 0,
|
|
814
|
+
'error': 0
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
for mod in self.active_modifications.values():
|
|
818
|
+
validation_stats[mod.validation_status] = validation_stats.get(mod.validation_status, 0) + 1
|
|
819
|
+
|
|
820
|
+
stats['validation_stats'] = validation_stats
|
|
821
|
+
|
|
822
|
+
# Backup stats
|
|
823
|
+
backup_stats = {
|
|
824
|
+
'total_backups': len(list(self.backup_root.iterdir())),
|
|
825
|
+
'backup_size_mb': sum(
|
|
826
|
+
f.stat().st_size for f in self.backup_root.rglob('*') if f.is_file()
|
|
827
|
+
) / (1024 * 1024)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
stats['backup_stats'] = backup_stats
|
|
831
|
+
|
|
832
|
+
return stats
|
|
833
|
+
|
|
834
|
+
def register_modification_callback(self, callback: Callable[[AgentModification], None]) -> None:
|
|
835
|
+
"""Register callback for modification events."""
|
|
836
|
+
self.modification_callbacks.append(callback)
|
|
837
|
+
|
|
838
|
+
def unregister_modification_callback(self, callback: Callable[[AgentModification], None]) -> None:
|
|
839
|
+
"""Unregister modification callback."""
|
|
840
|
+
if callback in self.modification_callbacks:
|
|
841
|
+
self.modification_callbacks.remove(callback)
|