claude-mpm 4.20.3__py3-none-any.whl → 5.1.8__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.

Files changed (520) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +35 -6
  3. claude_mpm/agents/OUTPUT_STYLE.md +3 -48
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +1241 -667
  5. claude_mpm/agents/PM_INSTRUCTIONS_TEACH.md +1322 -0
  6. claude_mpm/agents/WORKFLOW.md +75 -2
  7. claude_mpm/agents/__init__.py +6 -0
  8. claude_mpm/agents/agent_loader.py +1 -4
  9. claude_mpm/agents/base_agent.json +6 -3
  10. claude_mpm/agents/base_agent_loader.py +10 -35
  11. claude_mpm/agents/frontmatter_validator.py +1 -1
  12. claude_mpm/agents/templates/circuit-breakers.md +1254 -0
  13. claude_mpm/agents/templates/context-management-examples.md +544 -0
  14. claude_mpm/agents/templates/{pm_red_flags.md → pm-red-flags.md} +89 -19
  15. claude_mpm/agents/templates/pr-workflow-examples.md +427 -0
  16. claude_mpm/agents/templates/research-gate-examples.md +669 -0
  17. claude_mpm/agents/templates/structured-questions-examples.md +615 -0
  18. claude_mpm/agents/templates/ticket-completeness-examples.md +139 -0
  19. claude_mpm/agents/templates/ticketing-examples.md +277 -0
  20. claude_mpm/cli/__init__.py +37 -2
  21. claude_mpm/cli/commands/__init__.py +2 -0
  22. claude_mpm/cli/commands/agent_source.py +774 -0
  23. claude_mpm/cli/commands/agent_state_manager.py +188 -30
  24. claude_mpm/cli/commands/agents.py +959 -36
  25. claude_mpm/cli/commands/agents_cleanup.py +210 -0
  26. claude_mpm/cli/commands/agents_discover.py +338 -0
  27. claude_mpm/cli/commands/aggregate.py +1 -1
  28. claude_mpm/cli/commands/analyze.py +3 -3
  29. claude_mpm/cli/commands/auto_configure.py +537 -239
  30. claude_mpm/cli/commands/cleanup.py +1 -1
  31. claude_mpm/cli/commands/config.py +7 -4
  32. claude_mpm/cli/commands/configure.py +924 -45
  33. claude_mpm/cli/commands/configure_agent_display.py +4 -4
  34. claude_mpm/cli/commands/configure_navigation.py +63 -46
  35. claude_mpm/cli/commands/debug.py +12 -12
  36. claude_mpm/cli/commands/doctor.py +10 -2
  37. claude_mpm/cli/commands/hook_errors.py +277 -0
  38. claude_mpm/cli/commands/local_deploy.py +1 -4
  39. claude_mpm/cli/commands/mcp_install_commands.py +1 -1
  40. claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
  41. claude_mpm/cli/commands/mpm_init/core.py +573 -0
  42. claude_mpm/cli/commands/mpm_init/display.py +341 -0
  43. claude_mpm/cli/commands/mpm_init/git_activity.py +427 -0
  44. claude_mpm/cli/commands/mpm_init/modes.py +397 -0
  45. claude_mpm/cli/commands/mpm_init/prompts.py +442 -0
  46. claude_mpm/cli/commands/mpm_init_cli.py +396 -0
  47. claude_mpm/cli/commands/mpm_init_handler.py +67 -1
  48. claude_mpm/cli/commands/postmortem.py +401 -0
  49. claude_mpm/cli/commands/run.py +125 -167
  50. claude_mpm/cli/commands/skill_source.py +694 -0
  51. claude_mpm/cli/commands/skills.py +835 -44
  52. claude_mpm/cli/executor.py +78 -3
  53. claude_mpm/cli/interactive/agent_wizard.py +1032 -47
  54. claude_mpm/cli/parsers/agent_source_parser.py +171 -0
  55. claude_mpm/cli/parsers/agents_parser.py +256 -4
  56. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  57. claude_mpm/cli/parsers/base_parser.py +53 -0
  58. claude_mpm/cli/parsers/config_parser.py +96 -43
  59. claude_mpm/cli/parsers/mpm_init_parser.py +42 -0
  60. claude_mpm/cli/parsers/skill_source_parser.py +169 -0
  61. claude_mpm/cli/parsers/skills_parser.py +145 -0
  62. claude_mpm/cli/parsers/source_parser.py +138 -0
  63. claude_mpm/cli/startup.py +564 -108
  64. claude_mpm/cli/startup_display.py +480 -0
  65. claude_mpm/cli/utils.py +1 -1
  66. claude_mpm/cli_module/commands.py +1 -1
  67. claude_mpm/commands/{mpm-auto-configure.md → mpm-agents-auto-configure.md} +9 -0
  68. claude_mpm/commands/mpm-agents-detect.md +9 -0
  69. claude_mpm/commands/{mpm-agents.md → mpm-agents-list.md} +9 -0
  70. claude_mpm/commands/mpm-agents-recommend.md +9 -0
  71. claude_mpm/commands/{mpm-config.md → mpm-config-view.md} +9 -0
  72. claude_mpm/commands/mpm-doctor.md +9 -0
  73. claude_mpm/commands/mpm-help.md +17 -2
  74. claude_mpm/commands/mpm-init.md +28 -3
  75. claude_mpm/commands/mpm-monitor.md +9 -0
  76. claude_mpm/commands/mpm-postmortem.md +123 -0
  77. claude_mpm/commands/mpm-session-resume.md +381 -0
  78. claude_mpm/commands/mpm-status.md +9 -0
  79. claude_mpm/commands/{mpm-organize.md → mpm-ticket-organize.md} +9 -0
  80. claude_mpm/commands/mpm-ticket-view.md +552 -0
  81. claude_mpm/commands/mpm-version.md +9 -0
  82. claude_mpm/commands/mpm.md +11 -0
  83. claude_mpm/config/agent_presets.py +488 -0
  84. claude_mpm/config/agent_sources.py +325 -0
  85. claude_mpm/config/skill_presets.py +392 -0
  86. claude_mpm/config/skill_sources.py +590 -0
  87. claude_mpm/constants.py +13 -0
  88. claude_mpm/core/api_validator.py +1 -1
  89. claude_mpm/core/claude_runner.py +19 -35
  90. claude_mpm/core/config.py +24 -0
  91. claude_mpm/core/constants.py +1 -1
  92. claude_mpm/core/framework/__init__.py +3 -16
  93. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  94. claude_mpm/core/framework/loaders/instruction_loader.py +25 -5
  95. claude_mpm/core/framework/processors/metadata_processor.py +1 -1
  96. claude_mpm/core/hook_error_memory.py +381 -0
  97. claude_mpm/core/hook_manager.py +41 -2
  98. claude_mpm/core/interactive_session.py +131 -10
  99. claude_mpm/core/interfaces.py +56 -1
  100. claude_mpm/core/logger.py +3 -1
  101. claude_mpm/core/oneshot_session.py +110 -8
  102. claude_mpm/core/protocols/__init__.py +23 -0
  103. claude_mpm/core/protocols/runner_protocol.py +103 -0
  104. claude_mpm/core/protocols/session_protocol.py +131 -0
  105. claude_mpm/core/shared/singleton_manager.py +11 -4
  106. claude_mpm/core/system_context.py +38 -0
  107. claude_mpm/core/unified_config.py +22 -0
  108. claude_mpm/dashboard/static/css/activity.css +69 -69
  109. claude_mpm/dashboard/static/css/connection-status.css +10 -10
  110. claude_mpm/dashboard/static/css/dashboard.css +15 -15
  111. claude_mpm/dashboard/static/js/components/activity-tree.js +178 -178
  112. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +101 -101
  113. claude_mpm/dashboard/static/js/components/agent-inference.js +31 -31
  114. claude_mpm/dashboard/static/js/components/build-tracker.js +59 -59
  115. claude_mpm/dashboard/static/js/components/code-simple.js +107 -107
  116. claude_mpm/dashboard/static/js/components/connection-debug.js +101 -101
  117. claude_mpm/dashboard/static/js/components/diff-viewer.js +113 -113
  118. claude_mpm/dashboard/static/js/components/event-viewer.js +12 -12
  119. claude_mpm/dashboard/static/js/components/file-change-tracker.js +57 -57
  120. claude_mpm/dashboard/static/js/components/file-change-viewer.js +74 -74
  121. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +6 -6
  122. claude_mpm/dashboard/static/js/components/file-viewer.js +42 -42
  123. claude_mpm/dashboard/static/js/components/module-viewer.js +27 -27
  124. claude_mpm/dashboard/static/js/components/session-manager.js +14 -14
  125. claude_mpm/dashboard/static/js/components/socket-manager.js +1 -1
  126. claude_mpm/dashboard/static/js/components/ui-state-manager.js +14 -14
  127. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +110 -110
  128. claude_mpm/dashboard/static/js/components/working-directory.js +8 -8
  129. claude_mpm/dashboard/static/js/connection-manager.js +76 -76
  130. claude_mpm/dashboard/static/js/dashboard.js +76 -58
  131. claude_mpm/dashboard/static/js/extension-error-handler.js +22 -22
  132. claude_mpm/dashboard/static/js/socket-client.js +138 -121
  133. claude_mpm/dashboard/templates/code_simple.html +23 -23
  134. claude_mpm/dashboard/templates/index.html +18 -18
  135. claude_mpm/experimental/cli_enhancements.py +1 -5
  136. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/event_handlers.py +3 -1
  143. claude_mpm/hooks/claude_hooks/hook_handler.py +24 -7
  144. claude_mpm/hooks/claude_hooks/installer.py +45 -0
  145. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  146. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  151. claude_mpm/hooks/failure_learning/__init__.py +2 -8
  152. claude_mpm/hooks/failure_learning/failure_detection_hook.py +1 -6
  153. claude_mpm/hooks/failure_learning/fix_detection_hook.py +1 -6
  154. claude_mpm/hooks/failure_learning/learning_extraction_hook.py +1 -6
  155. claude_mpm/hooks/kuzu_response_hook.py +1 -5
  156. claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
  157. claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
  158. claude_mpm/models/git_repository.py +198 -0
  159. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  160. claude_mpm/scripts/start_activity_logging.py +3 -1
  161. claude_mpm/services/agents/agent_builder.py +45 -9
  162. claude_mpm/services/agents/agent_preset_service.py +238 -0
  163. claude_mpm/services/agents/agent_selection_service.py +484 -0
  164. claude_mpm/services/agents/auto_deploy_index_parser.py +569 -0
  165. claude_mpm/services/agents/cache_git_manager.py +621 -0
  166. claude_mpm/services/agents/deployment/agent_deployment.py +126 -2
  167. claude_mpm/services/agents/deployment/agent_discovery_service.py +105 -73
  168. claude_mpm/services/agents/deployment/agent_format_converter.py +1 -1
  169. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +1 -5
  170. claude_mpm/services/agents/deployment/agent_metrics_collector.py +3 -3
  171. claude_mpm/services/agents/deployment/agent_restore_handler.py +1 -4
  172. claude_mpm/services/agents/deployment/agent_template_builder.py +236 -15
  173. claude_mpm/services/agents/deployment/agents_directory_resolver.py +101 -15
  174. claude_mpm/services/agents/deployment/async_agent_deployment.py +2 -1
  175. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -3
  176. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +115 -15
  177. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +2 -2
  178. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +1 -4
  179. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +363 -0
  180. claude_mpm/services/agents/deployment/single_agent_deployer.py +2 -2
  181. claude_mpm/services/agents/deployment/system_instructions_deployer.py +168 -46
  182. claude_mpm/services/agents/deployment/validation/deployment_validator.py +2 -2
  183. claude_mpm/services/agents/git_source_manager.py +629 -0
  184. claude_mpm/services/agents/loading/framework_agent_loader.py +9 -12
  185. claude_mpm/services/agents/local_template_manager.py +50 -10
  186. claude_mpm/services/agents/single_tier_deployment_service.py +696 -0
  187. claude_mpm/services/agents/sources/__init__.py +13 -0
  188. claude_mpm/services/agents/sources/agent_sync_state.py +516 -0
  189. claude_mpm/services/agents/sources/git_source_sync_service.py +1087 -0
  190. claude_mpm/services/agents/startup_sync.py +239 -0
  191. claude_mpm/services/agents/toolchain_detector.py +474 -0
  192. claude_mpm/services/analysis/__init__.py +25 -0
  193. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  194. claude_mpm/services/analysis/postmortem_service.py +765 -0
  195. claude_mpm/services/cli/session_pause_manager.py +504 -0
  196. claude_mpm/services/cli/session_resume_helper.py +36 -16
  197. claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
  198. claude_mpm/services/command_deployment_service.py +200 -6
  199. claude_mpm/services/core/base.py +31 -11
  200. claude_mpm/services/core/interfaces/__init__.py +1 -3
  201. claude_mpm/services/core/interfaces/health.py +1 -4
  202. claude_mpm/services/core/interfaces.py +56 -1
  203. claude_mpm/services/core/models/__init__.py +2 -11
  204. claude_mpm/services/core/models/agent_config.py +3 -0
  205. claude_mpm/services/core/models/process.py +4 -0
  206. claude_mpm/services/diagnostics/checks/__init__.py +4 -0
  207. claude_mpm/services/diagnostics/checks/agent_check.py +0 -2
  208. claude_mpm/services/diagnostics/checks/agent_sources_check.py +577 -0
  209. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  210. claude_mpm/services/diagnostics/checks/mcp_check.py +0 -1
  211. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  212. claude_mpm/services/diagnostics/checks/monitor_check.py +0 -1
  213. claude_mpm/services/diagnostics/checks/skill_sources_check.py +587 -0
  214. claude_mpm/services/diagnostics/diagnostic_runner.py +9 -0
  215. claude_mpm/services/diagnostics/doctor_reporter.py +40 -10
  216. claude_mpm/services/diagnostics/models.py +21 -0
  217. claude_mpm/services/event_bus/direct_relay.py +3 -3
  218. claude_mpm/services/event_bus/event_bus.py +36 -3
  219. claude_mpm/services/event_bus/relay.py +23 -7
  220. claude_mpm/services/events/consumers/logging.py +1 -2
  221. claude_mpm/services/git/__init__.py +21 -0
  222. claude_mpm/services/git/git_operations_service.py +494 -0
  223. claude_mpm/services/github/__init__.py +21 -0
  224. claude_mpm/services/github/github_cli_service.py +397 -0
  225. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -5
  226. claude_mpm/services/infrastructure/monitoring/aggregator.py +1 -6
  227. claude_mpm/services/infrastructure/monitoring/resources.py +1 -1
  228. claude_mpm/services/instructions/__init__.py +9 -0
  229. claude_mpm/services/instructions/instruction_cache_service.py +374 -0
  230. claude_mpm/services/local_ops/__init__.py +5 -13
  231. claude_mpm/services/local_ops/health_checks/__init__.py +1 -3
  232. claude_mpm/services/local_ops/health_manager.py +1 -4
  233. claude_mpm/services/local_ops/process_manager.py +1 -1
  234. claude_mpm/services/local_ops/resource_monitor.py +2 -2
  235. claude_mpm/services/mcp_config_manager.py +75 -145
  236. claude_mpm/services/mcp_gateway/auto_configure.py +31 -25
  237. claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
  238. claude_mpm/services/mcp_gateway/core/process_pool.py +41 -26
  239. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +1 -6
  240. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -2
  241. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
  242. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +26 -21
  243. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +6 -2
  244. claude_mpm/services/mcp_service_verifier.py +6 -3
  245. claude_mpm/services/memory/failure_tracker.py +19 -4
  246. claude_mpm/services/memory/optimizer.py +1 -1
  247. claude_mpm/services/model/model_router.py +8 -9
  248. claude_mpm/services/monitor/daemon.py +29 -9
  249. claude_mpm/services/monitor/daemon_manager.py +96 -19
  250. claude_mpm/services/monitor/server.py +2 -2
  251. claude_mpm/services/native_agent_converter.py +356 -0
  252. claude_mpm/services/port_manager.py +1 -1
  253. claude_mpm/services/pr/__init__.py +14 -0
  254. claude_mpm/services/pr/pr_template_service.py +329 -0
  255. claude_mpm/services/project/documentation_manager.py +2 -1
  256. claude_mpm/services/project/project_organizer.py +4 -0
  257. claude_mpm/services/project/toolchain_analyzer.py +3 -1
  258. claude_mpm/services/runner_configuration_service.py +17 -3
  259. claude_mpm/services/self_upgrade_service.py +165 -7
  260. claude_mpm/services/session_management_service.py +16 -4
  261. claude_mpm/services/skills/__init__.py +18 -0
  262. claude_mpm/services/skills/git_skill_source_manager.py +1169 -0
  263. claude_mpm/services/skills/skill_discovery_service.py +568 -0
  264. claude_mpm/services/skills_config.py +547 -0
  265. claude_mpm/services/skills_deployer.py +955 -0
  266. claude_mpm/services/socketio/handlers/connection.py +1 -1
  267. claude_mpm/services/socketio/handlers/git.py +2 -2
  268. claude_mpm/services/socketio/server/core.py +1 -4
  269. claude_mpm/services/socketio/server/main.py +1 -3
  270. claude_mpm/services/system_instructions_service.py +1 -3
  271. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +0 -3
  272. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +0 -1
  273. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +1 -1
  274. claude_mpm/services/unified/deployment_strategies/vercel.py +1 -5
  275. claude_mpm/services/unified/unified_deployment.py +1 -5
  276. claude_mpm/services/version_control/conflict_resolution.py +6 -4
  277. claude_mpm/services/visualization/__init__.py +1 -5
  278. claude_mpm/services/visualization/mermaid_generator.py +2 -3
  279. claude_mpm/skills/__init__.py +3 -3
  280. claude_mpm/skills/agent_skills_injector.py +42 -49
  281. claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
  282. claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +17 -10
  283. claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +92 -39
  284. claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +13 -12
  285. claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +5 -3
  286. claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +19 -12
  287. claude_mpm/skills/bundled/performance-profiling.md +6 -0
  288. claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +6 -6
  289. claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +13 -9
  290. claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +8 -8
  291. claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +37 -15
  292. claude_mpm/skills/skills_registry.py +44 -48
  293. claude_mpm/skills/skills_service.py +117 -108
  294. claude_mpm/templates/questions/__init__.py +38 -0
  295. claude_mpm/templates/questions/base.py +193 -0
  296. claude_mpm/templates/questions/pr_strategy.py +311 -0
  297. claude_mpm/templates/questions/project_init.py +385 -0
  298. claude_mpm/templates/questions/ticket_mgmt.py +394 -0
  299. claude_mpm/tools/__main__.py +8 -8
  300. claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
  301. claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
  302. claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
  303. claude_mpm/tools/code_tree_analyzer/core.py +380 -0
  304. claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
  305. claude_mpm/tools/code_tree_analyzer/events.py +168 -0
  306. claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
  307. claude_mpm/tools/code_tree_analyzer/models.py +39 -0
  308. claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
  309. claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
  310. claude_mpm/utils/agent_dependency_loader.py +80 -13
  311. claude_mpm/utils/agent_filters.py +288 -0
  312. claude_mpm/utils/dependency_cache.py +3 -1
  313. claude_mpm/utils/gitignore.py +244 -0
  314. claude_mpm/utils/log_cleanup.py +3 -3
  315. claude_mpm/utils/migration.py +372 -0
  316. claude_mpm/utils/progress.py +387 -0
  317. claude_mpm/utils/robust_installer.py +3 -5
  318. claude_mpm/utils/structured_questions.py +619 -0
  319. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/METADATA +496 -65
  320. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/RECORD +328 -416
  321. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -17
  322. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +0 -3
  323. claude_mpm/agents/templates/agent-manager.json +0 -273
  324. claude_mpm/agents/templates/agentic-coder-optimizer.json +0 -248
  325. claude_mpm/agents/templates/api_qa.json +0 -180
  326. claude_mpm/agents/templates/circuit_breakers.md +0 -638
  327. claude_mpm/agents/templates/clerk-ops.json +0 -235
  328. claude_mpm/agents/templates/code_analyzer.json +0 -101
  329. claude_mpm/agents/templates/content-agent.json +0 -358
  330. claude_mpm/agents/templates/dart_engineer.json +0 -307
  331. claude_mpm/agents/templates/data_engineer.json +0 -225
  332. claude_mpm/agents/templates/documentation.json +0 -211
  333. claude_mpm/agents/templates/engineer.json +0 -210
  334. claude_mpm/agents/templates/gcp_ops_agent.json +0 -253
  335. claude_mpm/agents/templates/golang_engineer.json +0 -270
  336. claude_mpm/agents/templates/imagemagick.json +0 -264
  337. claude_mpm/agents/templates/java_engineer.json +0 -346
  338. claude_mpm/agents/templates/local_ops_agent.json +0 -1840
  339. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +0 -39
  340. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250901_010124_142.md +0 -400
  341. claude_mpm/agents/templates/memory_manager.json +0 -158
  342. claude_mpm/agents/templates/nextjs_engineer.json +0 -285
  343. claude_mpm/agents/templates/ops.json +0 -185
  344. claude_mpm/agents/templates/php-engineer.json +0 -281
  345. claude_mpm/agents/templates/product_owner.json +0 -338
  346. claude_mpm/agents/templates/project_organizer.json +0 -140
  347. claude_mpm/agents/templates/prompt-engineer.json +0 -737
  348. claude_mpm/agents/templates/python_engineer.json +0 -387
  349. claude_mpm/agents/templates/qa.json +0 -242
  350. claude_mpm/agents/templates/react_engineer.json +0 -238
  351. claude_mpm/agents/templates/refactoring_engineer.json +0 -276
  352. claude_mpm/agents/templates/research.json +0 -188
  353. claude_mpm/agents/templates/ruby-engineer.json +0 -280
  354. claude_mpm/agents/templates/rust_engineer.json +0 -275
  355. claude_mpm/agents/templates/security.json +0 -202
  356. claude_mpm/agents/templates/svelte-engineer.json +0 -225
  357. claude_mpm/agents/templates/ticketing.json +0 -177
  358. claude_mpm/agents/templates/typescript_engineer.json +0 -285
  359. claude_mpm/agents/templates/vercel_ops_agent.json +0 -412
  360. claude_mpm/agents/templates/version_control.json +0 -157
  361. claude_mpm/agents/templates/web_qa.json +0 -399
  362. claude_mpm/agents/templates/web_ui.json +0 -189
  363. claude_mpm/cli/commands/mpm_init.py +0 -2093
  364. claude_mpm/commands/mpm-tickets.md +0 -102
  365. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +0 -1
  366. claude_mpm/dashboard/react/components/DataInspector/DataInspector.module.css +0 -188
  367. claude_mpm/dashboard/react/components/EventViewer/EventViewer.module.css +0 -156
  368. claude_mpm/dashboard/react/components/shared/ConnectionStatus.module.css +0 -38
  369. claude_mpm/dashboard/react/components/shared/FilterBar.module.css +0 -92
  370. claude_mpm/dashboard/static/archive/activity_dashboard_fixed.html +0 -248
  371. claude_mpm/dashboard/static/archive/activity_dashboard_test.html +0 -61
  372. claude_mpm/dashboard/static/archive/test_activity_connection.html +0 -179
  373. claude_mpm/dashboard/static/archive/test_claude_tree_tab.html +0 -68
  374. claude_mpm/dashboard/static/archive/test_dashboard.html +0 -409
  375. claude_mpm/dashboard/static/archive/test_dashboard_fixed.html +0 -519
  376. claude_mpm/dashboard/static/archive/test_dashboard_verification.html +0 -181
  377. claude_mpm/dashboard/static/archive/test_file_data.html +0 -315
  378. claude_mpm/dashboard/static/archive/test_file_tree_empty_state.html +0 -243
  379. claude_mpm/dashboard/static/archive/test_file_tree_fix.html +0 -234
  380. claude_mpm/dashboard/static/archive/test_file_tree_rename.html +0 -117
  381. claude_mpm/dashboard/static/archive/test_file_tree_tab.html +0 -115
  382. claude_mpm/dashboard/static/archive/test_file_viewer.html +0 -224
  383. claude_mpm/dashboard/static/archive/test_final_activity.html +0 -220
  384. claude_mpm/dashboard/static/archive/test_tab_fix.html +0 -139
  385. claude_mpm/dashboard/static/built/assets/events.DjpNxWNo.css +0 -1
  386. claude_mpm/dashboard/static/built/components/activity-tree.js +0 -2
  387. claude_mpm/dashboard/static/built/components/agent-hierarchy.js +0 -777
  388. claude_mpm/dashboard/static/built/components/agent-inference.js +0 -2
  389. claude_mpm/dashboard/static/built/components/build-tracker.js +0 -333
  390. claude_mpm/dashboard/static/built/components/code-simple.js +0 -857
  391. claude_mpm/dashboard/static/built/components/code-tree/tree-breadcrumb.js +0 -353
  392. claude_mpm/dashboard/static/built/components/code-tree/tree-constants.js +0 -235
  393. claude_mpm/dashboard/static/built/components/code-tree/tree-search.js +0 -409
  394. claude_mpm/dashboard/static/built/components/code-tree/tree-utils.js +0 -435
  395. claude_mpm/dashboard/static/built/components/code-tree.js +0 -2
  396. claude_mpm/dashboard/static/built/components/code-viewer.js +0 -2
  397. claude_mpm/dashboard/static/built/components/connection-debug.js +0 -654
  398. claude_mpm/dashboard/static/built/components/diff-viewer.js +0 -891
  399. claude_mpm/dashboard/static/built/components/event-processor.js +0 -2
  400. claude_mpm/dashboard/static/built/components/event-viewer.js +0 -2
  401. claude_mpm/dashboard/static/built/components/export-manager.js +0 -2
  402. claude_mpm/dashboard/static/built/components/file-change-tracker.js +0 -443
  403. claude_mpm/dashboard/static/built/components/file-change-viewer.js +0 -690
  404. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +0 -2
  405. claude_mpm/dashboard/static/built/components/file-viewer.js +0 -2
  406. claude_mpm/dashboard/static/built/components/hud-library-loader.js +0 -2
  407. claude_mpm/dashboard/static/built/components/hud-manager.js +0 -2
  408. claude_mpm/dashboard/static/built/components/hud-visualizer.js +0 -2
  409. claude_mpm/dashboard/static/built/components/module-viewer.js +0 -2
  410. claude_mpm/dashboard/static/built/components/nav-bar.js +0 -145
  411. claude_mpm/dashboard/static/built/components/page-structure.js +0 -429
  412. claude_mpm/dashboard/static/built/components/session-manager.js +0 -2
  413. claude_mpm/dashboard/static/built/components/socket-manager.js +0 -2
  414. claude_mpm/dashboard/static/built/components/ui-state-manager.js +0 -2
  415. claude_mpm/dashboard/static/built/components/unified-data-viewer.js +0 -2
  416. claude_mpm/dashboard/static/built/components/working-directory.js +0 -2
  417. claude_mpm/dashboard/static/built/connection-manager.js +0 -536
  418. claude_mpm/dashboard/static/built/dashboard.js +0 -2
  419. claude_mpm/dashboard/static/built/extension-error-handler.js +0 -164
  420. claude_mpm/dashboard/static/built/react/events.js +0 -30
  421. claude_mpm/dashboard/static/built/shared/dom-helpers.js +0 -396
  422. claude_mpm/dashboard/static/built/shared/event-bus.js +0 -330
  423. claude_mpm/dashboard/static/built/shared/event-filter-service.js +0 -540
  424. claude_mpm/dashboard/static/built/shared/logger.js +0 -385
  425. claude_mpm/dashboard/static/built/shared/page-structure.js +0 -249
  426. claude_mpm/dashboard/static/built/shared/tooltip-service.js +0 -253
  427. claude_mpm/dashboard/static/built/socket-client.js +0 -2
  428. claude_mpm/dashboard/static/built/tab-isolation-fix.js +0 -185
  429. claude_mpm/dashboard/static/dist/assets/events.DjpNxWNo.css +0 -1
  430. claude_mpm/dashboard/static/dist/components/activity-tree.js +0 -2
  431. claude_mpm/dashboard/static/dist/components/agent-inference.js +0 -2
  432. claude_mpm/dashboard/static/dist/components/code-tree.js +0 -2
  433. claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
  434. claude_mpm/dashboard/static/dist/components/event-processor.js +0 -2
  435. claude_mpm/dashboard/static/dist/components/event-viewer.js +0 -2
  436. claude_mpm/dashboard/static/dist/components/export-manager.js +0 -2
  437. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +0 -2
  438. claude_mpm/dashboard/static/dist/components/file-viewer.js +0 -2
  439. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +0 -2
  440. claude_mpm/dashboard/static/dist/components/hud-manager.js +0 -2
  441. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +0 -2
  442. claude_mpm/dashboard/static/dist/components/module-viewer.js +0 -2
  443. claude_mpm/dashboard/static/dist/components/session-manager.js +0 -2
  444. claude_mpm/dashboard/static/dist/components/socket-manager.js +0 -2
  445. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +0 -2
  446. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +0 -2
  447. claude_mpm/dashboard/static/dist/components/working-directory.js +0 -2
  448. claude_mpm/dashboard/static/dist/dashboard.js +0 -2
  449. claude_mpm/dashboard/static/dist/react/events.js +0 -30
  450. claude_mpm/dashboard/static/dist/socket-client.js +0 -2
  451. claude_mpm/dashboard/static/events.html +0 -607
  452. claude_mpm/dashboard/static/index.html +0 -635
  453. claude_mpm/dashboard/static/js/shared/dom-helpers.js +0 -396
  454. claude_mpm/dashboard/static/js/shared/event-bus.js +0 -330
  455. claude_mpm/dashboard/static/js/shared/logger.js +0 -385
  456. claude_mpm/dashboard/static/js/shared/tooltip-service.js +0 -253
  457. claude_mpm/dashboard/static/js/stores/dashboard-store.js +0 -562
  458. claude_mpm/dashboard/static/legacy/activity.html +0 -736
  459. claude_mpm/dashboard/static/legacy/agents.html +0 -786
  460. claude_mpm/dashboard/static/legacy/files.html +0 -747
  461. claude_mpm/dashboard/static/legacy/tools.html +0 -831
  462. claude_mpm/dashboard/static/monitors.html +0 -431
  463. claude_mpm/dashboard/static/production/events.html +0 -659
  464. claude_mpm/dashboard/static/production/main.html +0 -698
  465. claude_mpm/dashboard/static/production/monitors.html +0 -483
  466. claude_mpm/dashboard/static/test-archive/dashboard.html +0 -635
  467. claude_mpm/dashboard/static/test-archive/debug-events.html +0 -147
  468. claude_mpm/dashboard/static/test-archive/test-navigation.html +0 -256
  469. claude_mpm/dashboard/static/test-archive/test-react-exports.html +0 -180
  470. claude_mpm/dashboard/static/test-archive/test_debug.html +0 -25
  471. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +0 -75
  472. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +0 -184
  473. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +0 -107
  474. claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +0 -146
  475. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +0 -118
  476. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +0 -177
  477. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +0 -119
  478. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +0 -148
  479. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +0 -483
  480. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +0 -452
  481. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +0 -449
  482. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +0 -411
  483. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +0 -14
  484. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +0 -58
  485. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +0 -68
  486. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +0 -69
  487. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +0 -175
  488. claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +0 -213
  489. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +0 -314
  490. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +0 -227
  491. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +0 -74
  492. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +0 -32
  493. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +0 -47
  494. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +0 -65
  495. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +0 -30
  496. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +0 -16
  497. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +0 -328
  498. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +0 -602
  499. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +0 -915
  500. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +0 -916
  501. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +0 -752
  502. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +0 -209
  503. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +0 -123
  504. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +0 -145
  505. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +0 -543
  506. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +0 -741
  507. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +0 -470
  508. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +0 -458
  509. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +0 -639
  510. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +0 -304
  511. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +0 -96
  512. claude_mpm/tools/code_tree_analyzer.py +0 -1825
  513. /claude_mpm/agents/templates/{git_file_tracking.md → git-file-tracking.md} +0 -0
  514. /claude_mpm/agents/templates/{pm_examples.md → pm-examples.md} +0 -0
  515. /claude_mpm/agents/templates/{response_format.md → response-format.md} +0 -0
  516. /claude_mpm/agents/templates/{validation_templates.md → validation-templates.md} +0 -0
  517. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/WHEEL +0 -0
  518. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/entry_points.txt +0 -0
  519. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/licenses/LICENSE +0 -0
  520. {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1169 @@
1
+ """Git source manager for multi-repository skill sync and discovery.
2
+
3
+ This module manages multiple Git-based skill sources with priority resolution.
4
+ It orchestrates syncing, caching, and discovery of skills from multiple repositories,
5
+ applying priority-based conflict resolution when skills have the same ID.
6
+
7
+ Design Decision: Reuse GitSourceSyncService for all Git operations
8
+
9
+ Rationale: The GitSourceSyncService provides robust ETag-based caching and
10
+ incremental updates for Git repositories. Rather than duplicating this logic,
11
+ we compose it and adapt for skills-specific discovery.
12
+
13
+ Trade-offs:
14
+ - Code Reuse: Leverage proven sync infrastructure
15
+ - Maintainability: Single source of truth for Git operations
16
+ - Flexibility: Easy to extend with skills-specific features
17
+ """
18
+
19
+ from concurrent.futures import ThreadPoolExecutor, as_completed
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from threading import Lock
23
+ from typing import Any, Dict, List, Optional, Tuple
24
+
25
+ from claude_mpm.config.skill_sources import SkillSource, SkillSourceConfiguration
26
+ from claude_mpm.core.logging_config import get_logger
27
+ from claude_mpm.services.agents.sources.git_source_sync_service import (
28
+ GitSourceSyncService,
29
+ )
30
+ from claude_mpm.services.skills.skill_discovery_service import SkillDiscoveryService
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ class GitSkillSourceManager:
36
+ """Manages multiple Git-based skill sources with priority resolution.
37
+
38
+ Responsibilities:
39
+ - Coordinate syncing of multiple skill repositories
40
+ - Apply priority-based resolution for duplicate skills
41
+ - Provide unified catalog of available skills
42
+ - Handle caching and updates
43
+
44
+ Priority Resolution:
45
+ - Lower priority number = higher precedence
46
+ - Priority 0 reserved for system repository
47
+ - Skills with same ID: lowest priority wins
48
+
49
+ Design Pattern: Orchestrator with Dependency Injection
50
+
51
+ This class orchestrates multiple services (sync, discovery) without
52
+ reimplementing their logic. Services can be injected for testing.
53
+
54
+ Example:
55
+ >>> config = SkillSourceConfiguration()
56
+ >>> manager = GitSkillSourceManager(config)
57
+ >>> results = manager.sync_all_sources()
58
+ >>> skills = manager.get_all_skills()
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ config: SkillSourceConfiguration,
64
+ cache_dir: Optional[Path] = None,
65
+ sync_service: Optional[GitSourceSyncService] = None,
66
+ ):
67
+ """Initialize skill source manager.
68
+
69
+ Args:
70
+ config: Skill source configuration
71
+ cache_dir: Cache directory (defaults to ~/.claude-mpm/cache/skills/)
72
+ sync_service: Git sync service (injected for testing)
73
+ """
74
+ if cache_dir is None:
75
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "skills"
76
+
77
+ self.config = config
78
+ self.cache_dir = cache_dir
79
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
80
+ self.sync_service = sync_service # Use injected if provided
81
+ self.logger = get_logger(__name__)
82
+ self._etag_cache_lock = Lock() # Thread-safe ETag cache operations
83
+
84
+ self.logger.info(
85
+ f"GitSkillSourceManager initialized with cache: {self.cache_dir}"
86
+ )
87
+
88
+ def sync_all_sources(
89
+ self, force: bool = False, progress_callback=None
90
+ ) -> Dict[str, Any]:
91
+ """Sync all enabled skill sources.
92
+
93
+ Syncs sources in priority order (lower priority first). Individual
94
+ failures don't stop overall sync.
95
+
96
+ Args:
97
+ force: Force re-download even if cached
98
+ progress_callback: Optional callback(increment: int) called for each file synced
99
+
100
+ Returns:
101
+ Dict with sync results for each source:
102
+ {
103
+ "synced_count": int,
104
+ "failed_count": int,
105
+ "total_files_updated": int,
106
+ "total_files_cached": int,
107
+ "sources": {
108
+ "source_id": {
109
+ "synced": bool,
110
+ "files_updated": int,
111
+ "skills_discovered": int,
112
+ "error": str (if failed)
113
+ }
114
+ },
115
+ "timestamp": str
116
+ }
117
+
118
+ Example:
119
+ >>> manager = GitSkillSourceManager(config)
120
+ >>> results = manager.sync_all_sources()
121
+ >>> print(f"Synced {results['synced_count']} sources")
122
+ """
123
+ sources = self.config.get_enabled_sources()
124
+ self.logger.info(f"Syncing {len(sources)} enabled skill sources")
125
+
126
+ results = {
127
+ "synced_count": 0,
128
+ "failed_count": 0,
129
+ "total_files_updated": 0,
130
+ "total_files_cached": 0,
131
+ "sources": {},
132
+ "timestamp": datetime.now(timezone.utc).isoformat(),
133
+ }
134
+
135
+ for source in sources:
136
+ try:
137
+ result = self.sync_source(
138
+ source.id, force=force, progress_callback=progress_callback
139
+ )
140
+ results["sources"][source.id] = result
141
+
142
+ if result.get("synced"):
143
+ results["synced_count"] += 1
144
+ results["total_files_updated"] += result.get("files_updated", 0)
145
+ results["total_files_cached"] += result.get("files_cached", 0)
146
+ else:
147
+ results["failed_count"] += 1
148
+
149
+ except Exception as e:
150
+ self.logger.error(f"Exception syncing source {source.id}: {e}")
151
+ results["sources"][source.id] = {"synced": False, "error": str(e)}
152
+ results["failed_count"] += 1
153
+
154
+ self.logger.info(
155
+ f"Sync complete: {results['synced_count']} succeeded, "
156
+ f"{results['failed_count']} failed"
157
+ )
158
+
159
+ return results
160
+
161
+ def sync_source(
162
+ self, source_id: str, force: bool = False, progress_callback=None
163
+ ) -> Dict[str, Any]:
164
+ """Sync a specific skill source.
165
+
166
+ Design Decision: Recursive GitHub directory download for skills
167
+
168
+ Rationale: Skills use nested directory structures (e.g., universal/collaboration/SKILL.md)
169
+ unlike agents which are flat .md files. We need to recursively download the entire
170
+ repository structure to discover all SKILL.md files.
171
+
172
+ Approach: Use GitHub API to recursively discover all files, then download each via
173
+ raw.githubusercontent.com with ETag caching for efficiency.
174
+
175
+ Args:
176
+ source_id: ID of source to sync
177
+ force: Force re-download
178
+ progress_callback: Optional callback(increment: int) called for each file synced
179
+
180
+ Returns:
181
+ Sync result dict:
182
+ {
183
+ "synced": bool,
184
+ "files_updated": int,
185
+ "files_cached": int,
186
+ "skills_discovered": int,
187
+ "timestamp": str,
188
+ "error": str (if failed)
189
+ }
190
+
191
+ Raises:
192
+ ValueError: If source_id not found
193
+
194
+ Example:
195
+ >>> manager = GitSkillSourceManager(config)
196
+ >>> result = manager.sync_source("system")
197
+ >>> print(f"Updated {result['files_updated']} files")
198
+ """
199
+ source = self.config.get_source(source_id)
200
+ if not source:
201
+ raise ValueError(f"Source not found: {source_id}")
202
+
203
+ if not source.enabled:
204
+ self.logger.warning(f"Source is disabled: {source_id}")
205
+ return {"synced": False, "error": "Source is disabled"}
206
+
207
+ self.logger.info(f"Syncing skill source: {source_id} ({source.url})")
208
+
209
+ try:
210
+ # Determine cache path for this source
211
+ cache_path = self._get_source_cache_path(source)
212
+ cache_path.mkdir(parents=True, exist_ok=True)
213
+
214
+ # Recursively sync repository structure
215
+ files_updated, files_cached = self._recursive_sync_repository(
216
+ source, cache_path, force, progress_callback
217
+ )
218
+
219
+ # Discover skills in cache
220
+ discovery_service = SkillDiscoveryService(cache_path)
221
+ discovered_skills = discovery_service.discover_skills()
222
+
223
+ # Build result
224
+ result = {
225
+ "synced": True,
226
+ "files_updated": files_updated,
227
+ "files_cached": files_cached,
228
+ "skills_discovered": len(discovered_skills),
229
+ "timestamp": datetime.now(timezone.utc).isoformat(),
230
+ }
231
+
232
+ self.logger.info(
233
+ f"Sync complete for {source_id}: {result['files_updated']} updated, "
234
+ f"{result['skills_discovered']} skills discovered"
235
+ )
236
+
237
+ return result
238
+
239
+ except Exception as e:
240
+ self.logger.error(f"Failed to sync source {source_id}: {e}")
241
+ return {
242
+ "synced": False,
243
+ "error": str(e),
244
+ "timestamp": datetime.now(timezone.utc).isoformat(),
245
+ }
246
+
247
+ def get_all_skills(self) -> List[Dict[str, Any]]:
248
+ """Get all skills from all sources with priority resolution.
249
+
250
+ Returns:
251
+ List of resolved skill dicts, each containing:
252
+ {
253
+ "skill_id": str,
254
+ "name": str,
255
+ "description": str,
256
+ "version": str,
257
+ "tags": List[str],
258
+ "agent_types": List[str],
259
+ "content": str,
260
+ "source_id": str,
261
+ "source_priority": int,
262
+ "source_file": str
263
+ }
264
+
265
+ Priority Resolution Algorithm:
266
+ 1. Load skills from all enabled sources
267
+ 2. Group by skill ID (name converted to ID)
268
+ 3. For each group, select skill with lowest priority
269
+ 4. Return deduplicated skill list
270
+
271
+ Example:
272
+ >>> manager = GitSkillSourceManager(config)
273
+ >>> skills = manager.get_all_skills()
274
+ >>> for skill in skills:
275
+ ... print(f"{skill['name']} from {skill['source_id']}")
276
+ """
277
+ sources = self.config.get_enabled_sources()
278
+
279
+ if not sources:
280
+ self.logger.warning("No enabled sources found")
281
+ return []
282
+
283
+ # Collect skills from all sources
284
+ skills_by_source = {}
285
+
286
+ for source in sources:
287
+ try:
288
+ cache_path = self._get_source_cache_path(source)
289
+ if not cache_path.exists():
290
+ self.logger.debug(f"Cache not found for source: {source.id}")
291
+ continue
292
+
293
+ discovery_service = SkillDiscoveryService(cache_path)
294
+ source_skills = discovery_service.discover_skills()
295
+
296
+ # Tag skills with source metadata
297
+ for skill in source_skills:
298
+ skill["source_id"] = source.id
299
+ skill["source_priority"] = source.priority
300
+
301
+ skills_by_source[source.id] = source_skills
302
+
303
+ except Exception as e:
304
+ self.logger.warning(f"Failed to discover skills from {source.id}: {e}")
305
+ continue
306
+
307
+ # Apply priority resolution
308
+ resolved_skills = self._apply_priority_resolution(skills_by_source)
309
+
310
+ self.logger.info(
311
+ f"Discovered {len(resolved_skills)} skills from {len(skills_by_source)} sources"
312
+ )
313
+
314
+ return resolved_skills
315
+
316
+ def get_skills_by_source(self, source_id: str) -> List[Dict[str, Any]]:
317
+ """Get skills from a specific source.
318
+
319
+ Args:
320
+ source_id: ID of source to query
321
+
322
+ Returns:
323
+ List of skill dicts from that source
324
+
325
+ Example:
326
+ >>> manager = GitSkillSourceManager(config)
327
+ >>> skills = manager.get_skills_by_source("system")
328
+ >>> print(f"Found {len(skills)} system skills")
329
+ """
330
+ source = self.config.get_source(source_id)
331
+ if not source:
332
+ self.logger.warning(f"Source not found: {source_id}")
333
+ return []
334
+
335
+ cache_path = self._get_source_cache_path(source)
336
+ if not cache_path.exists():
337
+ self.logger.debug(f"Cache not found for source: {source_id}")
338
+ return []
339
+
340
+ try:
341
+ discovery_service = SkillDiscoveryService(cache_path)
342
+ skills = discovery_service.discover_skills()
343
+
344
+ # Tag with source metadata
345
+ for skill in skills:
346
+ skill["source_id"] = source.id
347
+ skill["source_priority"] = source.priority
348
+
349
+ return skills
350
+
351
+ except Exception as e:
352
+ self.logger.error(f"Failed to discover skills from {source_id}: {e}")
353
+ return []
354
+
355
+ def _apply_priority_resolution(
356
+ self, skills_by_source: Dict[str, List[Dict[str, Any]]]
357
+ ) -> List[Dict[str, Any]]:
358
+ """Apply priority resolution to skill list.
359
+
360
+ Args:
361
+ skills_by_source: Dict mapping source_id to skill list
362
+
363
+ Returns:
364
+ Deduplicated skill list with priority resolution applied
365
+
366
+ Resolution Strategy:
367
+ - Group skills by skill_id
368
+ - For each group, select skill from source with lowest priority
369
+ - If multiple skills have same priority, use first encountered
370
+
371
+ Example:
372
+ skills_by_source = {
373
+ "system": [{"skill_id": "review", "source_priority": 0}],
374
+ "custom": [{"skill_id": "review", "source_priority": 100}]
375
+ }
376
+ # Returns: skill from "system" (priority 0 < 100)
377
+ """
378
+ # Flatten skills from all sources
379
+ all_skills = []
380
+ for skills in skills_by_source.values():
381
+ all_skills.extend(skills)
382
+
383
+ if not all_skills:
384
+ return []
385
+
386
+ # Group by skill_id
387
+ skills_by_id: Dict[str, List[Dict[str, Any]]] = {}
388
+ for skill in all_skills:
389
+ skill_id = skill.get("skill_id", skill.get("name", "unknown"))
390
+ if skill_id not in skills_by_id:
391
+ skills_by_id[skill_id] = []
392
+ skills_by_id[skill_id].append(skill)
393
+
394
+ # Select skill with lowest priority for each group
395
+ resolved_skills = []
396
+ for skill_id, skill_group in skills_by_id.items():
397
+ # Sort by priority (ascending), take first
398
+ skill_group_sorted = sorted(
399
+ skill_group, key=lambda s: s.get("source_priority", 999)
400
+ )
401
+ selected_skill = skill_group_sorted[0]
402
+
403
+ # Log if multiple versions exist
404
+ if len(skill_group) > 1:
405
+ sources = [s.get("source_id") for s in skill_group]
406
+ self.logger.debug(
407
+ f"Skill '{skill_id}' found in multiple sources {sources}, "
408
+ f"using source '{selected_skill.get('source_id')}'"
409
+ )
410
+
411
+ resolved_skills.append(selected_skill)
412
+
413
+ return resolved_skills
414
+
415
+ def _recursive_sync_repository(
416
+ self,
417
+ source: SkillSource,
418
+ cache_path: Path,
419
+ force: bool = False,
420
+ progress_callback=None,
421
+ ) -> Tuple[int, int]:
422
+ """Recursively sync entire GitHub repository structure to cache.
423
+
424
+ Design Decision: Two-phase sync architecture (Phase 2 refactoring)
425
+
426
+ Rationale: Separates syncing (to cache) from deployment (to project).
427
+ Phase 1: Download ALL repository files to cache with Git Tree API
428
+ Phase 2: Deploy selected skills from cache to project-specific locations
429
+
430
+ This refactoring follows the agent sync pattern (git_source_sync_service.py)
431
+ with cache-first architecture for multi-project support.
432
+
433
+ Trade-offs:
434
+ - Storage: 2x disk usage (cache + deployments) vs. direct deployment
435
+ - Performance: Copy operation adds ~10ms, but enables offline deployment
436
+ - Flexibility: Multiple projects can deploy from single cache
437
+ - Isolation: Projects have independent skill sets from shared cache
438
+
439
+ Args:
440
+ source: SkillSource configuration
441
+ cache_path: Local cache directory (structure preserved)
442
+ force: Force re-download even if ETag cached
443
+ progress_callback: Optional callback(absolute_position: int) for progress tracking
444
+
445
+ Returns:
446
+ Tuple of (files_updated, files_cached)
447
+
448
+ Algorithm:
449
+ 1. Parse GitHub URL to extract owner/repo
450
+ 2. Discover ALL files via Git Tree API (recursive=1, single request)
451
+ 3. Filter for relevant files (.md, .json, .gitignore)
452
+ 4. Download each file to cache with ETag caching
453
+ 5. Call progress_callback with ABSOLUTE position (not increment)
454
+ 6. Preserve nested directory structure in cache
455
+
456
+ Error Handling:
457
+ - Invalid GitHub URL: Raises ValueError
458
+ - Tree API failure: Returns 0, 0 (logged as warning)
459
+ - Individual file failures: Logged but don't stop sync
460
+ """
461
+ # Parse GitHub URL
462
+ url_parts = source.url.rstrip("/").replace(".git", "").split("github.com/")
463
+ if len(url_parts) != 2:
464
+ raise ValueError(f"Invalid GitHub URL: {source.url}")
465
+
466
+ repo_path = url_parts[1].strip("/")
467
+ owner_repo = "/".join(repo_path.split("/")[:2])
468
+
469
+ # Step 1: Discover all files via GitHub Tree API (single request)
470
+ # This discovers the COMPLETE repository structure (272 files for skills)
471
+ all_files = self._discover_repository_files_via_tree_api(
472
+ owner_repo, source.branch
473
+ )
474
+
475
+ if not all_files:
476
+ self.logger.warning(f"No files discovered in repository: {source.url}")
477
+ return 0, 0
478
+
479
+ self.logger.info(
480
+ f"Discovered {len(all_files)} files in {owner_repo}/{source.branch} via Tree API"
481
+ )
482
+
483
+ # Step 2: Filter to only download relevant files (markdown, JSON metadata)
484
+ relevant_files = [
485
+ f
486
+ for f in all_files
487
+ if f.endswith(".md") or f.endswith(".json") or f == ".gitignore"
488
+ ]
489
+
490
+ self.logger.info(
491
+ f"Filtered to {len(relevant_files)} relevant files (.md, .json, .gitignore)"
492
+ )
493
+
494
+ # Step 3: Download files to cache with ETag caching (parallel)
495
+ files_updated = 0
496
+ files_cached = 0
497
+
498
+ # Use ThreadPoolExecutor for parallel downloads (10 workers for optimal performance)
499
+ # Trade-off: 10 workers balances speed (306 files in ~3-5s) vs. GitHub rate limits
500
+ with ThreadPoolExecutor(max_workers=10) as executor:
501
+ # Submit all download tasks
502
+ future_to_file = {}
503
+ for file_path in relevant_files:
504
+ raw_url = f"https://raw.githubusercontent.com/{owner_repo}/{source.branch}/{file_path}"
505
+ cache_file = cache_path / file_path
506
+ future = executor.submit(
507
+ self._download_file_with_etag, raw_url, cache_file, force
508
+ )
509
+ future_to_file[future] = file_path
510
+
511
+ # Process completed downloads as they finish
512
+ completed = 0
513
+ for future in as_completed(future_to_file):
514
+ completed += 1
515
+ try:
516
+ updated = future.result()
517
+ if updated:
518
+ files_updated += 1
519
+ else:
520
+ files_cached += 1
521
+ except Exception as e:
522
+ file_path = future_to_file[future]
523
+ self.logger.warning(f"Failed to download {file_path}: {e}")
524
+
525
+ # Call progress callback with ABSOLUTE position
526
+ if progress_callback:
527
+ progress_callback(completed)
528
+
529
+ self.logger.info(
530
+ f"Repository sync complete: {files_updated} updated, "
531
+ f"{files_cached} cached from {len(relevant_files)} files"
532
+ )
533
+ return files_updated, files_cached
534
+
535
+ def _discover_repository_files_via_tree_api(
536
+ self, owner_repo: str, branch: str
537
+ ) -> List[str]:
538
+ """Discover all files in repository using GitHub Git Tree API.
539
+
540
+ Design Decision: Two-step Tree API pattern (Phase 2 refactoring)
541
+
542
+ Rationale: Git Tree API with recursive=1 discovers entire repository
543
+ structure in a SINGLE request, solving the "limited file discovery" issue.
544
+ This is the same pattern used successfully in agent sync (Phase 1).
545
+
546
+ Previous Issue: Contents API only showed top-level files, missing nested
547
+ directories. This caused skills sync to discover only 1-2 files instead
548
+ of 272 files in the repository.
549
+
550
+ Trade-offs:
551
+ - Performance: Single API call vs. 50+ recursive Contents API calls
552
+ - Rate Limiting: 1 request vs. dozens (avoids 403 rate limit errors)
553
+ - Discovery: Finds ALL 272 files in nested structure
554
+ - API Complexity: Requires commit SHA lookup before tree fetch
555
+
556
+ Algorithm (matches agents pattern from git_source_sync_service.py):
557
+ 1. GET /repos/{owner}/{repo}/git/refs/heads/{branch} → commit SHA
558
+ 2. GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1 → all files
559
+ 3. Filter for blobs (files), exclude trees (directories)
560
+ 4. Return complete file list
561
+
562
+ Args:
563
+ owner_repo: GitHub owner/repo (e.g., "bobmatnyc/claude-mpm-skills")
564
+ branch: Branch name (e.g., "main")
565
+
566
+ Returns:
567
+ List of all file paths in repository
568
+ (e.g., ["collections/toolchains/python/pytest.md", ...])
569
+
570
+ Error Handling:
571
+ - HTTP 404: Branch or repo not found, raises RequestException
572
+ - HTTP 403: Rate limit exceeded (warns about GITHUB_TOKEN)
573
+ - Timeout: 30 second timeout per request
574
+ - Empty tree: Returns empty list (logged as warning)
575
+
576
+ Performance:
577
+ - Expected: ~500-800ms for 272 files (2 API calls)
578
+ - Rate Limit: Consumes 2 API calls per sync
579
+ - Scalability: Handles 1000s of files without truncation
580
+
581
+ Example:
582
+ >>> files = self._discover_repository_files_via_tree_api(
583
+ ... "bobmatnyc/claude-mpm-skills", "main"
584
+ ... )
585
+ >>> print(len(files))
586
+ 272 # Complete repository (not just top-level)
587
+ """
588
+ import requests
589
+
590
+ all_files = []
591
+
592
+ try:
593
+ # Step 1: Get the latest commit SHA for the branch
594
+ refs_url = (
595
+ f"https://api.github.com/repos/{owner_repo}/git/refs/heads/{branch}"
596
+ )
597
+ self.logger.debug(f"Fetching commit SHA from {refs_url}")
598
+
599
+ refs_response = requests.get(
600
+ refs_url, headers={"Accept": "application/vnd.github+json"}, timeout=30
601
+ )
602
+
603
+ # Check for rate limiting
604
+ if refs_response.status_code == 403:
605
+ self.logger.warning(
606
+ "GitHub API rate limit exceeded (HTTP 403). "
607
+ "Consider setting GITHUB_TOKEN environment variable for higher limits."
608
+ )
609
+ raise requests.RequestException("Rate limit exceeded")
610
+
611
+ refs_response.raise_for_status()
612
+ commit_sha = refs_response.json()["object"]["sha"]
613
+ self.logger.debug(f"Resolved {branch} to commit {commit_sha[:8]}")
614
+
615
+ # Step 2: Get the tree for that commit (recursive=1 gets ALL files)
616
+ tree_url = (
617
+ f"https://api.github.com/repos/{owner_repo}/git/trees/{commit_sha}"
618
+ )
619
+ params = {"recursive": "1"} # Recursively get entire tree
620
+
621
+ self.logger.debug(f"Fetching recursive tree from {tree_url}")
622
+ tree_response = requests.get(
623
+ tree_url,
624
+ headers={"Accept": "application/vnd.github+json"},
625
+ params=params,
626
+ timeout=30,
627
+ )
628
+ tree_response.raise_for_status()
629
+
630
+ tree_data = tree_response.json()
631
+ all_items = tree_data.get("tree", [])
632
+
633
+ self.logger.debug(f"Tree API returned {len(all_items)} total items")
634
+
635
+ # Step 3: Extract file paths (filter out directories)
636
+ for item in all_items:
637
+ if item["type"] == "blob": # blob = file, tree = directory
638
+ all_files.append(item["path"])
639
+
640
+ self.logger.info(
641
+ f"Discovered {len(all_files)} files via Tree API in {owner_repo}/{branch}"
642
+ )
643
+
644
+ except requests.exceptions.RequestException as e:
645
+ self.logger.error(f"Failed to discover files via Tree API: {e}")
646
+ # Fall back to empty list (sync will fail gracefully)
647
+ return []
648
+ except (KeyError, ValueError) as e:
649
+ self.logger.error(f"Error parsing GitHub API response: {e}")
650
+ return []
651
+
652
+ return all_files
653
+
654
+ def _download_file_with_etag(
655
+ self, url: str, local_path: Path, force: bool = False
656
+ ) -> bool:
657
+ """Download file from URL with ETag caching (thread-safe).
658
+
659
+ Args:
660
+ url: Raw GitHub URL
661
+ local_path: Local file path to save to
662
+ force: Force download even if cached
663
+
664
+ Returns:
665
+ True if file was updated, False if cached
666
+ """
667
+
668
+ import json
669
+
670
+ import requests
671
+
672
+ # Create parent directory (thread-safe with exist_ok=True)
673
+ local_path.parent.mkdir(parents=True, exist_ok=True)
674
+
675
+ # Thread-safe ETag cache operations
676
+ etag_cache_file = local_path.parent / ".etag_cache.json"
677
+
678
+ # Read cached ETag (lock required for file read)
679
+ with self._etag_cache_lock:
680
+ etag_cache = {}
681
+ if etag_cache_file.exists():
682
+ try:
683
+ with open(etag_cache_file, encoding="utf-8") as f:
684
+ etag_cache = json.load(f)
685
+ except Exception:
686
+ pass
687
+
688
+ cached_etag = etag_cache.get(str(local_path))
689
+
690
+ # Make conditional request (no lock needed - independent HTTP call)
691
+ headers = {}
692
+ if cached_etag and not force:
693
+ headers["If-None-Match"] = cached_etag
694
+
695
+ try:
696
+ response = requests.get(url, headers=headers, timeout=30)
697
+
698
+ # 304 Not Modified - use cached version
699
+ if response.status_code == 304:
700
+ self.logger.debug(f"Cache hit (ETag match): {local_path.name}")
701
+ return False
702
+
703
+ response.raise_for_status()
704
+
705
+ # Download and save file (no lock needed - independent file write)
706
+ local_path.write_bytes(response.content)
707
+
708
+ # Save new ETag (lock required for cache file write)
709
+ if "ETag" in response.headers:
710
+ with self._etag_cache_lock:
711
+ # Re-read cache in case other threads updated it
712
+ if etag_cache_file.exists():
713
+ try:
714
+ with open(etag_cache_file, encoding="utf-8") as f:
715
+ etag_cache = json.load(f)
716
+ except Exception:
717
+ etag_cache = {}
718
+
719
+ etag_cache[str(local_path)] = response.headers["ETag"]
720
+ with open(etag_cache_file, "w", encoding="utf-8") as f:
721
+ json.dump(etag_cache, f, indent=2)
722
+
723
+ self.logger.debug(f"Downloaded: {local_path.name}")
724
+ return True
725
+
726
+ except requests.exceptions.RequestException as e:
727
+ self.logger.warning(f"Failed to download {url}: {e}")
728
+ return False
729
+
730
+ def _build_raw_github_url(self, source: SkillSource) -> str:
731
+ """Build raw GitHub URL for source.
732
+
733
+ Args:
734
+ source: SkillSource instance
735
+
736
+ Returns:
737
+ Raw GitHub content URL
738
+
739
+ Example:
740
+ >>> source = SkillSource(
741
+ ... id="system",
742
+ ... url="https://github.com/owner/repo",
743
+ ... branch="main"
744
+ ... )
745
+ >>> url = manager._build_raw_github_url(source)
746
+ >>> print(url)
747
+ 'https://raw.githubusercontent.com/owner/repo/main'
748
+ """
749
+ # Parse GitHub URL to extract owner/repo
750
+ url = source.url.rstrip("/")
751
+ if url.endswith(".git"):
752
+ url = url[:-4]
753
+
754
+ # Extract path components
755
+ parts = url.split("github.com/")
756
+ if len(parts) != 2:
757
+ raise ValueError(f"Invalid GitHub URL: {source.url}")
758
+
759
+ repo_path = parts[1].strip("/")
760
+ owner_repo = "/".join(repo_path.split("/")[:2])
761
+
762
+ return f"https://raw.githubusercontent.com/{owner_repo}/{source.branch}"
763
+
764
+ def _get_source_cache_path(self, source: SkillSource) -> Path:
765
+ """Get cache directory path for a source.
766
+
767
+ Args:
768
+ source: SkillSource instance
769
+
770
+ Returns:
771
+ Absolute path to cache directory
772
+
773
+ Cache Structure:
774
+ ~/.claude-mpm/cache/skills/{source_id}/
775
+
776
+ Example:
777
+ >>> source = SkillSource(id="system", ...)
778
+ >>> path = manager._get_source_cache_path(source)
779
+ >>> print(path)
780
+ Path('/Users/user/.claude-mpm/cache/skills/system')
781
+ """
782
+ return self.cache_dir / source.id
783
+
784
+ def deploy_skills_to_project(
785
+ self,
786
+ project_dir: Path,
787
+ skill_list: Optional[List[str]] = None,
788
+ force: bool = False,
789
+ ) -> Dict[str, Any]:
790
+ """Deploy skills from cache to project directory (Phase 2 deployment).
791
+
792
+ Design Decision: Deploy from cache to project-specific directory
793
+
794
+ Rationale: Follows agent deployment pattern (git_source_sync_service.py).
795
+ Separates sync (cache) from deployment (project), enabling:
796
+ - Multiple projects using same cached skills
797
+ - Offline deployment from cache
798
+ - Project-specific skill selection
799
+ - Consistent two-phase architecture
800
+
801
+ This complements deploy_skills() which deploys to global ~/.claude/skills/.
802
+ This method deploys to project-local .claude-mpm/skills/ for project-specific
803
+ skill management.
804
+
805
+ Trade-offs:
806
+ - Storage: 2x disk (cache + project deployments)
807
+ - Performance: Copy ~10ms for 50 skills (negligible)
808
+ - Flexibility: Project-specific skill sets from shared cache
809
+ - Isolation: Projects don't affect each other
810
+
811
+ Args:
812
+ project_dir: Project root directory (e.g., /path/to/myproject)
813
+ skill_list: Optional list of skill names to deploy (deploys all if None)
814
+ force: Force redeployment even if up-to-date
815
+
816
+ Returns:
817
+ Dictionary with deployment results:
818
+ {
819
+ "deployed": ["skill1"], # Newly deployed
820
+ "updated": ["skill2"], # Updated existing
821
+ "skipped": ["skill3"], # Already up-to-date
822
+ "failed": [], # Copy failures
823
+ "deployment_dir": "/path/.claude-mpm/skills"
824
+ }
825
+
826
+ Algorithm:
827
+ 1. Create .claude-mpm/skills/ in project directory
828
+ 2. Get all skills from cache (or use provided list)
829
+ 3. For each skill:
830
+ a. Check if cache file exists
831
+ b. Flatten nested path to deployment name
832
+ c. Compare modification times (skip if up-to-date)
833
+ d. Copy from cache to project
834
+ e. Track result (deployed/updated/skipped/failed)
835
+ 4. Return deployment statistics
836
+
837
+ Error Handling:
838
+ - Missing cache files: Logged and added to "failed"
839
+ - Permission errors: Individual failures don't stop deployment
840
+ - Path validation: Security check prevents directory traversal
841
+
842
+ Example:
843
+ >>> manager = GitSkillSourceManager(config)
844
+ >>> manager.sync_all_sources() # Sync to cache first
845
+ >>> result = manager.deploy_skills_to_project(Path("/my/project"))
846
+ >>> print(f"Deployed {len(result['deployed'])} skills")
847
+ """
848
+ import shutil
849
+
850
+ deployment_dir = project_dir / ".claude-mpm" / "skills"
851
+
852
+ # Try to create deployment directory
853
+ try:
854
+ deployment_dir.mkdir(parents=True, exist_ok=True)
855
+ except PermissionError as e:
856
+ self.logger.error(f"Permission denied creating deployment directory: {e}")
857
+ return {
858
+ "deployed": [],
859
+ "deployed_count": 0,
860
+ "updated": [],
861
+ "updated_count": 0,
862
+ "skipped": [],
863
+ "skipped_count": 0,
864
+ "failed": [],
865
+ "failed_count": 0,
866
+ "deployment_dir": str(deployment_dir),
867
+ }
868
+
869
+ results = {
870
+ "deployed": [],
871
+ "updated": [],
872
+ "skipped": [],
873
+ "failed": [],
874
+ "deployment_dir": str(deployment_dir),
875
+ }
876
+
877
+ # Get all skills from cache or use provided list
878
+ if skill_list is None:
879
+ all_skills = self.get_all_skills()
880
+ else:
881
+ # Filter skills by provided list
882
+ all_skills = [
883
+ s for s in self.get_all_skills() if s.get("name") in skill_list
884
+ ]
885
+
886
+ self.logger.info(
887
+ f"Deploying {len(all_skills)} skills from cache to {deployment_dir}"
888
+ )
889
+
890
+ for skill in all_skills:
891
+ skill_name = skill.get("name", "unknown")
892
+ deployment_name = skill.get("deployment_name")
893
+ source_file = skill.get("source_file")
894
+
895
+ if not deployment_name or not source_file:
896
+ self.logger.warning(
897
+ f"Skill {skill_name} missing deployment_name or source_file, skipping"
898
+ )
899
+ results["failed"].append(skill_name)
900
+ continue
901
+
902
+ try:
903
+ source_path = Path(source_file)
904
+ if not source_path.exists():
905
+ self.logger.warning(f"Cache file not found: {source_file}")
906
+ results["failed"].append(skill_name)
907
+ continue
908
+
909
+ # Source is the entire skill directory (not just SKILL.md)
910
+ source_dir = source_path.parent
911
+ target_skill_dir = deployment_dir / deployment_name
912
+
913
+ # Check if already deployed and up-to-date
914
+ should_deploy = force
915
+ was_existing = target_skill_dir.exists()
916
+
917
+ if not force and was_existing:
918
+ # Compare modification times of SKILL.md files
919
+ source_mtime = source_path.stat().st_mtime
920
+ target_file = target_skill_dir / "SKILL.md"
921
+ if target_file.exists():
922
+ target_mtime = target_file.stat().st_mtime
923
+ should_deploy = source_mtime > target_mtime
924
+ else:
925
+ should_deploy = True
926
+
927
+ if not should_deploy and was_existing:
928
+ results["skipped"].append(deployment_name)
929
+ self.logger.debug(f"Skipped (up-to-date): {deployment_name}")
930
+ continue
931
+
932
+ # Security: Validate paths
933
+ if not self._validate_safe_path(deployment_dir, target_skill_dir):
934
+ self.logger.error(f"Invalid target path: {target_skill_dir}")
935
+ results["failed"].append(skill_name)
936
+ continue
937
+
938
+ # Remove existing if force or updating
939
+ if target_skill_dir.exists():
940
+ if target_skill_dir.is_symlink():
941
+ self.logger.warning(f"Removing symlink: {target_skill_dir}")
942
+ target_skill_dir.unlink()
943
+ else:
944
+ shutil.rmtree(target_skill_dir)
945
+
946
+ # Copy entire skill directory from cache
947
+ shutil.copytree(source_dir, target_skill_dir)
948
+
949
+ # Track result
950
+ if was_existing:
951
+ results["updated"].append(deployment_name)
952
+ self.logger.info(f"Updated: {deployment_name}")
953
+ else:
954
+ results["deployed"].append(deployment_name)
955
+ self.logger.info(f"Deployed: {deployment_name}")
956
+
957
+ except PermissionError as e:
958
+ self.logger.error(f"Permission denied deploying {skill_name}: {e}")
959
+ results["failed"].append(skill_name)
960
+ except OSError as e:
961
+ self.logger.error(f"IO error deploying {skill_name}: {e}")
962
+ results["failed"].append(skill_name)
963
+ except Exception as e:
964
+ self.logger.error(f"Unexpected error deploying {skill_name}: {e}")
965
+ results["failed"].append(skill_name)
966
+
967
+ # Log summary
968
+ total_success = len(results["deployed"]) + len(results["updated"])
969
+ self.logger.info(
970
+ f"Deployment complete: {total_success} deployed/updated, "
971
+ f"{len(results['skipped'])} skipped, {len(results['failed'])} failed"
972
+ )
973
+
974
+ # Return format matching agents deployment pattern
975
+ return {
976
+ "deployed": results["deployed"],
977
+ "deployed_count": len(results["deployed"]),
978
+ "updated": results["updated"],
979
+ "updated_count": len(results["updated"]),
980
+ "skipped": results["skipped"],
981
+ "skipped_count": len(results["skipped"]),
982
+ "failed": results["failed"],
983
+ "failed_count": len(results["failed"]),
984
+ "deployment_dir": results["deployment_dir"],
985
+ }
986
+
987
+ def deploy_skills(
988
+ self,
989
+ target_dir: Optional[Path] = None,
990
+ force: bool = False,
991
+ progress_callback=None,
992
+ ) -> Dict[str, Any]:
993
+ """Deploy skills from cache to target directory with flat structure.
994
+
995
+ Flattens nested Git repository structure into Claude Code compatible
996
+ flat directory structure. Each skill directory is copied with a
997
+ hyphen-separated name derived from its path.
998
+
999
+ Transformation Example:
1000
+ Cache: collaboration/dispatching-parallel-agents/SKILL.md
1001
+ Deploy: collaboration-dispatching-parallel-agents/SKILL.md
1002
+
1003
+ Args:
1004
+ target_dir: Target deployment directory (default: ~/.claude/skills/)
1005
+ force: Overwrite existing skills
1006
+ progress_callback: Optional callback(increment: int) called for each skill deployed
1007
+
1008
+ Returns:
1009
+ Dict with deployment results:
1010
+ {
1011
+ "deployed_count": int,
1012
+ "skipped_count": int,
1013
+ "failed_count": int,
1014
+ "deployed_skills": List[str],
1015
+ "skipped_skills": List[str],
1016
+ "errors": List[str]
1017
+ }
1018
+
1019
+ Example:
1020
+ >>> manager = GitSkillSourceManager(config)
1021
+ >>> result = manager.deploy_skills()
1022
+ >>> print(f"Deployed {result['deployed_count']} skills")
1023
+ """
1024
+ if target_dir is None:
1025
+ target_dir = Path.home() / ".claude" / "skills"
1026
+
1027
+ target_dir.mkdir(parents=True, exist_ok=True)
1028
+
1029
+ deployed = []
1030
+ skipped = []
1031
+ errors = []
1032
+
1033
+ # Get all skills from all sources
1034
+ all_skills = self.get_all_skills()
1035
+
1036
+ self.logger.info(
1037
+ f"Deploying {len(all_skills)} skills to {target_dir} (force={force})"
1038
+ )
1039
+
1040
+ for idx, skill in enumerate(all_skills, start=1):
1041
+ skill_name = skill.get("name", "unknown")
1042
+ deployment_name = skill.get("deployment_name")
1043
+
1044
+ if not deployment_name:
1045
+ self.logger.warning(
1046
+ f"Skill {skill_name} missing deployment_name, skipping"
1047
+ )
1048
+ errors.append(f"{skill_name}: Missing deployment_name")
1049
+ if progress_callback:
1050
+ progress_callback(idx)
1051
+ continue
1052
+
1053
+ try:
1054
+ result = self._deploy_single_skill(
1055
+ skill, target_dir, deployment_name, force
1056
+ )
1057
+
1058
+ if result["deployed"]:
1059
+ deployed.append(deployment_name)
1060
+ elif result["skipped"]:
1061
+ skipped.append(deployment_name)
1062
+
1063
+ if result["error"]:
1064
+ errors.append(result["error"])
1065
+
1066
+ except Exception as e:
1067
+ self.logger.error(f"Failed to deploy {skill_name}: {e}")
1068
+ errors.append(f"{skill_name}: {e}")
1069
+
1070
+ # Call progress callback for each skill processed
1071
+ if progress_callback:
1072
+ progress_callback(idx)
1073
+
1074
+ self.logger.info(
1075
+ f"Deployment complete: {len(deployed)} deployed, "
1076
+ f"{len(skipped)} skipped, {len(errors)} errors"
1077
+ )
1078
+
1079
+ return {
1080
+ "deployed_count": len(deployed),
1081
+ "skipped_count": len(skipped),
1082
+ "failed_count": len(errors),
1083
+ "deployed_skills": deployed,
1084
+ "skipped_skills": skipped,
1085
+ "errors": errors,
1086
+ }
1087
+
1088
+ def _deploy_single_skill(
1089
+ self, skill: Dict[str, Any], target_dir: Path, deployment_name: str, force: bool
1090
+ ) -> Dict[str, Any]:
1091
+ """Deploy a single skill with flattened directory name.
1092
+
1093
+ Args:
1094
+ skill: Skill metadata dict
1095
+ target_dir: Target deployment directory
1096
+ deployment_name: Flattened deployment directory name
1097
+ force: Overwrite if exists
1098
+
1099
+ Returns:
1100
+ Dict with deployed, skipped, error flags
1101
+ """
1102
+ import shutil
1103
+
1104
+ source_file = Path(skill["source_file"])
1105
+ source_dir = source_file.parent
1106
+
1107
+ target_skill_dir = target_dir / deployment_name
1108
+
1109
+ # Check if already deployed
1110
+ if target_skill_dir.exists() and not force:
1111
+ self.logger.debug(f"Skipped {deployment_name} (already exists)")
1112
+ return {"deployed": False, "skipped": True, "error": None}
1113
+
1114
+ # Security: Validate paths
1115
+ if not self._validate_safe_path(target_dir, target_skill_dir):
1116
+ return {
1117
+ "deployed": False,
1118
+ "skipped": False,
1119
+ "error": f"Invalid target path: {target_skill_dir}",
1120
+ }
1121
+
1122
+ try:
1123
+ # Remove existing if force
1124
+ if target_skill_dir.exists():
1125
+ if target_skill_dir.is_symlink():
1126
+ self.logger.warning(f"Removing symlink: {target_skill_dir}")
1127
+ target_skill_dir.unlink()
1128
+ else:
1129
+ shutil.rmtree(target_skill_dir)
1130
+
1131
+ # Copy entire skill directory with all resources
1132
+ shutil.copytree(source_dir, target_skill_dir)
1133
+
1134
+ self.logger.debug(
1135
+ f"Deployed {deployment_name} from {source_dir} to {target_skill_dir}"
1136
+ )
1137
+ return {"deployed": True, "skipped": False, "error": None}
1138
+
1139
+ except Exception as e:
1140
+ return {
1141
+ "deployed": False,
1142
+ "skipped": False,
1143
+ "error": f"{deployment_name}: {e}",
1144
+ }
1145
+
1146
+ def _validate_safe_path(self, base: Path, target: Path) -> bool:
1147
+ """Ensure target path is within base directory (security).
1148
+
1149
+ Args:
1150
+ base: Base directory
1151
+ target: Target path to validate
1152
+
1153
+ Returns:
1154
+ True if path is safe, False otherwise
1155
+ """
1156
+ try:
1157
+ target.resolve().relative_to(base.resolve())
1158
+ return True
1159
+ except ValueError:
1160
+ return False
1161
+
1162
+ def __repr__(self) -> str:
1163
+ """Return string representation."""
1164
+ sources = self.config.load()
1165
+ enabled_count = len([s for s in sources if s.enabled])
1166
+ return (
1167
+ f"GitSkillSourceManager(cache='{self.cache_dir}', "
1168
+ f"sources={len(sources)}, enabled={enabled_count})"
1169
+ )