claude-mpm 5.4.3__py3-none-any.whl → 5.4.21__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/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +166 -21
- claude_mpm/agents/agent_loader.py +3 -27
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agents.py +0 -31
- claude_mpm/cli/commands/auto_configure.py +210 -25
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +85 -43
- claude_mpm/cli/commands/configure_agent_display.py +3 -1
- claude_mpm/cli/commands/mpm_init/core.py +2 -45
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/executor.py +3 -3
- claude_mpm/cli/parsers/agents_parser.py +0 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +490 -41
- claude_mpm/commands/mpm-config.md +265 -0
- claude_mpm/commands/mpm-help.md +14 -95
- claude_mpm/commands/mpm-organize.md +350 -153
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +176 -76
- claude_mpm/hooks/claude_hooks/hook_handler.py +2 -0
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +0 -19
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +6 -7
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +1 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +13 -0
- claude_mpm/services/agents/git_source_manager.py +14 -0
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/toolchain_detector.py +6 -3
- claude_mpm/services/command_deployment_service.py +81 -8
- claude_mpm/services/git/git_operations_service.py +93 -8
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +32 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/METADATA +47 -8
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/RECORD +58 -82
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/entry_points.txt +0 -3
- claude_mpm-5.4.21.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.21.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_ENGINEER.md +0 -658
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent.json +0 -31
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm-5.4.3.dist-info/licenses/LICENSE +0 -21
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/top_level.txt +0 -0
claude_mpm/init.py
CHANGED
|
@@ -141,25 +141,6 @@ class ProjectInitializer:
|
|
|
141
141
|
if not gitignore.exists():
|
|
142
142
|
gitignore.write_text("logs/\n*.log\n*.pyc\n__pycache__/\n")
|
|
143
143
|
|
|
144
|
-
# Also ensure MCP directories are in main project .gitignore
|
|
145
|
-
try:
|
|
146
|
-
from claude_mpm.services.project.project_organizer import (
|
|
147
|
-
ProjectOrganizer,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Check if we're in a git repository
|
|
151
|
-
if (project_root / ".git").exists():
|
|
152
|
-
organizer = ProjectOrganizer(project_root)
|
|
153
|
-
# This will add MCP directories and other standard patterns
|
|
154
|
-
organizer.update_gitignore()
|
|
155
|
-
self.logger.debug(
|
|
156
|
-
"Updated project .gitignore with MCP and standard patterns"
|
|
157
|
-
)
|
|
158
|
-
except Exception as e:
|
|
159
|
-
self.logger.debug(
|
|
160
|
-
f"Could not update project gitignore with MCP patterns: {e}"
|
|
161
|
-
)
|
|
162
|
-
|
|
163
144
|
# Log successful creation with details
|
|
164
145
|
self.logger.info(f"Initialized project directory at {self.project_dir}")
|
|
165
146
|
self.logger.debug("Created directories: agents, config, responses, logs")
|
|
@@ -88,18 +88,21 @@ fi
|
|
|
88
88
|
#
|
|
89
89
|
# STRATEGY:
|
|
90
90
|
# This function implements a fallback chain to find Python with claude-mpm dependencies:
|
|
91
|
-
# 1.
|
|
92
|
-
# 2.
|
|
93
|
-
# 3.
|
|
94
|
-
# 4.
|
|
91
|
+
# 1. UV-managed projects (uv.lock detected) - uses "uv run python"
|
|
92
|
+
# 2. pipx installations - uses pipx venv Python
|
|
93
|
+
# 3. Project-specific virtual environments (venv, .venv)
|
|
94
|
+
# 4. Currently active virtual environment ($VIRTUAL_ENV)
|
|
95
|
+
# 5. System python3 (may lack dependencies)
|
|
96
|
+
# 6. System python (last resort)
|
|
95
97
|
#
|
|
96
98
|
# WHY THIS APPROACH:
|
|
97
99
|
# - Claude MPM requires specific packages (socketio, eventlet) not in system Python
|
|
98
|
-
# -
|
|
100
|
+
# - UV and virtual environments ensure dependency isolation and availability
|
|
99
101
|
# - Multiple naming conventions supported (venv vs .venv)
|
|
100
102
|
# - Graceful degradation to system Python if no venv found
|
|
101
103
|
#
|
|
102
104
|
# ACTIVATION STRATEGY:
|
|
105
|
+
# - UV projects: use "uv run python" to execute in UV-managed environment
|
|
103
106
|
# - Sources activate script to set up environment variables
|
|
104
107
|
# - Returns specific Python path for exec (not just 'python')
|
|
105
108
|
# - Maintains environment in same shell process
|
|
@@ -110,10 +113,18 @@ fi
|
|
|
110
113
|
# - Caches result in process environment
|
|
111
114
|
#
|
|
112
115
|
# RETURNS:
|
|
113
|
-
# Absolute path to Python executable with claude-mpm dependencies
|
|
116
|
+
# Absolute path to Python executable with claude-mpm dependencies, or "uv run python" for UV projects
|
|
114
117
|
#
|
|
115
118
|
find_python_command() {
|
|
116
|
-
# 1. Check
|
|
119
|
+
# 1. Check for UV project first (uv.lock or pyproject.toml with uv)
|
|
120
|
+
if [ -f "$CLAUDE_MPM_ROOT/uv.lock" ]; then
|
|
121
|
+
if command -v uv &> /dev/null; then
|
|
122
|
+
echo "uv run python"
|
|
123
|
+
return
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# 2. Check if we're in a pipx installation
|
|
117
128
|
if [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
|
|
118
129
|
# pipx installation - use the pipx venv's Python directly
|
|
119
130
|
if [ -f "$CLAUDE_MPM_ROOT/bin/python" ]; then
|
|
@@ -122,7 +133,7 @@ find_python_command() {
|
|
|
122
133
|
fi
|
|
123
134
|
fi
|
|
124
135
|
|
|
125
|
-
#
|
|
136
|
+
# 3. Check for project-local virtual environment (common in development)
|
|
126
137
|
if [ -f "$CLAUDE_MPM_ROOT/venv/bin/activate" ]; then
|
|
127
138
|
source "$CLAUDE_MPM_ROOT/venv/bin/activate"
|
|
128
139
|
echo "$CLAUDE_MPM_ROOT/venv/bin/python"
|
|
@@ -173,15 +184,44 @@ fi
|
|
|
173
184
|
# Set Socket.IO configuration for hook events
|
|
174
185
|
export CLAUDE_MPM_SOCKETIO_PORT="${CLAUDE_MPM_SOCKETIO_PORT:-8765}"
|
|
175
186
|
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
if ! exec "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log; then
|
|
179
|
-
# If the Python handler fails, always return continue to not block Claude
|
|
187
|
+
# Function for debug logging
|
|
188
|
+
log_debug() {
|
|
180
189
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
181
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)]
|
|
182
|
-
|
|
190
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] $1" >> /tmp/claude-mpm-hook.log
|
|
191
|
+
fi
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Test Python works and module exists
|
|
195
|
+
# Handle UV's multi-word command specially
|
|
196
|
+
if [[ "$PYTHON_CMD" == "uv run python" ]]; then
|
|
197
|
+
if ! uv run python -c "import claude_mpm" 2>/dev/null; then
|
|
198
|
+
log_debug "claude_mpm module not available, continuing without hook"
|
|
199
|
+
echo '{"action": "continue"}'
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
else
|
|
203
|
+
if ! $PYTHON_CMD -c "import claude_mpm" 2>/dev/null; then
|
|
204
|
+
log_debug "claude_mpm module not available, continuing without hook"
|
|
205
|
+
echo '{"action": "continue"}'
|
|
206
|
+
exit 0
|
|
183
207
|
fi
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# Run the Python hook handler with all input
|
|
211
|
+
# Use exec to replace the shell process with Python
|
|
212
|
+
# Handle UV's multi-word command specially
|
|
213
|
+
if [[ "$PYTHON_CMD" == "uv run python" ]]; then
|
|
214
|
+
exec uv run python -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
215
|
+
else
|
|
216
|
+
exec "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Note: exec replaces the shell process, so code below only runs if exec fails
|
|
220
|
+
# If we reach here, the Python handler failed
|
|
221
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
222
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/claude-mpm-hook-error.log" >> /tmp/claude-mpm-hook.log
|
|
223
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Error: $(cat /tmp/claude-mpm-hook-error.log 2>/dev/null | head -5)" >> /tmp/claude-mpm-hook.log
|
|
224
|
+
fi
|
|
225
|
+
# Return continue action to prevent blocking Claude Code
|
|
226
|
+
echo '{"action": "continue"}'
|
|
227
|
+
exit 0
|
|
File without changes
|
|
@@ -9,8 +9,8 @@ to specific engineer agents, plus always includes core agents.
|
|
|
9
9
|
|
|
10
10
|
Architecture:
|
|
11
11
|
- Toolchain-based recommendations: Python → python-engineer, etc.
|
|
12
|
-
- Core agents (always recommended): qa-agent,
|
|
13
|
-
|
|
12
|
+
- Core agents (always recommended): engineer, qa-agent, memory-manager-agent, local-ops-agent,
|
|
13
|
+
research-agent, documentation-agent, security-agent
|
|
14
14
|
- Confidence-based filtering: Only recommend high-confidence detections
|
|
15
15
|
"""
|
|
16
16
|
|
|
@@ -33,14 +33,13 @@ class AgentRecommendationService:
|
|
|
33
33
|
# Core agents always included - matches ToolchainDetector.CORE_AGENTS
|
|
34
34
|
# Uses exact agent IDs from repository for consistency
|
|
35
35
|
CORE_AGENTS = {
|
|
36
|
+
"engineer",
|
|
36
37
|
"qa-agent",
|
|
38
|
+
"memory-manager-agent",
|
|
39
|
+
"local-ops-agent",
|
|
37
40
|
"research-agent",
|
|
38
41
|
"documentation-agent",
|
|
39
|
-
"
|
|
40
|
-
"local-ops-agent",
|
|
41
|
-
# Keep version-control and security as universal recommended agents
|
|
42
|
-
"version-control",
|
|
43
|
-
"security",
|
|
42
|
+
"security-agent",
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
# Map detected languages to recommended engineer agents
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Agent review service for comparing project agents with managed agents.
|
|
2
|
+
|
|
3
|
+
WHY: This service helps users maintain a clean agent directory by:
|
|
4
|
+
1. Identifying which agents are managed vs custom
|
|
5
|
+
2. Detecting outdated versions of managed agents
|
|
6
|
+
3. Finding unused agents that don't match the detected toolchain
|
|
7
|
+
4. Safely archiving unnecessary agents instead of deleting them
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Archive to .claude/agents/unused/ instead of deleting (safe, recoverable)
|
|
11
|
+
- Add timestamps to archived files to prevent conflicts
|
|
12
|
+
- Preserve custom user agents (not in managed set)
|
|
13
|
+
- Compare versions to detect outdated managed agents
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Set
|
|
20
|
+
|
|
21
|
+
from claude_mpm.core.logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentReviewService:
|
|
27
|
+
"""Service for reviewing and managing project agents.
|
|
28
|
+
|
|
29
|
+
This service analyzes the relationship between project agents and managed
|
|
30
|
+
agents from the claude-mpm-agents repository, categorizing them as:
|
|
31
|
+
- Managed: In sync with managed agents
|
|
32
|
+
- Outdated: Older version of managed agent exists
|
|
33
|
+
- Custom: User-created agents not in managed set
|
|
34
|
+
- Unused: Not recommended for this project's toolchain
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the agent review service."""
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
def review_project_agents(
|
|
42
|
+
self,
|
|
43
|
+
project_agents_dir: Path,
|
|
44
|
+
managed_agents: List[Dict[str, Any]],
|
|
45
|
+
recommended_agent_ids: Set[str],
|
|
46
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
47
|
+
"""Review existing project agents and categorize them.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project_agents_dir: Directory containing project agents (.claude/agents/)
|
|
51
|
+
managed_agents: List of managed agent dicts from cache
|
|
52
|
+
recommended_agent_ids: Set of agent IDs recommended for this toolchain
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary with categorized agents:
|
|
56
|
+
{
|
|
57
|
+
"managed": [...], # In sync with managed
|
|
58
|
+
"outdated": [...], # Older version exists
|
|
59
|
+
"custom": [...], # User-created
|
|
60
|
+
"unused": [...], # Not needed for this toolchain
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
results = {
|
|
64
|
+
"managed": [],
|
|
65
|
+
"outdated": [],
|
|
66
|
+
"custom": [],
|
|
67
|
+
"unused": [],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if not project_agents_dir.exists():
|
|
71
|
+
self.logger.debug(
|
|
72
|
+
f"Project agents directory does not exist: {project_agents_dir}"
|
|
73
|
+
)
|
|
74
|
+
return results
|
|
75
|
+
|
|
76
|
+
# Build lookup map of managed agents by ID
|
|
77
|
+
managed_by_id = {agent["agent_id"]: agent for agent in managed_agents}
|
|
78
|
+
|
|
79
|
+
# Scan project agents
|
|
80
|
+
for agent_file in project_agents_dir.glob("*.md"):
|
|
81
|
+
# Skip the unused directory itself
|
|
82
|
+
if agent_file.name == "unused":
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
agent_name = agent_file.stem
|
|
86
|
+
|
|
87
|
+
# Parse agent to get version and metadata
|
|
88
|
+
project_agent_info = self._parse_project_agent(agent_file)
|
|
89
|
+
|
|
90
|
+
# Check if this is a managed agent
|
|
91
|
+
if agent_name in managed_by_id:
|
|
92
|
+
managed_agent = managed_by_id[agent_name]
|
|
93
|
+
|
|
94
|
+
# Compare versions
|
|
95
|
+
project_version = project_agent_info.get("version", "unknown")
|
|
96
|
+
managed_version = managed_agent.get("version", "unknown")
|
|
97
|
+
|
|
98
|
+
if self._is_outdated(project_version, managed_version):
|
|
99
|
+
# Outdated version of managed agent
|
|
100
|
+
results["outdated"].append(
|
|
101
|
+
{
|
|
102
|
+
"name": agent_name,
|
|
103
|
+
"path": agent_file,
|
|
104
|
+
"current_version": project_version,
|
|
105
|
+
"available_version": managed_version,
|
|
106
|
+
"recommended": agent_name in recommended_agent_ids,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# Up-to-date managed agent
|
|
111
|
+
results["managed"].append(
|
|
112
|
+
{
|
|
113
|
+
"name": agent_name,
|
|
114
|
+
"path": agent_file,
|
|
115
|
+
"version": project_version,
|
|
116
|
+
"recommended": agent_name in recommended_agent_ids,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
# Custom user agent (not in managed set)
|
|
121
|
+
results["custom"].append(
|
|
122
|
+
{
|
|
123
|
+
"name": agent_name,
|
|
124
|
+
"path": agent_file,
|
|
125
|
+
"version": project_agent_info.get("version", "unknown"),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Identify unused agents (managed or outdated but not recommended)
|
|
130
|
+
for category in ["managed", "outdated"]:
|
|
131
|
+
for agent in results[category][:]: # Copy list to modify during iteration
|
|
132
|
+
if not agent.get("recommended", False):
|
|
133
|
+
# This managed/outdated agent is not recommended for this toolchain
|
|
134
|
+
results["unused"].append(agent)
|
|
135
|
+
results[category].remove(agent)
|
|
136
|
+
|
|
137
|
+
self.logger.info(
|
|
138
|
+
f"Agent review complete: "
|
|
139
|
+
f"{len(results['managed'])} managed, "
|
|
140
|
+
f"{len(results['outdated'])} outdated, "
|
|
141
|
+
f"{len(results['custom'])} custom, "
|
|
142
|
+
f"{len(results['unused'])} unused"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return results
|
|
146
|
+
|
|
147
|
+
def archive_agents(
|
|
148
|
+
self, agents_to_archive: List[Dict[str, Any]], project_agents_dir: Path
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
"""Archive agents by moving them to .claude/agents/unused/.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
agents_to_archive: List of agent dicts with 'name' and 'path' keys
|
|
154
|
+
project_agents_dir: Base agents directory (.claude/agents/)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dictionary with archival results:
|
|
158
|
+
{
|
|
159
|
+
"archived": [...], # Successfully archived
|
|
160
|
+
"errors": [...], # Archival errors
|
|
161
|
+
}
|
|
162
|
+
"""
|
|
163
|
+
results = {"archived": [], "errors": []}
|
|
164
|
+
|
|
165
|
+
if not agents_to_archive:
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
# Create unused directory
|
|
169
|
+
unused_dir = project_agents_dir / "unused"
|
|
170
|
+
unused_dir.mkdir(exist_ok=True)
|
|
171
|
+
|
|
172
|
+
for agent in agents_to_archive:
|
|
173
|
+
agent_path = agent["path"]
|
|
174
|
+
agent_name = agent["name"]
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Generate timestamped filename to avoid conflicts
|
|
178
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
179
|
+
archived_name = f"{agent_name}_{timestamp}.md"
|
|
180
|
+
archived_path = unused_dir / archived_name
|
|
181
|
+
|
|
182
|
+
# Move the file
|
|
183
|
+
shutil.move(str(agent_path), str(archived_path))
|
|
184
|
+
|
|
185
|
+
results["archived"].append(
|
|
186
|
+
{
|
|
187
|
+
"name": agent_name,
|
|
188
|
+
"original_path": str(agent_path),
|
|
189
|
+
"archived_path": str(archived_path),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.logger.debug(f"Archived {agent_name} to {archived_path}")
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
error_msg = f"Failed to archive {agent_name}: {e}"
|
|
197
|
+
self.logger.error(error_msg)
|
|
198
|
+
results["errors"].append(error_msg)
|
|
199
|
+
|
|
200
|
+
self.logger.info(
|
|
201
|
+
f"Archived {len(results['archived'])} agents, "
|
|
202
|
+
f"{len(results['errors'])} errors"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
def _parse_project_agent(self, agent_file: Path) -> Dict[str, Any]:
|
|
208
|
+
"""Parse a project agent file to extract metadata.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
agent_file: Path to agent Markdown file
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary with agent metadata (version, name, etc.)
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
content = agent_file.read_text(encoding="utf-8")
|
|
218
|
+
|
|
219
|
+
# Extract version from YAML frontmatter
|
|
220
|
+
import re
|
|
221
|
+
|
|
222
|
+
version_match = re.search(
|
|
223
|
+
r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE
|
|
224
|
+
)
|
|
225
|
+
version = version_match.group(1) if version_match else "unknown"
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"version": version,
|
|
229
|
+
"name": agent_file.stem,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.logger.warning(f"Failed to parse agent {agent_file.name}: {e}")
|
|
234
|
+
return {"version": "unknown", "name": agent_file.stem}
|
|
235
|
+
|
|
236
|
+
def _is_outdated(self, current_version: str, available_version: str) -> bool:
|
|
237
|
+
"""Check if current version is outdated compared to available version.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
current_version: Currently deployed version
|
|
241
|
+
available_version: Available version from managed agents
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if current version is outdated
|
|
245
|
+
"""
|
|
246
|
+
# Handle unknown versions
|
|
247
|
+
if current_version == "unknown" or available_version == "unknown":
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
# Simple string comparison for now
|
|
251
|
+
# TODO: Implement semantic version comparison (1.2.3 vs 1.2.4)
|
|
252
|
+
return current_version != available_version
|
|
253
|
+
|
|
254
|
+
def get_archive_summary(self, project_agents_dir: Path) -> Dict[str, Any]:
|
|
255
|
+
"""Get summary of archived agents.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
project_agents_dir: Base agents directory (.claude/agents/)
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Dictionary with archive statistics
|
|
262
|
+
"""
|
|
263
|
+
unused_dir = project_agents_dir / "unused"
|
|
264
|
+
|
|
265
|
+
if not unused_dir.exists():
|
|
266
|
+
return {"count": 0, "agents": []}
|
|
267
|
+
|
|
268
|
+
archived_files = list(unused_dir.glob("*.md"))
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"count": len(archived_files),
|
|
272
|
+
"agents": [
|
|
273
|
+
{
|
|
274
|
+
"name": f.stem,
|
|
275
|
+
"path": str(f),
|
|
276
|
+
"size_bytes": f.stat().st_size,
|
|
277
|
+
}
|
|
278
|
+
for f in archived_files
|
|
279
|
+
],
|
|
280
|
+
}
|
|
@@ -215,9 +215,8 @@ class AgentDiscoveryService:
|
|
|
215
215
|
# Extract YAML frontmatter
|
|
216
216
|
frontmatter = self._extract_yaml_frontmatter(template_content)
|
|
217
217
|
if not frontmatter:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
218
|
+
# Silently return None for files without frontmatter
|
|
219
|
+
# (e.g., PM instruction templates in templates/ directory)
|
|
221
220
|
return None
|
|
222
221
|
|
|
223
222
|
# Extract metadata directly from frontmatter (flat structure)
|
|
@@ -16,6 +16,8 @@ import os
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Dict, List, Optional, Tuple
|
|
18
18
|
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
19
21
|
from claude_mpm.core.config import Config
|
|
20
22
|
from claude_mpm.core.logging_config import get_logger
|
|
21
23
|
|
|
@@ -51,6 +53,70 @@ class MultiSourceAgentDeploymentService:
|
|
|
51
53
|
self.logger = get_logger(__name__)
|
|
52
54
|
self.version_manager = AgentVersionManager()
|
|
53
55
|
|
|
56
|
+
def _read_template_version(self, template_path: Path) -> Optional[str]:
|
|
57
|
+
"""Read version from template file (supports both .md and .json formats).
|
|
58
|
+
|
|
59
|
+
For .md files: Extract version from YAML frontmatter
|
|
60
|
+
For .json files: Extract version from JSON structure
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
template_path: Path to template file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Version string or None if version cannot be extracted
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
if template_path.suffix == ".md":
|
|
70
|
+
# Parse markdown with YAML frontmatter
|
|
71
|
+
content = template_path.read_text()
|
|
72
|
+
|
|
73
|
+
# Extract YAML frontmatter (between --- markers)
|
|
74
|
+
if not content.strip().startswith("---"):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
parts = content.split("---", 2)
|
|
78
|
+
if len(parts) < 3:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Parse YAML frontmatter
|
|
82
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
83
|
+
if not frontmatter:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Extract version from frontmatter
|
|
87
|
+
version = frontmatter.get("version")
|
|
88
|
+
return version if version else None
|
|
89
|
+
|
|
90
|
+
if template_path.suffix == ".json":
|
|
91
|
+
# Parse JSON template
|
|
92
|
+
template_data = json.loads(template_path.read_text())
|
|
93
|
+
metadata = template_data.get("metadata", {})
|
|
94
|
+
version = (
|
|
95
|
+
template_data.get("agent_version")
|
|
96
|
+
or template_data.get("version")
|
|
97
|
+
or metadata.get("version")
|
|
98
|
+
)
|
|
99
|
+
return version if version else None
|
|
100
|
+
|
|
101
|
+
self.logger.warning(
|
|
102
|
+
f"Unknown template format: {template_path.suffix} for {template_path.name}"
|
|
103
|
+
)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
except yaml.YAMLError as e:
|
|
107
|
+
self.logger.warning(
|
|
108
|
+
f"Invalid YAML frontmatter in {template_path.name}: {e}"
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
except json.JSONDecodeError as e:
|
|
112
|
+
self.logger.warning(f"Invalid JSON in {template_path.name}: {e}")
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.warning(
|
|
116
|
+
f"Error reading template version from {template_path.name}: {e}"
|
|
117
|
+
)
|
|
118
|
+
return None
|
|
119
|
+
|
|
54
120
|
def _build_canonical_id_for_agent(self, agent_info: Dict[str, Any]) -> str:
|
|
55
121
|
"""Build or retrieve canonical_id for an agent.
|
|
56
122
|
|
|
@@ -827,17 +893,20 @@ class MultiSourceAgentDeploymentService:
|
|
|
827
893
|
comparison_results["needs_update"].append(agent_name)
|
|
828
894
|
continue
|
|
829
895
|
|
|
830
|
-
# Read template version
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
template_data.get("agent_version")
|
|
836
|
-
or template_data.get("version")
|
|
837
|
-
or metadata.get("version", "0.0.0")
|
|
896
|
+
# Read template version using format-aware helper
|
|
897
|
+
version_string = self._read_template_version(template_path)
|
|
898
|
+
if not version_string:
|
|
899
|
+
self.logger.warning(
|
|
900
|
+
f"Could not extract version from template for '{agent_name}', skipping"
|
|
838
901
|
)
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
try:
|
|
905
|
+
template_version = self.version_manager.parse_version(version_string)
|
|
839
906
|
except Exception as e:
|
|
840
|
-
self.logger.warning(
|
|
907
|
+
self.logger.warning(
|
|
908
|
+
f"Error parsing version '{version_string}' for '{agent_name}': {e}"
|
|
909
|
+
)
|
|
841
910
|
continue
|
|
842
911
|
|
|
843
912
|
# Read deployed version
|
|
@@ -464,9 +464,22 @@ class RemoteAgentDiscoveryService:
|
|
|
464
464
|
"AUTO-DEPLOY-INDEX.md",
|
|
465
465
|
"PHASE1_COMPLETE.md",
|
|
466
466
|
"AGENTS.md",
|
|
467
|
+
# Skill-related files (should not be treated as agents)
|
|
468
|
+
"SKILL.md",
|
|
469
|
+
"SKILLS.md",
|
|
470
|
+
"skill-template.md",
|
|
467
471
|
}
|
|
468
472
|
md_files = [f for f in md_files if f.name not in excluded_files]
|
|
469
473
|
|
|
474
|
+
# Filter out files from skills-related directories
|
|
475
|
+
# Skills are not agents and should not be discovered here
|
|
476
|
+
excluded_directory_patterns = {"references", "examples", "claude-mpm-skills"}
|
|
477
|
+
md_files = [
|
|
478
|
+
f
|
|
479
|
+
for f in md_files
|
|
480
|
+
if not any(excluded in f.parts for excluded in excluded_directory_patterns)
|
|
481
|
+
]
|
|
482
|
+
|
|
470
483
|
# In flattened cache mode, also exclude files from git repository subdirectories
|
|
471
484
|
# (files under directories that contain .git folder)
|
|
472
485
|
if scan_dir == self.remote_agents_dir:
|
|
@@ -351,6 +351,12 @@ class GitSourceManager:
|
|
|
351
351
|
"claude-mpm",
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
# Repositories that are NOT agent repositories (should be excluded from agent discovery)
|
|
355
|
+
# These contain skills, documentation, or other non-agent content
|
|
356
|
+
EXCLUDED_REPOSITORIES = {
|
|
357
|
+
"claude-mpm-skills", # Skills repository, not agents
|
|
358
|
+
}
|
|
359
|
+
|
|
354
360
|
for owner_dir in self.cache_root.iterdir():
|
|
355
361
|
if not owner_dir.is_dir():
|
|
356
362
|
continue
|
|
@@ -367,6 +373,14 @@ class GitSourceManager:
|
|
|
367
373
|
for repo_dir in owner_dir.iterdir():
|
|
368
374
|
if not repo_dir.is_dir():
|
|
369
375
|
continue
|
|
376
|
+
|
|
377
|
+
# Skip excluded repositories (e.g., skills repos are not agent repos)
|
|
378
|
+
if repo_dir.name in EXCLUDED_REPOSITORIES:
|
|
379
|
+
logger.debug(
|
|
380
|
+
f"[DEBUG] Skipping excluded repository: {repo_dir.name}"
|
|
381
|
+
)
|
|
382
|
+
continue
|
|
383
|
+
|
|
370
384
|
logger.debug(f"[DEBUG] Processing repo_dir: {repo_dir.name}")
|
|
371
385
|
|
|
372
386
|
# Bug #5 fix: Don't iterate subdirectories - RemoteAgentDiscoveryService
|
|
@@ -13,9 +13,6 @@ from enum import Enum
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any, Dict, List, Optional
|
|
15
15
|
|
|
16
|
-
# Lazy import for base_agent_loader to reduce initialization overhead
|
|
17
|
-
# base_agent_loader adds ~500ms to import time
|
|
18
|
-
# from claude_mpm.agents.base_agent_loader import clear_base_agent_cache
|
|
19
16
|
from claude_mpm.core.logging_utils import get_logger
|
|
20
17
|
from claude_mpm.services.memory.cache.shared_prompt_cache import SharedPromptCache
|
|
21
18
|
from claude_mpm.services.shared import ConfigServiceBase
|
|
@@ -23,13 +20,6 @@ from claude_mpm.services.shared import ConfigServiceBase
|
|
|
23
20
|
logger = get_logger(__name__)
|
|
24
21
|
|
|
25
22
|
|
|
26
|
-
def _get_clear_base_agent_cache():
|
|
27
|
-
"""Lazy loader for clear_base_agent_cache function."""
|
|
28
|
-
from claude_mpm.agents.base_agent_loader import clear_base_agent_cache
|
|
29
|
-
|
|
30
|
-
return clear_base_agent_cache
|
|
31
|
-
|
|
32
|
-
|
|
33
23
|
class BaseAgentSection(str, Enum):
|
|
34
24
|
"""Base agent markdown sections."""
|
|
35
25
|
|
|
@@ -143,9 +133,7 @@ class BaseAgentManager(ConfigServiceBase):
|
|
|
143
133
|
content = self._structure_to_markdown(current)
|
|
144
134
|
self.base_agent_path.write_text(content, encoding="utf-8")
|
|
145
135
|
|
|
146
|
-
# Clear
|
|
147
|
-
clear_base_agent_cache = _get_clear_base_agent_cache()
|
|
148
|
-
clear_base_agent_cache()
|
|
136
|
+
# Clear cache
|
|
149
137
|
self.cache.invalidate("base_agent:instructions")
|
|
150
138
|
|
|
151
139
|
logger.info("Base agent updated successfully")
|