claude-mpm 5.4.21__py3-none-any.whl → 5.4.59__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/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/BASE_ENGINEER.md +658 -0
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +771 -1019
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +1 -1
- claude_mpm/agents/base_agent.json +31 -0
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/cli/commands/agent_state_manager.py +10 -10
- claude_mpm/cli/commands/agents.py +9 -9
- claude_mpm/cli/commands/auto_configure.py +4 -4
- claude_mpm/cli/commands/configure.py +1 -1
- claude_mpm/cli/commands/configure_agent_display.py +12 -0
- claude_mpm/cli/commands/mpm_init/core.py +72 -0
- claude_mpm/cli/commands/postmortem.py +1 -1
- claude_mpm/cli/commands/profile.py +276 -0
- claude_mpm/cli/commands/skills.py +14 -18
- claude_mpm/cli/executor.py +10 -0
- claude_mpm/cli/interactive/agent_wizard.py +2 -2
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/profile_parser.py +147 -0
- claude_mpm/cli/parsers/skills_parser.py +0 -6
- claude_mpm/cli/startup.py +506 -180
- claude_mpm/commands/mpm-config.md +13 -250
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -206
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +61 -441
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/loaders/agent_loader.py +1 -1
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/optimized_startup.py +61 -0
- claude_mpm/core/shared/config_loader.py +3 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Vzk33B_K.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.m1gL8KXf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
- claude_mpm/hooks/kuzu_memory_hook.py +5 -5
- claude_mpm/init.py +276 -0
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
- claude_mpm/services/agents/deployment/agent_template_builder.py +31 -19
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
- claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
- claude_mpm/services/agents/git_source_manager.py +23 -4
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +121 -10
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/monitor/management/lifecycle.py +7 -1
- claude_mpm/services/monitor/server.py +473 -3
- claude_mpm/services/pm_skills_deployer.py +711 -0
- claude_mpm/services/profile_manager.py +337 -0
- claude_mpm/services/skills/git_skill_source_manager.py +148 -11
- claude_mpm/services/skills/selective_skill_deployer.py +97 -48
- claude_mpm/services/skills_deployer.py +161 -65
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +37 -6
- claude_mpm/services/socketio/server/core.py +262 -123
- claude_mpm/skills/bundled/security-scanning.md +112 -0
- claude_mpm/skills/skill_manager.py +98 -3
- claude_mpm/templates/.pre-commit-config.yaml +112 -0
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +1 -1
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +7 -4
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +175 -81
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/top_level.txt +0 -0
|
@@ -42,7 +42,7 @@ class RemoteAgentMetadata:
|
|
|
42
42
|
class RemoteAgentDiscoveryService:
|
|
43
43
|
"""Discovers and converts remote Markdown agents to JSON format.
|
|
44
44
|
|
|
45
|
-
Remote agents are discovered from the cache directory (~/.claude-mpm/cache/
|
|
45
|
+
Remote agents are discovered from the cache directory (~/.claude-mpm/cache/agents/)
|
|
46
46
|
where they are stored as Markdown files. This service:
|
|
47
47
|
1. Discovers all *.md files in the remote agents cache
|
|
48
48
|
2. Parses Markdown frontmatter and content to extract metadata
|
|
@@ -61,20 +61,20 @@ class RemoteAgentDiscoveryService:
|
|
|
61
61
|
- Flexibility: Supports optional sections with defaults
|
|
62
62
|
"""
|
|
63
63
|
|
|
64
|
-
def __init__(self,
|
|
64
|
+
def __init__(self, agents_cache_dir: Path):
|
|
65
65
|
"""Initialize the remote agent discovery service.
|
|
66
66
|
|
|
67
67
|
Args:
|
|
68
|
-
|
|
68
|
+
agents_cache_dir: Directory containing cached agent Markdown files
|
|
69
69
|
"""
|
|
70
|
-
self.
|
|
70
|
+
self.agents_cache_dir = agents_cache_dir
|
|
71
71
|
self.logger = get_logger(__name__)
|
|
72
72
|
|
|
73
73
|
def _extract_collection_id_from_path(self, file_path: Path) -> Optional[str]:
|
|
74
74
|
"""Extract collection_id from repository path structure.
|
|
75
75
|
|
|
76
76
|
Collection ID is derived from the repository path structure:
|
|
77
|
-
~/.claude-mpm/cache/
|
|
77
|
+
~/.claude-mpm/cache/agents/{owner}/{repo}/agents/...
|
|
78
78
|
|
|
79
79
|
Args:
|
|
80
80
|
file_path: Absolute path to agent Markdown file
|
|
@@ -83,28 +83,29 @@ class RemoteAgentDiscoveryService:
|
|
|
83
83
|
Collection ID in format "owner/repo-name" or None if not found
|
|
84
84
|
|
|
85
85
|
Example:
|
|
86
|
-
Input: ~/.claude-mpm/cache/
|
|
86
|
+
Input: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
|
|
87
87
|
Output: "bobmatnyc/claude-mpm-agents"
|
|
88
88
|
"""
|
|
89
89
|
try:
|
|
90
|
-
# Find "
|
|
90
|
+
# Find "agents" cache directory in the path (looking for .claude-mpm/cache/agents)
|
|
91
91
|
path_parts = file_path.parts
|
|
92
|
-
|
|
92
|
+
agents_cache_idx = -1
|
|
93
93
|
|
|
94
94
|
for i, part in enumerate(path_parts):
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
# Look for cache/agents pattern
|
|
96
|
+
if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
|
|
97
|
+
agents_cache_idx = i
|
|
97
98
|
break
|
|
98
99
|
|
|
99
|
-
if
|
|
100
|
+
if agents_cache_idx == -1 or agents_cache_idx + 2 >= len(path_parts):
|
|
100
101
|
self.logger.debug(
|
|
101
102
|
f"Could not extract collection_id from path: {file_path}"
|
|
102
103
|
)
|
|
103
104
|
return None
|
|
104
105
|
|
|
105
|
-
# Extract owner and repo (next two parts after "
|
|
106
|
-
owner = path_parts[
|
|
107
|
-
repo = path_parts[
|
|
106
|
+
# Extract owner and repo (next two parts after "cache/agents")
|
|
107
|
+
owner = path_parts[agents_cache_idx + 1]
|
|
108
|
+
repo = path_parts[agents_cache_idx + 2]
|
|
108
109
|
|
|
109
110
|
collection_id = f"{owner}/{repo}"
|
|
110
111
|
self.logger.debug(f"Extracted collection_id: {collection_id}")
|
|
@@ -128,25 +129,26 @@ class RemoteAgentDiscoveryService:
|
|
|
128
129
|
Relative path from repo root, or None if not found
|
|
129
130
|
|
|
130
131
|
Example:
|
|
131
|
-
Input: ~/.claude-mpm/cache/
|
|
132
|
+
Input: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
|
|
132
133
|
Output: "agents/pm.md"
|
|
133
134
|
"""
|
|
134
135
|
try:
|
|
135
|
-
# Find "
|
|
136
|
+
# Find "agents" cache directory in the path
|
|
136
137
|
path_parts = file_path.parts
|
|
137
|
-
|
|
138
|
+
agents_cache_idx = -1
|
|
138
139
|
|
|
139
140
|
for i, part in enumerate(path_parts):
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
# Look for cache/agents pattern
|
|
142
|
+
if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
|
|
143
|
+
agents_cache_idx = i
|
|
142
144
|
break
|
|
143
145
|
|
|
144
|
-
if
|
|
146
|
+
if agents_cache_idx == -1 or agents_cache_idx + 3 >= len(path_parts):
|
|
145
147
|
return None
|
|
146
148
|
|
|
147
149
|
# Path after owner/repo is the source path
|
|
148
|
-
#
|
|
149
|
-
repo_root_idx =
|
|
150
|
+
# cache/agents/{owner}/{repo}/{source_path}
|
|
151
|
+
repo_root_idx = agents_cache_idx + 3
|
|
150
152
|
source_parts = path_parts[repo_root_idx:]
|
|
151
153
|
|
|
152
154
|
return "/".join(source_parts)
|
|
@@ -273,7 +275,7 @@ class RemoteAgentDiscoveryService:
|
|
|
273
275
|
|
|
274
276
|
Supports both cache structures:
|
|
275
277
|
1. Git repo: Calculate relative to /agents/ subdirectory
|
|
276
|
-
2. Flattened cache: Calculate relative to
|
|
278
|
+
2. Flattened cache: Calculate relative to agents_cache_dir directly
|
|
277
279
|
|
|
278
280
|
Example (Git repo):
|
|
279
281
|
Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
|
|
@@ -281,8 +283,8 @@ class RemoteAgentDiscoveryService:
|
|
|
281
283
|
Output: engineer/backend/python-engineer
|
|
282
284
|
|
|
283
285
|
Example (Flattened cache):
|
|
284
|
-
Input: /cache/
|
|
285
|
-
Root: /cache/
|
|
286
|
+
Input: /cache/agents/engineer/python-engineer.md
|
|
287
|
+
Root: /cache/agents
|
|
286
288
|
Output: engineer/python-engineer
|
|
287
289
|
|
|
288
290
|
Args:
|
|
@@ -293,7 +295,7 @@ class RemoteAgentDiscoveryService:
|
|
|
293
295
|
"""
|
|
294
296
|
try:
|
|
295
297
|
# Try git repo structure first: /agents/ subdirectory
|
|
296
|
-
agents_dir = self.
|
|
298
|
+
agents_dir = self.agents_cache_dir / "agents"
|
|
297
299
|
if agents_dir.exists():
|
|
298
300
|
try:
|
|
299
301
|
relative_path = file_path.relative_to(agents_dir)
|
|
@@ -301,12 +303,12 @@ class RemoteAgentDiscoveryService:
|
|
|
301
303
|
except ValueError:
|
|
302
304
|
pass # Not under agents_dir, try flattened structure
|
|
303
305
|
|
|
304
|
-
# Try flattened cache structure: calculate relative to
|
|
306
|
+
# Try flattened cache structure: calculate relative to agents_cache_dir
|
|
305
307
|
try:
|
|
306
|
-
relative_path = file_path.relative_to(self.
|
|
308
|
+
relative_path = file_path.relative_to(self.agents_cache_dir)
|
|
307
309
|
return str(relative_path.with_suffix("")).replace("\\", "/")
|
|
308
310
|
except ValueError:
|
|
309
|
-
pass # Not under
|
|
311
|
+
pass # Not under agents_cache_dir either
|
|
310
312
|
|
|
311
313
|
# Fall back to filename
|
|
312
314
|
self.logger.warning(
|
|
@@ -327,7 +329,7 @@ class RemoteAgentDiscoveryService:
|
|
|
327
329
|
|
|
328
330
|
Supports both cache structures:
|
|
329
331
|
1. Git repo: Calculate relative to /agents/ subdirectory
|
|
330
|
-
2. Flattened cache: Calculate relative to
|
|
332
|
+
2. Flattened cache: Calculate relative to agents_cache_dir directly
|
|
331
333
|
|
|
332
334
|
Example (Git repo):
|
|
333
335
|
Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
|
|
@@ -335,8 +337,8 @@ class RemoteAgentDiscoveryService:
|
|
|
335
337
|
Output: engineer/backend
|
|
336
338
|
|
|
337
339
|
Example (Flattened cache):
|
|
338
|
-
Input: /cache/
|
|
339
|
-
Root: /cache/
|
|
340
|
+
Input: /cache/agents/engineer/python-engineer.md
|
|
341
|
+
Root: /cache/agents
|
|
340
342
|
Output: engineer
|
|
341
343
|
|
|
342
344
|
Args:
|
|
@@ -347,7 +349,7 @@ class RemoteAgentDiscoveryService:
|
|
|
347
349
|
"""
|
|
348
350
|
try:
|
|
349
351
|
# Try git repo structure first: /agents/ subdirectory
|
|
350
|
-
agents_dir = self.
|
|
352
|
+
agents_dir = self.agents_cache_dir / "agents"
|
|
351
353
|
if agents_dir.exists():
|
|
352
354
|
try:
|
|
353
355
|
relative_path = file_path.relative_to(agents_dir)
|
|
@@ -356,13 +358,13 @@ class RemoteAgentDiscoveryService:
|
|
|
356
358
|
except ValueError:
|
|
357
359
|
pass # Not under agents_dir, try flattened structure
|
|
358
360
|
|
|
359
|
-
# Try flattened cache structure: calculate relative to
|
|
361
|
+
# Try flattened cache structure: calculate relative to agents_cache_dir
|
|
360
362
|
try:
|
|
361
|
-
relative_path = file_path.relative_to(self.
|
|
363
|
+
relative_path = file_path.relative_to(self.agents_cache_dir)
|
|
362
364
|
parts = relative_path.parts[:-1] # Exclude filename
|
|
363
365
|
return "/".join(parts) if parts else "universal"
|
|
364
366
|
except ValueError:
|
|
365
|
-
pass # Not under
|
|
367
|
+
pass # Not under agents_cache_dir either
|
|
366
368
|
|
|
367
369
|
return "universal"
|
|
368
370
|
except Exception:
|
|
@@ -385,7 +387,7 @@ class RemoteAgentDiscoveryService:
|
|
|
385
387
|
List of agent dictionaries in JSON template format
|
|
386
388
|
|
|
387
389
|
Example:
|
|
388
|
-
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/
|
|
390
|
+
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
|
|
389
391
|
>>> agents = service.discover_remote_agents()
|
|
390
392
|
>>> len(agents)
|
|
391
393
|
5
|
|
@@ -394,20 +396,21 @@ class RemoteAgentDiscoveryService:
|
|
|
394
396
|
"""
|
|
395
397
|
agents = []
|
|
396
398
|
|
|
397
|
-
if not self.
|
|
399
|
+
if not self.agents_cache_dir.exists():
|
|
398
400
|
self.logger.debug(
|
|
399
|
-
f"
|
|
401
|
+
f"Agents cache directory does not exist: {self.agents_cache_dir}"
|
|
400
402
|
)
|
|
401
403
|
return agents
|
|
402
404
|
|
|
403
|
-
# Support
|
|
405
|
+
# Support four cache structures (PRIORITY ORDER):
|
|
404
406
|
# 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
|
|
405
407
|
# 2. Git repo path: {path}/agents/ - source files (fallback)
|
|
406
|
-
# 3.
|
|
408
|
+
# 3. Owner/repo structure: {path}/{owner}/{repo}/agents/ - GitHub sync structure
|
|
409
|
+
# 4. Flattened cache: {path}/ - directly contains category directories (legacy)
|
|
407
410
|
|
|
408
411
|
# Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
|
|
409
|
-
dist_agents_dir = self.
|
|
410
|
-
agents_dir = self.
|
|
412
|
+
dist_agents_dir = self.agents_cache_dir / "dist" / "agents"
|
|
413
|
+
agents_dir = self.agents_cache_dir / "agents"
|
|
411
414
|
|
|
412
415
|
if dist_agents_dir.exists():
|
|
413
416
|
# PREFERRED: Use built agents from dist/agents/
|
|
@@ -420,32 +423,52 @@ class RemoteAgentDiscoveryService:
|
|
|
420
423
|
self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
|
|
421
424
|
scan_dir = agents_dir
|
|
422
425
|
else:
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
scan_dir =
|
|
426
|
+
# Priority 3: Check for {owner}/{repo}/agents/ structure (GitHub sync)
|
|
427
|
+
# e.g., ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/
|
|
428
|
+
owner_repo_agents_dir = None
|
|
429
|
+
for owner_dir in self.agents_cache_dir.iterdir():
|
|
430
|
+
if owner_dir.is_dir() and not owner_dir.name.startswith("."):
|
|
431
|
+
for repo_dir in owner_dir.iterdir():
|
|
432
|
+
if repo_dir.is_dir():
|
|
433
|
+
potential_agents = repo_dir / "agents"
|
|
434
|
+
if potential_agents.exists():
|
|
435
|
+
owner_repo_agents_dir = potential_agents
|
|
436
|
+
self.logger.debug(
|
|
437
|
+
f"Using GitHub sync structure: {owner_repo_agents_dir}"
|
|
438
|
+
)
|
|
439
|
+
break
|
|
440
|
+
if owner_repo_agents_dir:
|
|
441
|
+
break
|
|
442
|
+
|
|
443
|
+
if owner_repo_agents_dir:
|
|
444
|
+
scan_dir = owner_repo_agents_dir
|
|
442
445
|
else:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
446
|
+
# LEGACY: Flattened cache structure - scan root directly
|
|
447
|
+
# Check if this looks like the flattened cache (has category subdirectories)
|
|
448
|
+
category_dirs = [
|
|
449
|
+
"universal",
|
|
450
|
+
"engineer",
|
|
451
|
+
"ops",
|
|
452
|
+
"qa",
|
|
453
|
+
"security",
|
|
454
|
+
"documentation",
|
|
455
|
+
]
|
|
456
|
+
has_categories = any(
|
|
457
|
+
(self.agents_cache_dir / cat).exists() for cat in category_dirs
|
|
447
458
|
)
|
|
448
|
-
|
|
459
|
+
|
|
460
|
+
if has_categories:
|
|
461
|
+
self.logger.debug(
|
|
462
|
+
f"Using flattened cache structure: {self.agents_cache_dir}"
|
|
463
|
+
)
|
|
464
|
+
scan_dir = self.agents_cache_dir
|
|
465
|
+
else:
|
|
466
|
+
self.logger.warning(
|
|
467
|
+
f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
|
|
468
|
+
f"owner/repo/agents/ structure, and category directories in {self.agents_cache_dir}. "
|
|
469
|
+
"Expected agents in /dist/agents/, /agents/, owner/repo/agents/, or category directories."
|
|
470
|
+
)
|
|
471
|
+
return agents
|
|
449
472
|
|
|
450
473
|
# Find all Markdown files recursively
|
|
451
474
|
md_files = list(scan_dir.rglob("*.md"))
|
|
@@ -482,16 +505,16 @@ class RemoteAgentDiscoveryService:
|
|
|
482
505
|
|
|
483
506
|
# In flattened cache mode, also exclude files from git repository subdirectories
|
|
484
507
|
# (files under directories that contain .git folder)
|
|
485
|
-
if scan_dir == self.
|
|
508
|
+
if scan_dir == self.agents_cache_dir:
|
|
486
509
|
filtered_files = []
|
|
487
510
|
for f in md_files:
|
|
488
511
|
# Check if this file is inside a git repository (has .git in path)
|
|
489
|
-
# Git repos are at {
|
|
490
|
-
path_parts = f.relative_to(self.
|
|
512
|
+
# Git repos are at {agents_cache_dir}/{owner}/{repo}/.git
|
|
513
|
+
path_parts = f.relative_to(self.agents_cache_dir).parts
|
|
491
514
|
if len(path_parts) >= 2:
|
|
492
515
|
# Check if this looks like a git repo path (owner/repo)
|
|
493
516
|
potential_repo = (
|
|
494
|
-
self.
|
|
517
|
+
self.agents_cache_dir / path_parts[0] / path_parts[1]
|
|
495
518
|
)
|
|
496
519
|
if (potential_repo / ".git").exists():
|
|
497
520
|
# This file is in a git repo, skip it (we'll handle git repos separately)
|
|
@@ -518,7 +541,7 @@ class RemoteAgentDiscoveryService:
|
|
|
518
541
|
self.logger.warning(f"Failed to parse remote agent {md_file.name}: {e}")
|
|
519
542
|
|
|
520
543
|
self.logger.info(
|
|
521
|
-
f"Discovered {len(agents)} remote agents from {self.
|
|
544
|
+
f"Discovered {len(agents)} remote agents from {self.agents_cache_dir.name}"
|
|
522
545
|
)
|
|
523
546
|
return agents
|
|
524
547
|
|
|
@@ -735,7 +758,7 @@ class RemoteAgentDiscoveryService:
|
|
|
735
758
|
RemoteAgentMetadata if found, None otherwise
|
|
736
759
|
"""
|
|
737
760
|
# Bug #4 fix: Search in /agents/ subdirectory, not root directory
|
|
738
|
-
agents_dir = self.
|
|
761
|
+
agents_dir = self.agents_cache_dir / "agents"
|
|
739
762
|
if not agents_dir.exists():
|
|
740
763
|
return None
|
|
741
764
|
|
|
@@ -767,7 +790,7 @@ class RemoteAgentDiscoveryService:
|
|
|
767
790
|
List of agent dictionaries from the specified collection
|
|
768
791
|
|
|
769
792
|
Example:
|
|
770
|
-
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/
|
|
793
|
+
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
|
|
771
794
|
>>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
|
|
772
795
|
>>> len(agents)
|
|
773
796
|
45
|
|
@@ -795,7 +818,7 @@ class RemoteAgentDiscoveryService:
|
|
|
795
818
|
- agents: List of agent IDs in collection
|
|
796
819
|
|
|
797
820
|
Example:
|
|
798
|
-
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/
|
|
821
|
+
>>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
|
|
799
822
|
>>> collections = service.list_collections()
|
|
800
823
|
>>> collections
|
|
801
824
|
[
|
|
@@ -50,10 +50,10 @@ class GitSourceManager:
|
|
|
50
50
|
|
|
51
51
|
Args:
|
|
52
52
|
cache_root: Root directory for repository caches.
|
|
53
|
-
Defaults to ~/.claude-mpm/cache/
|
|
53
|
+
Defaults to ~/.claude-mpm/cache/agents/
|
|
54
54
|
"""
|
|
55
55
|
if cache_root is None:
|
|
56
|
-
cache_root = Path.home() / ".claude-mpm" / "cache" / "
|
|
56
|
+
cache_root = Path.home() / ".claude-mpm" / "cache" / "agents"
|
|
57
57
|
|
|
58
58
|
self.cache_root = cache_root
|
|
59
59
|
self.cache_root.mkdir(parents=True, exist_ok=True)
|
|
@@ -395,8 +395,27 @@ class GitSourceManager:
|
|
|
395
395
|
)
|
|
396
396
|
logger.debug(f"[DEBUG] Found {len(agents)} agents so far")
|
|
397
397
|
|
|
398
|
-
logger.debug(
|
|
399
|
-
|
|
398
|
+
logger.debug(
|
|
399
|
+
f"[DEBUG] list_cached_agents COMPLETE: {len(agents)} total agents (before deduplication)"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Deduplicate agents by agent_id (Bug #2 fix)
|
|
403
|
+
# When same agent exists in multiple locations, keep only first occurrence
|
|
404
|
+
seen_ids = set()
|
|
405
|
+
deduplicated_agents = []
|
|
406
|
+
|
|
407
|
+
for agent in agents:
|
|
408
|
+
agent_id = agent.get("agent_id") or agent.get("metadata", {}).get("name")
|
|
409
|
+
if agent_id and agent_id not in seen_ids:
|
|
410
|
+
seen_ids.add(agent_id)
|
|
411
|
+
deduplicated_agents.append(agent)
|
|
412
|
+
elif agent_id:
|
|
413
|
+
logger.debug(f"[DEBUG] Skipping duplicate agent: {agent_id}")
|
|
414
|
+
|
|
415
|
+
logger.debug(
|
|
416
|
+
f"[DEBUG] After deduplication: {len(deduplicated_agents)} unique agents"
|
|
417
|
+
)
|
|
418
|
+
return deduplicated_agents
|
|
400
419
|
|
|
401
420
|
def _discover_agents_in_directory(
|
|
402
421
|
self,
|
|
@@ -226,9 +226,11 @@ class AgentRecommenderService(BaseService, IAgentRecommender):
|
|
|
226
226
|
if max_agents is not None:
|
|
227
227
|
recommendations = recommendations[:max_agents]
|
|
228
228
|
|
|
229
|
-
# Check if
|
|
230
|
-
if not recommendations
|
|
231
|
-
self.logger.info(
|
|
229
|
+
# Check if we have no recommendations (any reason: unknown language, low scores, etc.)
|
|
230
|
+
if not recommendations:
|
|
231
|
+
self.logger.info(
|
|
232
|
+
f"No agents scored above threshold for {toolchain.primary_language}; using defaults"
|
|
233
|
+
)
|
|
232
234
|
|
|
233
235
|
# Get default configuration
|
|
234
236
|
default_config = self._capabilities_config.get("default_configuration", {})
|
|
@@ -78,14 +78,14 @@ class SingleTierDeploymentService:
|
|
|
78
78
|
config: Agent source configuration with repositories
|
|
79
79
|
deployment_dir: Target deployment directory (.claude/agents/)
|
|
80
80
|
cache_root: Cache root for repositories
|
|
81
|
-
(defaults to ~/.claude-mpm/cache/
|
|
81
|
+
(defaults to ~/.claude-mpm/cache/agents/)
|
|
82
82
|
"""
|
|
83
83
|
self.config = config
|
|
84
84
|
self.deployment_dir = deployment_dir
|
|
85
85
|
self.deployment_dir.mkdir(parents=True, exist_ok=True)
|
|
86
86
|
|
|
87
87
|
if cache_root is None:
|
|
88
|
-
cache_root = Path.home() / ".claude-mpm" / "cache" / "
|
|
88
|
+
cache_root = Path.home() / ".claude-mpm" / "cache" / "agents"
|
|
89
89
|
|
|
90
90
|
self.cache_root = cache_root
|
|
91
91
|
self.git_source_manager = GitSourceManager(cache_root)
|
|
@@ -13,11 +13,16 @@ import logging
|
|
|
13
13
|
import time
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
16
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
17
17
|
|
|
18
18
|
import requests
|
|
19
19
|
|
|
20
20
|
from claude_mpm.core.file_utils import get_file_hash
|
|
21
|
+
|
|
22
|
+
# Import normalize function for exclusion filtering
|
|
23
|
+
from claude_mpm.services.agents.deployment.multi_source_deployment_service import (
|
|
24
|
+
_normalize_agent_name,
|
|
25
|
+
)
|
|
21
26
|
from claude_mpm.services.agents.sources.agent_sync_state import AgentSyncState
|
|
22
27
|
from claude_mpm.utils.progress import create_progress_bar
|
|
23
28
|
|
|
@@ -188,10 +193,10 @@ class GitSourceSyncService:
|
|
|
188
193
|
|
|
189
194
|
Args:
|
|
190
195
|
source_url: Base URL for raw files (without trailing slash)
|
|
191
|
-
cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/
|
|
196
|
+
cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/agents/)
|
|
192
197
|
source_id: Unique identifier for this source (for multi-source support)
|
|
193
198
|
|
|
194
|
-
Design Decision: Cache to ~/.claude-mpm/cache/
|
|
199
|
+
Design Decision: Cache to ~/.claude-mpm/cache/agents/ (canonical location)
|
|
195
200
|
|
|
196
201
|
Rationale: Separates cached repository structure from deployed agents.
|
|
197
202
|
This allows preserving nested directory structure in cache while
|
|
@@ -207,13 +212,13 @@ class GitSourceSyncService:
|
|
|
207
212
|
self.source_url = source_url.rstrip("/")
|
|
208
213
|
self.source_id = source_id
|
|
209
214
|
|
|
210
|
-
# Setup cache directory (canonical: ~/.claude-mpm/cache/
|
|
215
|
+
# Setup cache directory (canonical: ~/.claude-mpm/cache/agents/)
|
|
211
216
|
if cache_dir:
|
|
212
217
|
self.cache_dir = Path(cache_dir)
|
|
213
218
|
else:
|
|
214
|
-
# Default to ~/.claude-mpm/cache/
|
|
219
|
+
# Default to ~/.claude-mpm/cache/agents/ (canonical cache location)
|
|
215
220
|
home = Path.home()
|
|
216
|
-
self.cache_dir = home / ".claude-mpm" / "cache" / "
|
|
221
|
+
self.cache_dir = home / ".claude-mpm" / "cache" / "agents"
|
|
217
222
|
|
|
218
223
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
219
224
|
|
|
@@ -923,6 +928,62 @@ class GitSourceSyncService:
|
|
|
923
928
|
"""
|
|
924
929
|
return self.cache_dir
|
|
925
930
|
|
|
931
|
+
def _cleanup_excluded_agents(
|
|
932
|
+
self,
|
|
933
|
+
deployment_dir: Path,
|
|
934
|
+
excluded_set: Set[str],
|
|
935
|
+
) -> Dict[str, List[str]]:
|
|
936
|
+
"""Remove excluded agents from deployment directory.
|
|
937
|
+
|
|
938
|
+
Removes any agents in the deployment directory whose normalized
|
|
939
|
+
names match the exclusion list. This ensures that excluded agents
|
|
940
|
+
are cleaned up from previous deployments.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
deployment_dir: Directory containing deployed agents
|
|
944
|
+
excluded_set: Set of normalized agent names to exclude
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Dictionary with cleanup results:
|
|
948
|
+
- removed: List of agent names that were removed
|
|
949
|
+
"""
|
|
950
|
+
cleanup_results: Dict[str, List[str]] = {"removed": []}
|
|
951
|
+
|
|
952
|
+
if not deployment_dir.exists():
|
|
953
|
+
logger.debug("Deployment directory does not exist, no cleanup needed")
|
|
954
|
+
return cleanup_results
|
|
955
|
+
|
|
956
|
+
for item in deployment_dir.iterdir():
|
|
957
|
+
# Only process .md files
|
|
958
|
+
if not item.is_file() or item.suffix != ".md":
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
# Skip hidden files
|
|
962
|
+
if item.name.startswith("."):
|
|
963
|
+
continue
|
|
964
|
+
|
|
965
|
+
# Normalize agent name for comparison
|
|
966
|
+
agent_name = _normalize_agent_name(item.stem)
|
|
967
|
+
|
|
968
|
+
# Check if this agent is excluded
|
|
969
|
+
if agent_name in excluded_set:
|
|
970
|
+
try:
|
|
971
|
+
item.unlink()
|
|
972
|
+
cleanup_results["removed"].append(item.stem)
|
|
973
|
+
logger.info(f"Removed excluded agent: {item.stem}")
|
|
974
|
+
except PermissionError as e:
|
|
975
|
+
logger.error(f"Permission denied removing {item.stem}: {e}")
|
|
976
|
+
except Exception as e:
|
|
977
|
+
logger.error(f"Failed to remove {item.stem}: {e}")
|
|
978
|
+
|
|
979
|
+
# Log summary
|
|
980
|
+
if cleanup_results["removed"]:
|
|
981
|
+
logger.info(
|
|
982
|
+
f"Cleanup complete: removed {len(cleanup_results['removed'])} excluded agents"
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
return cleanup_results
|
|
986
|
+
|
|
926
987
|
def deploy_agents_to_project(
|
|
927
988
|
self,
|
|
928
989
|
project_dir: Path,
|
|
@@ -977,6 +1038,8 @@ class GitSourceSyncService:
|
|
|
977
1038
|
"""
|
|
978
1039
|
import shutil
|
|
979
1040
|
|
|
1041
|
+
from claude_mpm.core.config import Config
|
|
1042
|
+
|
|
980
1043
|
# Deploy to .claude/agents/ where Claude Code expects them
|
|
981
1044
|
deployment_dir = project_dir / ".claude" / "agents"
|
|
982
1045
|
deployment_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -989,10 +1052,55 @@ class GitSourceSyncService:
|
|
|
989
1052
|
"deployment_dir": str(deployment_dir),
|
|
990
1053
|
}
|
|
991
1054
|
|
|
1055
|
+
# Load project config to get exclusion list
|
|
1056
|
+
config_file = project_dir / ".claude-mpm" / "configuration.yaml"
|
|
1057
|
+
if config_file.exists():
|
|
1058
|
+
config = Config(config_file=config_file)
|
|
1059
|
+
excluded_agents = config.get("excluded_agents", [])
|
|
1060
|
+
else:
|
|
1061
|
+
# No project config, no exclusions
|
|
1062
|
+
excluded_agents = []
|
|
1063
|
+
|
|
1064
|
+
# Create normalized exclusion set
|
|
1065
|
+
excluded_set: Set[str] = (
|
|
1066
|
+
{_normalize_agent_name(name) for name in excluded_agents}
|
|
1067
|
+
if excluded_agents
|
|
1068
|
+
else set()
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
if excluded_set:
|
|
1072
|
+
logger.info(
|
|
1073
|
+
f"Applying exclusions: {', '.join(sorted(excluded_agents))} "
|
|
1074
|
+
f"(normalized: {', '.join(sorted(excluded_set))})"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
992
1077
|
# Get agents from cache or use provided list
|
|
993
1078
|
if agent_list is None:
|
|
994
1079
|
agent_list = self._discover_cached_agents()
|
|
995
1080
|
|
|
1081
|
+
# Filter out excluded agents
|
|
1082
|
+
if excluded_set:
|
|
1083
|
+
original_count = len(agent_list)
|
|
1084
|
+
agent_list = [
|
|
1085
|
+
agent_path
|
|
1086
|
+
for agent_path in agent_list
|
|
1087
|
+
if _normalize_agent_name(Path(agent_path).stem) not in excluded_set
|
|
1088
|
+
]
|
|
1089
|
+
filtered_count = original_count - len(agent_list)
|
|
1090
|
+
if filtered_count > 0:
|
|
1091
|
+
logger.info(f"Filtered out {filtered_count} excluded agents")
|
|
1092
|
+
|
|
1093
|
+
# Clean up any previously deployed excluded agents
|
|
1094
|
+
if excluded_set:
|
|
1095
|
+
cleanup_results = self._cleanup_excluded_agents(
|
|
1096
|
+
deployment_dir, excluded_set
|
|
1097
|
+
)
|
|
1098
|
+
if cleanup_results["removed"]:
|
|
1099
|
+
logger.info(
|
|
1100
|
+
f"Cleaned up {len(cleanup_results['removed'])} excluded agents: "
|
|
1101
|
+
f"{', '.join(cleanup_results['removed'])}"
|
|
1102
|
+
)
|
|
1103
|
+
|
|
996
1104
|
logger.info(
|
|
997
1105
|
f"Deploying {len(agent_list)} agents from cache to {deployment_dir}"
|
|
998
1106
|
)
|
|
@@ -1010,14 +1118,17 @@ class GitSourceSyncService:
|
|
|
1010
1118
|
deploy_filename = Path(agent_path).name
|
|
1011
1119
|
deploy_file = deployment_dir / deploy_filename
|
|
1012
1120
|
|
|
1013
|
-
# Check if update needed (compare
|
|
1121
|
+
# Check if update needed (compare content, not just mtime)
|
|
1122
|
+
# DESIGN: Use content hash comparison for reliable change detection
|
|
1123
|
+
# Mtime comparison can fail when cache downloads have older timestamps
|
|
1014
1124
|
should_deploy = force
|
|
1015
1125
|
was_existing = deploy_file.exists()
|
|
1016
1126
|
|
|
1017
1127
|
if not force and was_existing:
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1128
|
+
# Compare file contents using hash
|
|
1129
|
+
cache_content = cache_file.read_bytes()
|
|
1130
|
+
deploy_content = deploy_file.read_bytes()
|
|
1131
|
+
should_deploy = cache_content != deploy_content
|
|
1021
1132
|
|
|
1022
1133
|
if not should_deploy and was_existing:
|
|
1023
1134
|
results["skipped"].append(deploy_filename)
|