claude-mpm 4.0.32__py3-none-any.whl → 4.1.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +70 -2
- claude_mpm/agents/OUTPUT_STYLE.md +0 -11
- claude_mpm/agents/WORKFLOW.md +14 -2
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +111 -33
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/cli/commands/agents.py +82 -0
- claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
- claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
- claude_mpm/cli/parsers/agents_parser.py +27 -0
- claude_mpm/cli/parsers/base_parser.py +6 -0
- claude_mpm/cli/startup_logging.py +75 -0
- claude_mpm/core/framework_loader.py +173 -84
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +323 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +92 -11
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +25 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +125 -163
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +20 -11
- claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +396 -13
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
- claude_mpm/services/agents/memory/content_manager.py +98 -105
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/config.py +165 -0
- claude_mpm/services/event_bus/event_bus.py +349 -0
- claude_mpm/services/event_bus/relay.py +297 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +81 -23
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +29 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
- claude_mpm/services/socketio/server/main.py +25 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +28 -9
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +82 -56
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -15,32 +15,25 @@ class AgentsDirectoryResolver:
|
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
17
|
working_directory: Path,
|
|
18
|
-
is_system_deployment: bool,
|
|
19
|
-
is_project_specific: bool,
|
|
20
18
|
):
|
|
21
19
|
"""
|
|
22
20
|
Initialize the resolver.
|
|
23
21
|
|
|
24
22
|
Args:
|
|
25
23
|
working_directory: Current working directory
|
|
26
|
-
is_system_deployment: Whether this is a system agent deployment
|
|
27
|
-
is_project_specific: Whether this is a project-specific deployment
|
|
28
24
|
"""
|
|
29
25
|
self.working_directory = working_directory
|
|
30
|
-
self.is_system_deployment = is_system_deployment
|
|
31
|
-
self.is_project_specific = is_project_specific
|
|
32
26
|
|
|
33
27
|
def determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
|
|
34
28
|
"""
|
|
35
29
|
Determine the correct agents directory based on input.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Project-specific agents from <project>/.claude-mpm/agents/ → Deploy to <project>/.claude/agents/
|
|
30
|
+
|
|
31
|
+
MODIFIED: Always deploy to project .claude/agents directory
|
|
32
|
+
regardless of agent source (system, user, or project).
|
|
33
|
+
|
|
34
|
+
This ensures all agents are deployed at the project level while
|
|
35
|
+
maintaining discovery from both user (~/.claude-mpm) and project
|
|
36
|
+
(.claude-mpm) directories.
|
|
44
37
|
|
|
45
38
|
Args:
|
|
46
39
|
target_dir: Optional target directory
|
|
@@ -49,17 +42,9 @@ class AgentsDirectoryResolver:
|
|
|
49
42
|
Path to agents directory
|
|
50
43
|
"""
|
|
51
44
|
if not target_dir:
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
# System agents go to user's home ~/.claude/agents/
|
|
56
|
-
return Path.home() / ".claude" / "agents"
|
|
57
|
-
elif self.is_project_specific:
|
|
58
|
-
# Project agents stay in project directory
|
|
59
|
-
return self.working_directory / ".claude" / "agents"
|
|
60
|
-
else:
|
|
61
|
-
# Default: User custom agents go to home ~/.claude/agents/
|
|
62
|
-
return Path.home() / ".claude" / "agents"
|
|
45
|
+
# Always deploy to project directory
|
|
46
|
+
# This is the key change - all agents go to project .claude/agents
|
|
47
|
+
return self.working_directory / ".claude" / "agents"
|
|
63
48
|
|
|
64
49
|
# If target_dir provided, use it directly (caller decides structure)
|
|
65
50
|
target_dir = Path(target_dir)
|
|
@@ -13,6 +13,7 @@ Key Features:
|
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import logging
|
|
16
|
+
import os
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Dict, List, Optional, Tuple
|
|
18
19
|
|
|
@@ -190,8 +191,9 @@ class MultiSourceAgentDeploymentService:
|
|
|
190
191
|
user_agents_dir: Optional[Path] = None,
|
|
191
192
|
working_directory: Optional[Path] = None,
|
|
192
193
|
excluded_agents: Optional[List[str]] = None,
|
|
193
|
-
config: Optional[Config] = None
|
|
194
|
-
|
|
194
|
+
config: Optional[Config] = None,
|
|
195
|
+
cleanup_outdated: bool = True
|
|
196
|
+
) -> Tuple[Dict[str, Path], Dict[str, str], Dict[str, Any]]:
|
|
195
197
|
"""Get the highest version agents from all sources for deployment.
|
|
196
198
|
|
|
197
199
|
Args:
|
|
@@ -201,11 +203,13 @@ class MultiSourceAgentDeploymentService:
|
|
|
201
203
|
working_directory: Current working directory for finding project agents
|
|
202
204
|
excluded_agents: List of agent names to exclude from deployment
|
|
203
205
|
config: Configuration object for additional filtering
|
|
206
|
+
cleanup_outdated: Whether to cleanup outdated user agents (default: True)
|
|
204
207
|
|
|
205
208
|
Returns:
|
|
206
209
|
Tuple of:
|
|
207
210
|
- Dictionary mapping agent names to template file paths
|
|
208
211
|
- Dictionary mapping agent names to their source
|
|
212
|
+
- Dictionary with cleanup results (removed, preserved, errors)
|
|
209
213
|
"""
|
|
210
214
|
# Discover all available agents
|
|
211
215
|
agents_by_name = self.discover_agents_from_all_sources(
|
|
@@ -218,6 +222,27 @@ class MultiSourceAgentDeploymentService:
|
|
|
218
222
|
# Select highest version for each agent
|
|
219
223
|
selected_agents = self.select_highest_version_agents(agents_by_name)
|
|
220
224
|
|
|
225
|
+
# Clean up outdated user agents if enabled
|
|
226
|
+
cleanup_results = {"removed": [], "preserved": [], "errors": []}
|
|
227
|
+
if cleanup_outdated:
|
|
228
|
+
# Check if cleanup is enabled in config or environment
|
|
229
|
+
cleanup_enabled = True
|
|
230
|
+
|
|
231
|
+
# Check environment variable first (for CI/CD and testing)
|
|
232
|
+
env_cleanup = os.environ.get("CLAUDE_MPM_CLEANUP_USER_AGENTS", "").lower()
|
|
233
|
+
if env_cleanup in ["false", "0", "no", "disabled"]:
|
|
234
|
+
cleanup_enabled = False
|
|
235
|
+
self.logger.debug("User agent cleanup disabled via environment variable")
|
|
236
|
+
|
|
237
|
+
# Check config if environment doesn't disable it
|
|
238
|
+
if cleanup_enabled and config:
|
|
239
|
+
cleanup_enabled = config.get("agent_deployment.cleanup_outdated_user_agents", True)
|
|
240
|
+
|
|
241
|
+
if cleanup_enabled:
|
|
242
|
+
cleanup_results = self.cleanup_outdated_user_agents(
|
|
243
|
+
agents_by_name, selected_agents
|
|
244
|
+
)
|
|
245
|
+
|
|
221
246
|
# Apply exclusion filters
|
|
222
247
|
if excluded_agents:
|
|
223
248
|
for agent_name in excluded_agents:
|
|
@@ -256,7 +281,168 @@ class MultiSourceAgentDeploymentService:
|
|
|
256
281
|
f"user: {sum(1 for s in agent_sources.values() if s == 'user')})"
|
|
257
282
|
)
|
|
258
283
|
|
|
259
|
-
return agents_to_deploy, agent_sources
|
|
284
|
+
return agents_to_deploy, agent_sources, cleanup_results
|
|
285
|
+
|
|
286
|
+
def cleanup_outdated_user_agents(
|
|
287
|
+
self,
|
|
288
|
+
agents_by_name: Dict[str, List[Dict[str, Any]]],
|
|
289
|
+
selected_agents: Dict[str, Dict[str, Any]]
|
|
290
|
+
) -> Dict[str, Any]:
|
|
291
|
+
"""Remove outdated user agents when project or system agents have higher versions.
|
|
292
|
+
|
|
293
|
+
WHY: When project agents are updated to newer versions, outdated user agent
|
|
294
|
+
copies should be removed to prevent confusion and ensure the latest version
|
|
295
|
+
is always used. User agents with same or higher versions are preserved to
|
|
296
|
+
respect user customizations.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
agents_by_name: Dictionary mapping agent names to list of agent info from different sources
|
|
300
|
+
selected_agents: Dictionary mapping agent names to the selected highest version agent
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Dictionary with cleanup results:
|
|
304
|
+
- removed: List of removed agent info
|
|
305
|
+
- preserved: List of preserved agent info with reasons
|
|
306
|
+
- errors: List of errors during cleanup
|
|
307
|
+
"""
|
|
308
|
+
cleanup_results = {
|
|
309
|
+
"removed": [],
|
|
310
|
+
"preserved": [],
|
|
311
|
+
"errors": []
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Get user agents directory
|
|
315
|
+
user_agents_dir = Path.home() / ".claude-mpm" / "agents"
|
|
316
|
+
|
|
317
|
+
# Safety check - only operate on user agents directory
|
|
318
|
+
if not user_agents_dir.exists():
|
|
319
|
+
self.logger.debug("User agents directory does not exist, no cleanup needed")
|
|
320
|
+
return cleanup_results
|
|
321
|
+
|
|
322
|
+
for agent_name, agent_versions in agents_by_name.items():
|
|
323
|
+
# Skip if only one version exists
|
|
324
|
+
if len(agent_versions) < 2:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
selected = selected_agents.get(agent_name)
|
|
328
|
+
if not selected:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
# Process each version of this agent
|
|
332
|
+
for agent_info in agent_versions:
|
|
333
|
+
# Only consider user agents for cleanup
|
|
334
|
+
if agent_info["source"] != "user":
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Safety check - ensure path is within user agents directory
|
|
338
|
+
user_agent_path = Path(agent_info["path"])
|
|
339
|
+
try:
|
|
340
|
+
# Resolve paths to compare them safely
|
|
341
|
+
resolved_user_path = user_agent_path.resolve()
|
|
342
|
+
resolved_user_agents_dir = user_agents_dir.resolve()
|
|
343
|
+
|
|
344
|
+
# Verify the agent is actually in the user agents directory
|
|
345
|
+
if not str(resolved_user_path).startswith(str(resolved_user_agents_dir)):
|
|
346
|
+
self.logger.warning(
|
|
347
|
+
f"Skipping cleanup for {agent_name}: path {user_agent_path} "
|
|
348
|
+
f"is not within user agents directory"
|
|
349
|
+
)
|
|
350
|
+
cleanup_results["errors"].append({
|
|
351
|
+
"agent": agent_name,
|
|
352
|
+
"error": "Path outside user agents directory"
|
|
353
|
+
})
|
|
354
|
+
continue
|
|
355
|
+
except Exception as e:
|
|
356
|
+
self.logger.error(f"Error resolving paths for {agent_name}: {e}")
|
|
357
|
+
cleanup_results["errors"].append({
|
|
358
|
+
"agent": agent_name,
|
|
359
|
+
"error": f"Path resolution error: {e}"
|
|
360
|
+
})
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# Compare versions
|
|
364
|
+
user_version = self.version_manager.parse_version(
|
|
365
|
+
agent_info.get("version", "0.0.0")
|
|
366
|
+
)
|
|
367
|
+
selected_version = self.version_manager.parse_version(
|
|
368
|
+
selected.get("version", "0.0.0")
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
version_comparison = self.version_manager.compare_versions(
|
|
372
|
+
user_version, selected_version
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Determine action based on version comparison and selected source
|
|
376
|
+
if version_comparison < 0 and selected["source"] in ["project", "system"]:
|
|
377
|
+
# User agent has lower version than selected project/system agent - remove it
|
|
378
|
+
if user_agent_path.exists():
|
|
379
|
+
try:
|
|
380
|
+
# Log before removal for audit trail
|
|
381
|
+
self.logger.info(
|
|
382
|
+
f"Removing outdated user agent: {agent_name} "
|
|
383
|
+
f"v{self.version_manager.format_version_display(user_version)} "
|
|
384
|
+
f"(superseded by {selected['source']} "
|
|
385
|
+
f"v{self.version_manager.format_version_display(selected_version)})"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Remove the file
|
|
389
|
+
user_agent_path.unlink()
|
|
390
|
+
|
|
391
|
+
cleanup_results["removed"].append({
|
|
392
|
+
"name": agent_name,
|
|
393
|
+
"version": self.version_manager.format_version_display(user_version),
|
|
394
|
+
"path": str(user_agent_path),
|
|
395
|
+
"reason": f"Superseded by {selected['source']} v{self.version_manager.format_version_display(selected_version)}"
|
|
396
|
+
})
|
|
397
|
+
except PermissionError as e:
|
|
398
|
+
error_msg = f"Permission denied removing {agent_name}: {e}"
|
|
399
|
+
self.logger.error(error_msg)
|
|
400
|
+
cleanup_results["errors"].append({
|
|
401
|
+
"agent": agent_name,
|
|
402
|
+
"error": error_msg
|
|
403
|
+
})
|
|
404
|
+
except Exception as e:
|
|
405
|
+
error_msg = f"Error removing {agent_name}: {e}"
|
|
406
|
+
self.logger.error(error_msg)
|
|
407
|
+
cleanup_results["errors"].append({
|
|
408
|
+
"agent": agent_name,
|
|
409
|
+
"error": error_msg
|
|
410
|
+
})
|
|
411
|
+
else:
|
|
412
|
+
# Preserve the user agent
|
|
413
|
+
if version_comparison >= 0:
|
|
414
|
+
reason = "User version same or higher than selected version"
|
|
415
|
+
elif selected["source"] == "user":
|
|
416
|
+
reason = "User agent is the selected version"
|
|
417
|
+
else:
|
|
418
|
+
reason = "User customization preserved"
|
|
419
|
+
|
|
420
|
+
cleanup_results["preserved"].append({
|
|
421
|
+
"name": agent_name,
|
|
422
|
+
"version": self.version_manager.format_version_display(user_version),
|
|
423
|
+
"reason": reason
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
self.logger.debug(
|
|
427
|
+
f"Preserving user agent {agent_name} "
|
|
428
|
+
f"v{self.version_manager.format_version_display(user_version)}: {reason}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Log cleanup summary
|
|
432
|
+
if cleanup_results["removed"]:
|
|
433
|
+
self.logger.info(
|
|
434
|
+
f"Cleanup complete: removed {len(cleanup_results['removed'])} outdated user agents"
|
|
435
|
+
)
|
|
436
|
+
if cleanup_results["preserved"]:
|
|
437
|
+
self.logger.debug(
|
|
438
|
+
f"Preserved {len(cleanup_results['preserved'])} user agents"
|
|
439
|
+
)
|
|
440
|
+
if cleanup_results["errors"]:
|
|
441
|
+
self.logger.warning(
|
|
442
|
+
f"Encountered {len(cleanup_results['errors'])} errors during cleanup"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return cleanup_results
|
|
260
446
|
|
|
261
447
|
def _apply_config_filters(
|
|
262
448
|
self,
|
|
@@ -321,6 +507,7 @@ class MultiSourceAgentDeploymentService:
|
|
|
321
507
|
"needs_update": [],
|
|
322
508
|
"up_to_date": [],
|
|
323
509
|
"new_agents": [],
|
|
510
|
+
"orphaned_agents": [], # Agents without templates
|
|
324
511
|
"version_upgrades": [],
|
|
325
512
|
"version_downgrades": [],
|
|
326
513
|
"source_changes": []
|
|
@@ -341,9 +528,11 @@ class MultiSourceAgentDeploymentService:
|
|
|
341
528
|
# Read template version
|
|
342
529
|
try:
|
|
343
530
|
template_data = json.loads(template_path.read_text())
|
|
531
|
+
metadata = template_data.get("metadata", {})
|
|
344
532
|
template_version = self.version_manager.parse_version(
|
|
345
533
|
template_data.get("agent_version") or
|
|
346
|
-
template_data.get("version"
|
|
534
|
+
template_data.get("version") or
|
|
535
|
+
metadata.get("version", "0.0.0")
|
|
347
536
|
)
|
|
348
537
|
except Exception as e:
|
|
349
538
|
self.logger.warning(f"Error reading template for '{agent_name}': {e}")
|
|
@@ -411,13 +600,20 @@ class MultiSourceAgentDeploymentService:
|
|
|
411
600
|
"source": agent_sources[agent_name]
|
|
412
601
|
})
|
|
413
602
|
|
|
603
|
+
# Check for orphaned agents (deployed but no template)
|
|
604
|
+
orphaned = self._detect_orphaned_agents_simple(deployed_agents_dir, agents_to_deploy)
|
|
605
|
+
comparison_results["orphaned_agents"] = orphaned
|
|
606
|
+
|
|
414
607
|
# Log summary
|
|
415
|
-
|
|
416
|
-
f"
|
|
417
|
-
f"{len(comparison_results['
|
|
418
|
-
f"{len(comparison_results['up_to_date'])} up to date, "
|
|
608
|
+
summary_parts = [
|
|
609
|
+
f"{len(comparison_results['needs_update'])} need updates",
|
|
610
|
+
f"{len(comparison_results['up_to_date'])} up to date",
|
|
419
611
|
f"{len(comparison_results['new_agents'])} new agents"
|
|
420
|
-
|
|
612
|
+
]
|
|
613
|
+
if comparison_results["orphaned_agents"]:
|
|
614
|
+
summary_parts.append(f"{len(comparison_results['orphaned_agents'])} orphaned")
|
|
615
|
+
|
|
616
|
+
self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
|
|
421
617
|
|
|
422
618
|
if comparison_results["version_upgrades"]:
|
|
423
619
|
for upgrade in comparison_results["version_upgrades"]:
|
|
@@ -436,10 +632,24 @@ class MultiSourceAgentDeploymentService:
|
|
|
436
632
|
|
|
437
633
|
if comparison_results["version_downgrades"]:
|
|
438
634
|
for downgrade in comparison_results["version_downgrades"]:
|
|
439
|
-
|
|
440
|
-
|
|
635
|
+
# Changed from warning to debug - deployed versions higher than templates
|
|
636
|
+
# are not errors, just informational
|
|
637
|
+
self.logger.debug(
|
|
638
|
+
f" Note: {downgrade['name']} deployed version "
|
|
441
639
|
f"{downgrade['deployed_version']} is higher than template "
|
|
442
|
-
f"{downgrade['template_version']}"
|
|
640
|
+
f"{downgrade['template_version']} (keeping deployed version)"
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# Log orphaned agents if found
|
|
644
|
+
if comparison_results["orphaned_agents"]:
|
|
645
|
+
self.logger.info(
|
|
646
|
+
f"Found {len(comparison_results['orphaned_agents'])} orphaned agent(s) "
|
|
647
|
+
f"(deployed without templates):"
|
|
648
|
+
)
|
|
649
|
+
for orphan in comparison_results["orphaned_agents"]:
|
|
650
|
+
self.logger.info(
|
|
651
|
+
f" - {orphan['name']} v{orphan['version']} "
|
|
652
|
+
f"(consider removing or creating a template)"
|
|
443
653
|
)
|
|
444
654
|
|
|
445
655
|
return comparison_results
|
|
@@ -506,4 +716,177 @@ class MultiSourceAgentDeploymentService:
|
|
|
506
716
|
return "system"
|
|
507
717
|
|
|
508
718
|
# Complex names are more likely to be user/project agents
|
|
509
|
-
return "user"
|
|
719
|
+
return "user"
|
|
720
|
+
|
|
721
|
+
def detect_orphaned_agents(
|
|
722
|
+
self,
|
|
723
|
+
deployed_agents_dir: Path,
|
|
724
|
+
available_agents: Dict[str, Any]
|
|
725
|
+
) -> List[Dict[str, Any]]:
|
|
726
|
+
"""Detect deployed agents that don't have corresponding templates.
|
|
727
|
+
|
|
728
|
+
WHY: Orphaned agents can cause confusion with version warnings.
|
|
729
|
+
This method identifies them so they can be handled appropriately.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
deployed_agents_dir: Directory containing deployed agents
|
|
733
|
+
available_agents: Dictionary of available agents from all sources
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
List of orphaned agent information
|
|
737
|
+
"""
|
|
738
|
+
orphaned = []
|
|
739
|
+
|
|
740
|
+
if not deployed_agents_dir.exists():
|
|
741
|
+
return orphaned
|
|
742
|
+
|
|
743
|
+
# Build a mapping of file stems to agent names for comparison
|
|
744
|
+
# Since available_agents uses display names like "Code Analysis Agent"
|
|
745
|
+
# but deployed files use stems like "code_analyzer"
|
|
746
|
+
available_stems = set()
|
|
747
|
+
stem_to_name = {}
|
|
748
|
+
|
|
749
|
+
for agent_name, agent_sources in available_agents.items():
|
|
750
|
+
# Get the file path from the first source to extract the stem
|
|
751
|
+
if agent_sources and isinstance(agent_sources, list) and len(agent_sources) > 0:
|
|
752
|
+
first_source = agent_sources[0]
|
|
753
|
+
if 'file_path' in first_source:
|
|
754
|
+
file_path = Path(first_source['file_path'])
|
|
755
|
+
stem = file_path.stem
|
|
756
|
+
available_stems.add(stem)
|
|
757
|
+
stem_to_name[stem] = agent_name
|
|
758
|
+
|
|
759
|
+
for deployed_file in deployed_agents_dir.glob("*.md"):
|
|
760
|
+
agent_stem = deployed_file.stem
|
|
761
|
+
|
|
762
|
+
# Skip if this agent has a template (check by stem, not display name)
|
|
763
|
+
if agent_stem in available_stems:
|
|
764
|
+
continue
|
|
765
|
+
|
|
766
|
+
# This is an orphaned agent
|
|
767
|
+
try:
|
|
768
|
+
deployed_content = deployed_file.read_text()
|
|
769
|
+
deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
|
|
770
|
+
deployed_content
|
|
771
|
+
)
|
|
772
|
+
version_str = self.version_manager.format_version_display(deployed_version)
|
|
773
|
+
except Exception:
|
|
774
|
+
version_str = "unknown"
|
|
775
|
+
|
|
776
|
+
orphaned.append({
|
|
777
|
+
"name": agent_stem,
|
|
778
|
+
"file": str(deployed_file),
|
|
779
|
+
"version": version_str
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
return orphaned
|
|
783
|
+
|
|
784
|
+
def _detect_orphaned_agents_simple(
|
|
785
|
+
self,
|
|
786
|
+
deployed_agents_dir: Path,
|
|
787
|
+
agents_to_deploy: Dict[str, Path]
|
|
788
|
+
) -> List[Dict[str, Any]]:
|
|
789
|
+
"""Simple orphan detection that works with agents_to_deploy structure.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
deployed_agents_dir: Directory containing deployed agents
|
|
793
|
+
agents_to_deploy: Dictionary mapping file stems to template paths
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
List of orphaned agent information
|
|
797
|
+
"""
|
|
798
|
+
orphaned = []
|
|
799
|
+
|
|
800
|
+
if not deployed_agents_dir.exists():
|
|
801
|
+
return orphaned
|
|
802
|
+
|
|
803
|
+
# agents_to_deploy already contains file stems as keys
|
|
804
|
+
available_stems = set(agents_to_deploy.keys())
|
|
805
|
+
|
|
806
|
+
for deployed_file in deployed_agents_dir.glob("*.md"):
|
|
807
|
+
agent_stem = deployed_file.stem
|
|
808
|
+
|
|
809
|
+
# Skip if this agent has a template (check by stem)
|
|
810
|
+
if agent_stem in available_stems:
|
|
811
|
+
continue
|
|
812
|
+
|
|
813
|
+
# This is an orphaned agent
|
|
814
|
+
try:
|
|
815
|
+
deployed_content = deployed_file.read_text()
|
|
816
|
+
deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
|
|
817
|
+
deployed_content
|
|
818
|
+
)
|
|
819
|
+
version_str = self.version_manager.format_version_display(deployed_version)
|
|
820
|
+
except Exception:
|
|
821
|
+
version_str = "unknown"
|
|
822
|
+
|
|
823
|
+
orphaned.append({
|
|
824
|
+
"name": agent_stem,
|
|
825
|
+
"file": str(deployed_file),
|
|
826
|
+
"version": version_str
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
return orphaned
|
|
830
|
+
|
|
831
|
+
def cleanup_orphaned_agents(
|
|
832
|
+
self,
|
|
833
|
+
deployed_agents_dir: Path,
|
|
834
|
+
dry_run: bool = True
|
|
835
|
+
) -> Dict[str, Any]:
|
|
836
|
+
"""Clean up orphaned agents that don't have templates.
|
|
837
|
+
|
|
838
|
+
WHY: Orphaned agents can accumulate over time and cause confusion.
|
|
839
|
+
This method provides a way to clean them up systematically.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
deployed_agents_dir: Directory containing deployed agents
|
|
843
|
+
dry_run: If True, only report what would be removed
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
Dictionary with cleanup results
|
|
847
|
+
"""
|
|
848
|
+
results = {
|
|
849
|
+
"orphaned": [],
|
|
850
|
+
"removed": [],
|
|
851
|
+
"errors": []
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
# First, discover all available agents from all sources
|
|
855
|
+
all_agents = self.discover_agents_from_all_sources()
|
|
856
|
+
available_names = set(all_agents.keys())
|
|
857
|
+
|
|
858
|
+
# Detect orphaned agents
|
|
859
|
+
orphaned = self.detect_orphaned_agents(deployed_agents_dir, all_agents)
|
|
860
|
+
results["orphaned"] = orphaned
|
|
861
|
+
|
|
862
|
+
if not orphaned:
|
|
863
|
+
self.logger.info("No orphaned agents found")
|
|
864
|
+
return results
|
|
865
|
+
|
|
866
|
+
self.logger.info(f"Found {len(orphaned)} orphaned agent(s)")
|
|
867
|
+
|
|
868
|
+
for orphan in orphaned:
|
|
869
|
+
agent_file = Path(orphan["file"])
|
|
870
|
+
|
|
871
|
+
if dry_run:
|
|
872
|
+
self.logger.info(
|
|
873
|
+
f" Would remove: {orphan['name']} v{orphan['version']}"
|
|
874
|
+
)
|
|
875
|
+
else:
|
|
876
|
+
try:
|
|
877
|
+
agent_file.unlink()
|
|
878
|
+
results["removed"].append(orphan["name"])
|
|
879
|
+
self.logger.info(
|
|
880
|
+
f" Removed: {orphan['name']} v{orphan['version']}"
|
|
881
|
+
)
|
|
882
|
+
except Exception as e:
|
|
883
|
+
error_msg = f"Failed to remove {orphan['name']}: {e}"
|
|
884
|
+
results["errors"].append(error_msg)
|
|
885
|
+
self.logger.error(f" {error_msg}")
|
|
886
|
+
|
|
887
|
+
if dry_run and orphaned:
|
|
888
|
+
self.logger.info(
|
|
889
|
+
"Run with dry_run=False to actually remove orphaned agents"
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
return results
|
|
@@ -37,8 +37,9 @@ class TargetDirectorySetupStep(BaseDeploymentStep):
|
|
|
37
37
|
if context.target_dir:
|
|
38
38
|
context.actual_target_dir = context.target_dir
|
|
39
39
|
else:
|
|
40
|
-
# Default to
|
|
41
|
-
|
|
40
|
+
# MODIFIED: Default to project .claude/agents directory
|
|
41
|
+
# All agents now deploy to the project level
|
|
42
|
+
context.actual_target_dir = Path.cwd() / ".claude" / "agents"
|
|
42
43
|
|
|
43
44
|
# Create target directory if it doesn't exist
|
|
44
45
|
context.actual_target_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -63,15 +63,22 @@ class SystemAgentDeploymentStrategy(BaseDeploymentStrategy):
|
|
|
63
63
|
def determine_target_directory(self, context: DeploymentContext) -> Path:
|
|
64
64
|
"""Determine target directory for system agents.
|
|
65
65
|
|
|
66
|
-
System agents are
|
|
66
|
+
MODIFIED: System agents are now deployed to project .claude/agents/
|
|
67
|
+
to maintain consistency with the new deployment behavior.
|
|
68
|
+
All agents (system, user, project) deploy to the project level.
|
|
67
69
|
|
|
68
70
|
Args:
|
|
69
71
|
context: Deployment context
|
|
70
72
|
|
|
71
73
|
Returns:
|
|
72
|
-
Path to
|
|
74
|
+
Path to <project>/.claude/agents/
|
|
73
75
|
"""
|
|
74
|
-
|
|
76
|
+
# Always deploy to project directory
|
|
77
|
+
if context.working_directory:
|
|
78
|
+
return context.working_directory / ".claude" / "agents"
|
|
79
|
+
else:
|
|
80
|
+
# Fallback to current working directory if not specified
|
|
81
|
+
return Path.cwd() / ".claude" / "agents"
|
|
75
82
|
|
|
76
83
|
def get_templates_directory(self, context: DeploymentContext) -> Path:
|
|
77
84
|
"""Get templates directory for system agents.
|
|
@@ -57,26 +57,22 @@ class UserAgentDeploymentStrategy(BaseDeploymentStrategy):
|
|
|
57
57
|
def determine_target_directory(self, context: DeploymentContext) -> Path:
|
|
58
58
|
"""Determine target directory for user agents.
|
|
59
59
|
|
|
60
|
-
User agents
|
|
60
|
+
MODIFIED: User agents are now deployed to project .claude/agents/
|
|
61
|
+
to maintain consistency with the new deployment behavior.
|
|
62
|
+
All agents (system, user, project) deploy to the project level.
|
|
61
63
|
|
|
62
64
|
Args:
|
|
63
65
|
context: Deployment context
|
|
64
66
|
|
|
65
67
|
Returns:
|
|
66
|
-
Path to
|
|
68
|
+
Path to <project>/.claude/agents/
|
|
67
69
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if "CLAUDE_MPM_USER_PWD" in os.environ:
|
|
75
|
-
user_pwd = Path(os.environ["CLAUDE_MPM_USER_PWD"])
|
|
76
|
-
return user_pwd / ".claude" / "agents"
|
|
77
|
-
|
|
78
|
-
# Default to user's home directory
|
|
79
|
-
return Path.home() / ".claude-mpm" / "agents"
|
|
70
|
+
# Always deploy to project directory
|
|
71
|
+
if context.working_directory:
|
|
72
|
+
return context.working_directory / ".claude" / "agents"
|
|
73
|
+
else:
|
|
74
|
+
# Fallback to current working directory if not specified
|
|
75
|
+
return Path.cwd() / ".claude" / "agents"
|
|
80
76
|
|
|
81
77
|
def get_templates_directory(self, context: DeploymentContext) -> Path:
|
|
82
78
|
"""Get templates directory for user agents.
|