claude-mpm 4.21.0__py3-none-any.whl → 5.0.2__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.
Files changed (497) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +12 -0
  3. claude_mpm/agents/OUTPUT_STYLE.md +3 -48
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +632 -334
  5. claude_mpm/agents/WORKFLOW.md +75 -2
  6. claude_mpm/agents/__init__.py +6 -0
  7. claude_mpm/agents/agent_loader.py +1 -4
  8. claude_mpm/agents/base_agent.json +6 -3
  9. claude_mpm/agents/frontmatter_validator.py +1 -1
  10. claude_mpm/agents/templates/{circuit_breakers.md → circuit-breakers.md} +370 -3
  11. claude_mpm/agents/templates/context-management-examples.md +544 -0
  12. claude_mpm/agents/templates/{pm_red_flags.md → pm-red-flags.md} +89 -19
  13. claude_mpm/agents/templates/pr-workflow-examples.md +427 -0
  14. claude_mpm/agents/templates/research-gate-examples.md +669 -0
  15. claude_mpm/agents/templates/structured-questions-examples.md +615 -0
  16. claude_mpm/agents/templates/ticket-completeness-examples.md +139 -0
  17. claude_mpm/agents/templates/ticketing-examples.md +277 -0
  18. claude_mpm/cli/__init__.py +38 -2
  19. claude_mpm/cli/commands/agent_source.py +774 -0
  20. claude_mpm/cli/commands/agent_state_manager.py +125 -20
  21. claude_mpm/cli/commands/agents.py +684 -13
  22. claude_mpm/cli/commands/agents_cleanup.py +210 -0
  23. claude_mpm/cli/commands/agents_discover.py +338 -0
  24. claude_mpm/cli/commands/aggregate.py +1 -1
  25. claude_mpm/cli/commands/analyze.py +3 -3
  26. claude_mpm/cli/commands/auto_configure.py +2 -6
  27. claude_mpm/cli/commands/cleanup.py +1 -1
  28. claude_mpm/cli/commands/config.py +7 -4
  29. claude_mpm/cli/commands/configure.py +478 -44
  30. claude_mpm/cli/commands/configure_agent_display.py +4 -4
  31. claude_mpm/cli/commands/configure_navigation.py +63 -46
  32. claude_mpm/cli/commands/debug.py +12 -12
  33. claude_mpm/cli/commands/doctor.py +10 -2
  34. claude_mpm/cli/commands/hook_errors.py +277 -0
  35. claude_mpm/cli/commands/local_deploy.py +1 -4
  36. claude_mpm/cli/commands/mcp_install_commands.py +1 -1
  37. claude_mpm/cli/commands/mpm_init/core.py +50 -2
  38. claude_mpm/cli/commands/mpm_init/git_activity.py +10 -10
  39. claude_mpm/cli/commands/mpm_init/prompts.py +6 -6
  40. claude_mpm/cli/commands/run.py +124 -128
  41. claude_mpm/cli/commands/skill_source.py +694 -0
  42. claude_mpm/cli/commands/skills.py +435 -1
  43. claude_mpm/cli/executor.py +78 -3
  44. claude_mpm/cli/interactive/agent_wizard.py +919 -41
  45. claude_mpm/cli/parsers/agent_source_parser.py +171 -0
  46. claude_mpm/cli/parsers/agents_parser.py +173 -4
  47. claude_mpm/cli/parsers/base_parser.py +49 -0
  48. claude_mpm/cli/parsers/config_parser.py +96 -43
  49. claude_mpm/cli/parsers/skill_source_parser.py +169 -0
  50. claude_mpm/cli/parsers/skills_parser.py +138 -0
  51. claude_mpm/cli/parsers/source_parser.py +138 -0
  52. claude_mpm/cli/startup.py +499 -84
  53. claude_mpm/cli/startup_display.py +480 -0
  54. claude_mpm/cli/utils.py +1 -1
  55. claude_mpm/cli_module/commands.py +1 -1
  56. claude_mpm/commands/{mpm-auto-configure.md → mpm-agents-auto-configure.md} +9 -0
  57. claude_mpm/commands/mpm-agents-detect.md +9 -0
  58. claude_mpm/commands/{mpm-agents.md → mpm-agents-list.md} +9 -0
  59. claude_mpm/commands/mpm-agents-recommend.md +9 -0
  60. claude_mpm/commands/{mpm-config.md → mpm-config-view.md} +9 -0
  61. claude_mpm/commands/mpm-doctor.md +9 -0
  62. claude_mpm/commands/mpm-help.md +14 -2
  63. claude_mpm/commands/mpm-init.md +27 -2
  64. claude_mpm/commands/mpm-monitor.md +9 -0
  65. claude_mpm/commands/mpm-session-resume.md +381 -0
  66. claude_mpm/commands/mpm-status.md +9 -0
  67. claude_mpm/commands/{mpm-organize.md → mpm-ticket-organize.md} +9 -0
  68. claude_mpm/commands/mpm-ticket-view.md +552 -0
  69. claude_mpm/commands/mpm-version.md +9 -0
  70. claude_mpm/commands/mpm.md +11 -0
  71. claude_mpm/config/agent_presets.py +258 -0
  72. claude_mpm/config/agent_sources.py +325 -0
  73. claude_mpm/config/skill_sources.py +590 -0
  74. claude_mpm/constants.py +12 -0
  75. claude_mpm/core/api_validator.py +1 -1
  76. claude_mpm/core/claude_runner.py +17 -10
  77. claude_mpm/core/config.py +24 -0
  78. claude_mpm/core/constants.py +1 -1
  79. claude_mpm/core/framework/__init__.py +3 -16
  80. claude_mpm/core/framework/loaders/instruction_loader.py +25 -5
  81. claude_mpm/core/framework/processors/metadata_processor.py +1 -1
  82. claude_mpm/core/hook_error_memory.py +381 -0
  83. claude_mpm/core/hook_manager.py +41 -2
  84. claude_mpm/core/interactive_session.py +112 -5
  85. claude_mpm/core/logger.py +3 -1
  86. claude_mpm/core/oneshot_session.py +94 -4
  87. claude_mpm/dashboard/static/css/activity.css +69 -69
  88. claude_mpm/dashboard/static/css/connection-status.css +10 -10
  89. claude_mpm/dashboard/static/css/dashboard.css +15 -15
  90. claude_mpm/dashboard/static/js/components/activity-tree.js +178 -178
  91. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +101 -101
  92. claude_mpm/dashboard/static/js/components/agent-inference.js +31 -31
  93. claude_mpm/dashboard/static/js/components/build-tracker.js +59 -59
  94. claude_mpm/dashboard/static/js/components/code-simple.js +107 -107
  95. claude_mpm/dashboard/static/js/components/connection-debug.js +101 -101
  96. claude_mpm/dashboard/static/js/components/diff-viewer.js +113 -113
  97. claude_mpm/dashboard/static/js/components/event-viewer.js +12 -12
  98. claude_mpm/dashboard/static/js/components/file-change-tracker.js +57 -57
  99. claude_mpm/dashboard/static/js/components/file-change-viewer.js +74 -74
  100. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +6 -6
  101. claude_mpm/dashboard/static/js/components/file-viewer.js +42 -42
  102. claude_mpm/dashboard/static/js/components/module-viewer.js +27 -27
  103. claude_mpm/dashboard/static/js/components/session-manager.js +14 -14
  104. claude_mpm/dashboard/static/js/components/socket-manager.js +1 -1
  105. claude_mpm/dashboard/static/js/components/ui-state-manager.js +14 -14
  106. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +110 -110
  107. claude_mpm/dashboard/static/js/components/working-directory.js +8 -8
  108. claude_mpm/dashboard/static/js/connection-manager.js +76 -76
  109. claude_mpm/dashboard/static/js/dashboard.js +76 -58
  110. claude_mpm/dashboard/static/js/extension-error-handler.js +22 -22
  111. claude_mpm/dashboard/static/js/socket-client.js +138 -121
  112. claude_mpm/dashboard/templates/code_simple.html +23 -23
  113. claude_mpm/dashboard/templates/index.html +18 -18
  114. claude_mpm/experimental/cli_enhancements.py +1 -5
  115. claude_mpm/hooks/claude_hooks/event_handlers.py +3 -1
  116. claude_mpm/hooks/claude_hooks/hook_handler.py +24 -7
  117. claude_mpm/hooks/claude_hooks/installer.py +45 -0
  118. claude_mpm/hooks/failure_learning/__init__.py +2 -8
  119. claude_mpm/hooks/failure_learning/failure_detection_hook.py +1 -6
  120. claude_mpm/hooks/failure_learning/fix_detection_hook.py +1 -6
  121. claude_mpm/hooks/failure_learning/learning_extraction_hook.py +1 -6
  122. claude_mpm/hooks/kuzu_response_hook.py +1 -5
  123. claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
  124. claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
  125. claude_mpm/models/git_repository.py +198 -0
  126. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  127. claude_mpm/scripts/start_activity_logging.py +3 -1
  128. claude_mpm/services/agents/agent_builder.py +45 -9
  129. claude_mpm/services/agents/agent_preset_service.py +238 -0
  130. claude_mpm/services/agents/agent_selection_service.py +484 -0
  131. claude_mpm/services/agents/auto_deploy_index_parser.py +569 -0
  132. claude_mpm/services/agents/deployment/agent_deployment.py +126 -2
  133. claude_mpm/services/agents/deployment/agent_discovery_service.py +105 -73
  134. claude_mpm/services/agents/deployment/agent_format_converter.py +1 -1
  135. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +1 -5
  136. claude_mpm/services/agents/deployment/agent_metrics_collector.py +3 -3
  137. claude_mpm/services/agents/deployment/agent_restore_handler.py +1 -4
  138. claude_mpm/services/agents/deployment/agent_template_builder.py +236 -15
  139. claude_mpm/services/agents/deployment/agents_directory_resolver.py +101 -15
  140. claude_mpm/services/agents/deployment/async_agent_deployment.py +2 -1
  141. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -3
  142. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +115 -15
  143. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +2 -2
  144. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +1 -4
  145. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +363 -0
  146. claude_mpm/services/agents/deployment/single_agent_deployer.py +2 -2
  147. claude_mpm/services/agents/deployment/system_instructions_deployer.py +168 -46
  148. claude_mpm/services/agents/deployment/validation/deployment_validator.py +2 -2
  149. claude_mpm/services/agents/git_source_manager.py +629 -0
  150. claude_mpm/services/agents/loading/framework_agent_loader.py +9 -12
  151. claude_mpm/services/agents/local_template_manager.py +50 -10
  152. claude_mpm/services/agents/single_tier_deployment_service.py +696 -0
  153. claude_mpm/services/agents/sources/__init__.py +13 -0
  154. claude_mpm/services/agents/sources/agent_sync_state.py +516 -0
  155. claude_mpm/services/agents/sources/git_source_sync_service.py +1055 -0
  156. claude_mpm/services/agents/startup_sync.py +239 -0
  157. claude_mpm/services/agents/toolchain_detector.py +474 -0
  158. claude_mpm/services/cli/session_pause_manager.py +1 -1
  159. claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
  160. claude_mpm/services/command_deployment_service.py +92 -1
  161. claude_mpm/services/core/base.py +26 -11
  162. claude_mpm/services/core/interfaces/__init__.py +1 -3
  163. claude_mpm/services/core/interfaces/health.py +1 -4
  164. claude_mpm/services/core/models/__init__.py +2 -11
  165. claude_mpm/services/diagnostics/checks/__init__.py +4 -0
  166. claude_mpm/services/diagnostics/checks/agent_check.py +0 -2
  167. claude_mpm/services/diagnostics/checks/agent_sources_check.py +577 -0
  168. claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
  169. claude_mpm/services/diagnostics/checks/mcp_check.py +0 -1
  170. claude_mpm/services/diagnostics/checks/monitor_check.py +0 -1
  171. claude_mpm/services/diagnostics/checks/skill_sources_check.py +587 -0
  172. claude_mpm/services/diagnostics/diagnostic_runner.py +9 -0
  173. claude_mpm/services/diagnostics/doctor_reporter.py +40 -10
  174. claude_mpm/services/event_bus/direct_relay.py +3 -3
  175. claude_mpm/services/event_bus/event_bus.py +36 -3
  176. claude_mpm/services/event_bus/relay.py +23 -7
  177. claude_mpm/services/events/consumers/logging.py +1 -2
  178. claude_mpm/services/git/__init__.py +21 -0
  179. claude_mpm/services/git/git_operations_service.py +494 -0
  180. claude_mpm/services/github/__init__.py +21 -0
  181. claude_mpm/services/github/github_cli_service.py +397 -0
  182. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -5
  183. claude_mpm/services/infrastructure/monitoring/aggregator.py +1 -6
  184. claude_mpm/services/infrastructure/monitoring/resources.py +1 -1
  185. claude_mpm/services/instructions/__init__.py +9 -0
  186. claude_mpm/services/instructions/instruction_cache_service.py +374 -0
  187. claude_mpm/services/local_ops/__init__.py +3 -13
  188. claude_mpm/services/local_ops/health_checks/__init__.py +1 -3
  189. claude_mpm/services/local_ops/health_manager.py +1 -4
  190. claude_mpm/services/local_ops/process_manager.py +1 -1
  191. claude_mpm/services/local_ops/resource_monitor.py +2 -2
  192. claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
  193. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +1 -6
  194. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -2
  195. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
  196. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +6 -2
  197. claude_mpm/services/memory/failure_tracker.py +19 -4
  198. claude_mpm/services/memory/optimizer.py +1 -1
  199. claude_mpm/services/model/model_router.py +8 -9
  200. claude_mpm/services/monitor/daemon.py +1 -1
  201. claude_mpm/services/monitor/server.py +2 -2
  202. claude_mpm/services/native_agent_converter.py +356 -0
  203. claude_mpm/services/port_manager.py +1 -1
  204. claude_mpm/services/pr/__init__.py +14 -0
  205. claude_mpm/services/pr/pr_template_service.py +329 -0
  206. claude_mpm/services/project/documentation_manager.py +2 -1
  207. claude_mpm/services/project/toolchain_analyzer.py +3 -1
  208. claude_mpm/services/runner_configuration_service.py +1 -0
  209. claude_mpm/services/self_upgrade_service.py +165 -7
  210. claude_mpm/services/skills/__init__.py +18 -0
  211. claude_mpm/services/skills/git_skill_source_manager.py +1169 -0
  212. claude_mpm/services/skills/skill_discovery_service.py +568 -0
  213. claude_mpm/services/skills_config.py +547 -0
  214. claude_mpm/services/skills_deployer.py +955 -0
  215. claude_mpm/services/socketio/handlers/connection.py +1 -1
  216. claude_mpm/services/socketio/handlers/git.py +2 -2
  217. claude_mpm/services/socketio/server/core.py +1 -4
  218. claude_mpm/services/socketio/server/main.py +1 -3
  219. claude_mpm/services/system_instructions_service.py +1 -3
  220. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +0 -3
  221. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +0 -1
  222. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +1 -1
  223. claude_mpm/services/unified/deployment_strategies/vercel.py +1 -5
  224. claude_mpm/services/unified/unified_deployment.py +1 -5
  225. claude_mpm/services/version_control/conflict_resolution.py +6 -4
  226. claude_mpm/services/visualization/__init__.py +1 -5
  227. claude_mpm/services/visualization/mermaid_generator.py +2 -3
  228. claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
  229. claude_mpm/skills/bundled/performance-profiling.md +6 -0
  230. claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +2 -2
  231. claude_mpm/skills/skills_registry.py +0 -1
  232. claude_mpm/templates/questions/__init__.py +38 -0
  233. claude_mpm/templates/questions/base.py +193 -0
  234. claude_mpm/templates/questions/pr_strategy.py +311 -0
  235. claude_mpm/templates/questions/project_init.py +385 -0
  236. claude_mpm/templates/questions/ticket_mgmt.py +394 -0
  237. claude_mpm/tools/__main__.py +8 -8
  238. claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
  239. claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
  240. claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
  241. claude_mpm/tools/code_tree_analyzer/core.py +380 -0
  242. claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
  243. claude_mpm/tools/code_tree_analyzer/events.py +168 -0
  244. claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
  245. claude_mpm/tools/code_tree_analyzer/models.py +39 -0
  246. claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
  247. claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
  248. claude_mpm/utils/agent_dependency_loader.py +80 -13
  249. claude_mpm/utils/dependency_cache.py +3 -1
  250. claude_mpm/utils/gitignore.py +241 -0
  251. claude_mpm/utils/log_cleanup.py +3 -3
  252. claude_mpm/utils/progress.py +383 -0
  253. claude_mpm/utils/robust_installer.py +3 -5
  254. claude_mpm/utils/structured_questions.py +619 -0
  255. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/METADATA +429 -59
  256. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/RECORD +264 -427
  257. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -17
  258. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +0 -3
  259. claude_mpm/agents/templates/agent-manager.json +0 -273
  260. claude_mpm/agents/templates/agentic-coder-optimizer.json +0 -248
  261. claude_mpm/agents/templates/api_qa.json +0 -180
  262. claude_mpm/agents/templates/clerk-ops.json +0 -235
  263. claude_mpm/agents/templates/code_analyzer.json +0 -101
  264. claude_mpm/agents/templates/content-agent.json +0 -358
  265. claude_mpm/agents/templates/dart_engineer.json +0 -307
  266. claude_mpm/agents/templates/data_engineer.json +0 -225
  267. claude_mpm/agents/templates/documentation.json +0 -211
  268. claude_mpm/agents/templates/engineer.json +0 -210
  269. claude_mpm/agents/templates/gcp_ops_agent.json +0 -253
  270. claude_mpm/agents/templates/golang_engineer.json +0 -270
  271. claude_mpm/agents/templates/imagemagick.json +0 -264
  272. claude_mpm/agents/templates/java_engineer.json +0 -346
  273. claude_mpm/agents/templates/local_ops_agent.json +0 -1840
  274. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +0 -39
  275. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250901_010124_142.md +0 -400
  276. claude_mpm/agents/templates/memory_manager.json +0 -158
  277. claude_mpm/agents/templates/nextjs_engineer.json +0 -285
  278. claude_mpm/agents/templates/ops.json +0 -185
  279. claude_mpm/agents/templates/php-engineer.json +0 -287
  280. claude_mpm/agents/templates/product_owner.json +0 -338
  281. claude_mpm/agents/templates/project_organizer.json +0 -140
  282. claude_mpm/agents/templates/prompt-engineer.json +0 -737
  283. claude_mpm/agents/templates/python_engineer.json +0 -387
  284. claude_mpm/agents/templates/qa.json +0 -242
  285. claude_mpm/agents/templates/react_engineer.json +0 -238
  286. claude_mpm/agents/templates/refactoring_engineer.json +0 -276
  287. claude_mpm/agents/templates/research.json +0 -188
  288. claude_mpm/agents/templates/ruby-engineer.json +0 -280
  289. claude_mpm/agents/templates/rust_engineer.json +0 -275
  290. claude_mpm/agents/templates/security.json +0 -202
  291. claude_mpm/agents/templates/svelte-engineer.json +0 -225
  292. claude_mpm/agents/templates/ticketing.json +0 -177
  293. claude_mpm/agents/templates/typescript_engineer.json +0 -285
  294. claude_mpm/agents/templates/vercel_ops_agent.json +0 -412
  295. claude_mpm/agents/templates/version_control.json +0 -157
  296. claude_mpm/agents/templates/web_qa.json +0 -399
  297. claude_mpm/agents/templates/web_ui.json +0 -189
  298. claude_mpm/commands/mpm-tickets.md +0 -102
  299. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +0 -1
  300. claude_mpm/dashboard/react/components/DataInspector/DataInspector.module.css +0 -188
  301. claude_mpm/dashboard/react/components/EventViewer/EventViewer.module.css +0 -156
  302. claude_mpm/dashboard/react/components/shared/ConnectionStatus.module.css +0 -38
  303. claude_mpm/dashboard/react/components/shared/FilterBar.module.css +0 -92
  304. claude_mpm/dashboard/static/archive/activity_dashboard_fixed.html +0 -248
  305. claude_mpm/dashboard/static/archive/activity_dashboard_test.html +0 -61
  306. claude_mpm/dashboard/static/archive/test_activity_connection.html +0 -179
  307. claude_mpm/dashboard/static/archive/test_claude_tree_tab.html +0 -68
  308. claude_mpm/dashboard/static/archive/test_dashboard.html +0 -409
  309. claude_mpm/dashboard/static/archive/test_dashboard_fixed.html +0 -519
  310. claude_mpm/dashboard/static/archive/test_dashboard_verification.html +0 -181
  311. claude_mpm/dashboard/static/archive/test_file_data.html +0 -315
  312. claude_mpm/dashboard/static/archive/test_file_tree_empty_state.html +0 -243
  313. claude_mpm/dashboard/static/archive/test_file_tree_fix.html +0 -234
  314. claude_mpm/dashboard/static/archive/test_file_tree_rename.html +0 -117
  315. claude_mpm/dashboard/static/archive/test_file_tree_tab.html +0 -115
  316. claude_mpm/dashboard/static/archive/test_file_viewer.html +0 -224
  317. claude_mpm/dashboard/static/archive/test_final_activity.html +0 -220
  318. claude_mpm/dashboard/static/archive/test_tab_fix.html +0 -139
  319. claude_mpm/dashboard/static/built/assets/events.DjpNxWNo.css +0 -1
  320. claude_mpm/dashboard/static/built/components/activity-tree.js +0 -2
  321. claude_mpm/dashboard/static/built/components/agent-hierarchy.js +0 -777
  322. claude_mpm/dashboard/static/built/components/agent-inference.js +0 -2
  323. claude_mpm/dashboard/static/built/components/build-tracker.js +0 -333
  324. claude_mpm/dashboard/static/built/components/code-simple.js +0 -857
  325. claude_mpm/dashboard/static/built/components/code-tree/tree-breadcrumb.js +0 -353
  326. claude_mpm/dashboard/static/built/components/code-tree/tree-constants.js +0 -235
  327. claude_mpm/dashboard/static/built/components/code-tree/tree-search.js +0 -409
  328. claude_mpm/dashboard/static/built/components/code-tree/tree-utils.js +0 -435
  329. claude_mpm/dashboard/static/built/components/code-tree.js +0 -2
  330. claude_mpm/dashboard/static/built/components/code-viewer.js +0 -2
  331. claude_mpm/dashboard/static/built/components/connection-debug.js +0 -654
  332. claude_mpm/dashboard/static/built/components/diff-viewer.js +0 -891
  333. claude_mpm/dashboard/static/built/components/event-processor.js +0 -2
  334. claude_mpm/dashboard/static/built/components/event-viewer.js +0 -2
  335. claude_mpm/dashboard/static/built/components/export-manager.js +0 -2
  336. claude_mpm/dashboard/static/built/components/file-change-tracker.js +0 -443
  337. claude_mpm/dashboard/static/built/components/file-change-viewer.js +0 -690
  338. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +0 -2
  339. claude_mpm/dashboard/static/built/components/file-viewer.js +0 -2
  340. claude_mpm/dashboard/static/built/components/hud-library-loader.js +0 -2
  341. claude_mpm/dashboard/static/built/components/hud-manager.js +0 -2
  342. claude_mpm/dashboard/static/built/components/hud-visualizer.js +0 -2
  343. claude_mpm/dashboard/static/built/components/module-viewer.js +0 -2
  344. claude_mpm/dashboard/static/built/components/nav-bar.js +0 -145
  345. claude_mpm/dashboard/static/built/components/page-structure.js +0 -429
  346. claude_mpm/dashboard/static/built/components/session-manager.js +0 -2
  347. claude_mpm/dashboard/static/built/components/socket-manager.js +0 -2
  348. claude_mpm/dashboard/static/built/components/ui-state-manager.js +0 -2
  349. claude_mpm/dashboard/static/built/components/unified-data-viewer.js +0 -2
  350. claude_mpm/dashboard/static/built/components/working-directory.js +0 -2
  351. claude_mpm/dashboard/static/built/connection-manager.js +0 -536
  352. claude_mpm/dashboard/static/built/dashboard.js +0 -2
  353. claude_mpm/dashboard/static/built/extension-error-handler.js +0 -164
  354. claude_mpm/dashboard/static/built/react/events.js +0 -30
  355. claude_mpm/dashboard/static/built/shared/dom-helpers.js +0 -396
  356. claude_mpm/dashboard/static/built/shared/event-bus.js +0 -330
  357. claude_mpm/dashboard/static/built/shared/event-filter-service.js +0 -540
  358. claude_mpm/dashboard/static/built/shared/logger.js +0 -385
  359. claude_mpm/dashboard/static/built/shared/page-structure.js +0 -249
  360. claude_mpm/dashboard/static/built/shared/tooltip-service.js +0 -253
  361. claude_mpm/dashboard/static/built/socket-client.js +0 -2
  362. claude_mpm/dashboard/static/built/tab-isolation-fix.js +0 -185
  363. claude_mpm/dashboard/static/dist/assets/events.DjpNxWNo.css +0 -1
  364. claude_mpm/dashboard/static/dist/components/activity-tree.js +0 -2
  365. claude_mpm/dashboard/static/dist/components/agent-inference.js +0 -2
  366. claude_mpm/dashboard/static/dist/components/code-tree.js +0 -2
  367. claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
  368. claude_mpm/dashboard/static/dist/components/event-processor.js +0 -2
  369. claude_mpm/dashboard/static/dist/components/event-viewer.js +0 -2
  370. claude_mpm/dashboard/static/dist/components/export-manager.js +0 -2
  371. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +0 -2
  372. claude_mpm/dashboard/static/dist/components/file-viewer.js +0 -2
  373. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +0 -2
  374. claude_mpm/dashboard/static/dist/components/hud-manager.js +0 -2
  375. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +0 -2
  376. claude_mpm/dashboard/static/dist/components/module-viewer.js +0 -2
  377. claude_mpm/dashboard/static/dist/components/session-manager.js +0 -2
  378. claude_mpm/dashboard/static/dist/components/socket-manager.js +0 -2
  379. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +0 -2
  380. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +0 -2
  381. claude_mpm/dashboard/static/dist/components/working-directory.js +0 -2
  382. claude_mpm/dashboard/static/dist/dashboard.js +0 -2
  383. claude_mpm/dashboard/static/dist/react/events.js +0 -30
  384. claude_mpm/dashboard/static/dist/socket-client.js +0 -2
  385. claude_mpm/dashboard/static/events.html +0 -607
  386. claude_mpm/dashboard/static/index.html +0 -635
  387. claude_mpm/dashboard/static/js/shared/dom-helpers.js +0 -396
  388. claude_mpm/dashboard/static/js/shared/event-bus.js +0 -330
  389. claude_mpm/dashboard/static/js/shared/logger.js +0 -385
  390. claude_mpm/dashboard/static/js/shared/tooltip-service.js +0 -253
  391. claude_mpm/dashboard/static/js/stores/dashboard-store.js +0 -562
  392. claude_mpm/dashboard/static/legacy/activity.html +0 -736
  393. claude_mpm/dashboard/static/legacy/agents.html +0 -786
  394. claude_mpm/dashboard/static/legacy/files.html +0 -747
  395. claude_mpm/dashboard/static/legacy/tools.html +0 -831
  396. claude_mpm/dashboard/static/monitors.html +0 -431
  397. claude_mpm/dashboard/static/production/events.html +0 -659
  398. claude_mpm/dashboard/static/production/main.html +0 -698
  399. claude_mpm/dashboard/static/production/monitors.html +0 -483
  400. claude_mpm/dashboard/static/test-archive/dashboard.html +0 -635
  401. claude_mpm/dashboard/static/test-archive/debug-events.html +0 -147
  402. claude_mpm/dashboard/static/test-archive/test-navigation.html +0 -256
  403. claude_mpm/dashboard/static/test-archive/test-react-exports.html +0 -180
  404. claude_mpm/dashboard/static/test-archive/test_debug.html +0 -25
  405. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +0 -79
  406. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +0 -178
  407. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +0 -577
  408. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +0 -467
  409. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +0 -537
  410. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +0 -730
  411. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +0 -112
  412. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +0 -146
  413. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +0 -412
  414. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +0 -81
  415. claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +0 -362
  416. claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +0 -312
  417. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +0 -152
  418. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +0 -668
  419. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +0 -587
  420. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +0 -438
  421. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +0 -391
  422. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +0 -119
  423. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +0 -148
  424. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +0 -483
  425. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +0 -452
  426. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +0 -449
  427. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +0 -411
  428. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +0 -14
  429. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +0 -58
  430. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +0 -68
  431. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +0 -69
  432. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +0 -131
  433. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +0 -325
  434. claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +0 -490
  435. claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +0 -425
  436. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +0 -499
  437. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +0 -86
  438. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +0 -43
  439. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +0 -47
  440. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +0 -65
  441. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +0 -30
  442. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +0 -16
  443. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +0 -160
  444. claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +0 -412
  445. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +0 -602
  446. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +0 -915
  447. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +0 -916
  448. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +0 -752
  449. claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +0 -1237
  450. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +0 -189
  451. claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +0 -500
  452. claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +0 -464
  453. claude_mpm/skills/bundled/main/skill-creator/references/examples.md +0 -619
  454. claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +0 -437
  455. claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +0 -231
  456. claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +0 -170
  457. claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +0 -602
  458. claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +0 -821
  459. claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +0 -742
  460. claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +0 -726
  461. claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +0 -764
  462. claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +0 -831
  463. claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +0 -226
  464. claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +0 -901
  465. claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +0 -901
  466. claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +0 -775
  467. claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +0 -937
  468. claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +0 -770
  469. claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +0 -961
  470. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +0 -119
  471. claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +0 -253
  472. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +0 -145
  473. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +0 -543
  474. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +0 -741
  475. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +0 -470
  476. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +0 -458
  477. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +0 -639
  478. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +0 -140
  479. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +0 -572
  480. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +0 -411
  481. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +0 -569
  482. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +0 -695
  483. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +0 -184
  484. claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +0 -459
  485. claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +0 -479
  486. claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +0 -687
  487. claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +0 -758
  488. claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +0 -868
  489. claude_mpm/tools/code_tree_analyzer.py +0 -1825
  490. /claude_mpm/agents/templates/{git_file_tracking.md → git-file-tracking.md} +0 -0
  491. /claude_mpm/agents/templates/{pm_examples.md → pm-examples.md} +0 -0
  492. /claude_mpm/agents/templates/{response_format.md → response-format.md} +0 -0
  493. /claude_mpm/agents/templates/{validation_templates.md → validation-templates.md} +0 -0
  494. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/WHEEL +0 -0
  495. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/entry_points.txt +0 -0
  496. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/licenses/LICENSE +0 -0
  497. {claude_mpm-4.21.0.dist-info → claude_mpm-5.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1055 @@
1
+ """Git Source Sync Service for agent templates.
2
+
3
+ Syncs agent markdown files from remote Git repositories (GitHub) using
4
+ ETag-based caching and SQLite state tracking for efficient updates.
5
+ Implements Stage 1 of the three-stage sync algorithm:
6
+ - Check repository for updates using ETag headers
7
+ - Download agent files via raw.githubusercontent.com URLs
8
+ - Track content with SHA-256 hashes and sync history in SQLite
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+ import requests
19
+
20
+ from claude_mpm.core.file_utils import get_file_hash
21
+ from claude_mpm.services.agents.sources.agent_sync_state import AgentSyncState
22
+ from claude_mpm.utils.progress import create_progress_bar
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class GitSyncError(Exception):
28
+ """Base exception for git sync errors."""
29
+
30
+
31
+ class NetworkError(GitSyncError):
32
+ """Network/HTTP errors."""
33
+
34
+
35
+ class CacheError(GitSyncError):
36
+ """Cache read/write errors."""
37
+
38
+
39
+ class ETagCache:
40
+ """Manages ETag storage for efficient HTTP caching.
41
+
42
+ Design Decision: Simple JSON file-based cache for ETag storage
43
+
44
+ Rationale: ETags are small text strings that change infrequently.
45
+ JSON provides human-readable format for debugging and is sufficient
46
+ for this use case. Rejected SQLite as it adds complexity without
47
+ significant benefits for this simple key-value storage.
48
+
49
+ Trade-offs:
50
+ - Simplicity: JSON is simple and debuggable
51
+ - Performance: File I/O is fast enough for <100 ETags
52
+ - Scalability: Limited to ~1000s of ETags before performance degrades
53
+
54
+ Extension Points: Can be replaced with SQLite if ETag count exceeds
55
+ performance threshold (>1000 agents syncing).
56
+ """
57
+
58
+ def __init__(self, cache_file: Path):
59
+ """Initialize ETag cache.
60
+
61
+ Args:
62
+ cache_file: Path to JSON file storing ETags
63
+ """
64
+ self._cache_file = cache_file
65
+ self._cache: Dict[str, Dict[str, Any]] = self._load_cache()
66
+
67
+ def get_etag(self, url: str) -> Optional[str]:
68
+ """Retrieve stored ETag for URL.
69
+
70
+ Args:
71
+ url: URL to look up ETag for
72
+
73
+ Returns:
74
+ ETag string or None if not found
75
+ """
76
+ entry = self._cache.get(url, {})
77
+ return entry.get("etag")
78
+
79
+ def set_etag(self, url: str, etag: str, file_size: Optional[int] = None):
80
+ """Store ETag for URL.
81
+
82
+ Args:
83
+ url: URL to store ETag for
84
+ etag: ETag value to store
85
+ file_size: Optional file size in bytes
86
+ """
87
+ self._cache[url] = {
88
+ "etag": etag,
89
+ "last_modified": datetime.now(timezone.utc).isoformat(),
90
+ "file_size": file_size,
91
+ }
92
+ self._save_cache()
93
+
94
+ def _load_cache(self) -> Dict[str, Dict[str, Any]]:
95
+ """Load ETag cache from JSON file.
96
+
97
+ Returns:
98
+ Dictionary mapping URLs to ETag metadata
99
+
100
+ Error Handling:
101
+ - FileNotFoundError: Returns empty dict (first run)
102
+ - JSONDecodeError: Logs warning and returns empty dict
103
+ - PermissionError: Logs error and returns empty dict
104
+ """
105
+ if not self._cache_file.exists():
106
+ return {}
107
+
108
+ try:
109
+ with self._cache_file.open() as f:
110
+ return json.load(f)
111
+ except json.JSONDecodeError:
112
+ logger.warning(f"Invalid ETag cache file: {self._cache_file}, resetting")
113
+ return {}
114
+ except PermissionError as e:
115
+ logger.error(f"Permission denied reading ETag cache: {e}")
116
+ return {}
117
+ except Exception as e:
118
+ logger.error(f"Error loading ETag cache: {e}")
119
+ return {}
120
+
121
+ def _save_cache(self):
122
+ """Persist ETag cache to JSON file.
123
+
124
+ Error Handling:
125
+ - PermissionError: Logs error but doesn't raise (cache is optional)
126
+ - IOError: Logs error but doesn't raise (graceful degradation)
127
+
128
+ Failure Mode: If cache write fails, next sync will re-download
129
+ all files (inefficient but correct behavior).
130
+ """
131
+ try:
132
+ # Ensure parent directory exists
133
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
134
+
135
+ with self._cache_file.open("w") as f:
136
+ json.dump(self._cache, f, indent=2)
137
+ except PermissionError as e:
138
+ logger.error(f"Permission denied writing ETag cache: {e}")
139
+ except OSError as e:
140
+ logger.error(f"IO error writing ETag cache: {e}")
141
+ except Exception as e:
142
+ logger.error(f"Error saving ETag cache: {e}")
143
+
144
+
145
+ class GitSourceSyncService:
146
+ """Service for syncing agent templates from remote Git repositories.
147
+
148
+ Design Decision: Use raw.githubusercontent.com URLs instead of Git API
149
+
150
+ Rationale: Raw URLs bypass GitHub API rate limits (60/hour unauthenticated,
151
+ 5000/hour authenticated). For agent files, direct raw access is sufficient
152
+ and more reliable. Rejected Git API because it requires base64 decoding
153
+ and consumes rate limit unnecessarily.
154
+
155
+ Trade-offs:
156
+ - Performance: Raw URLs have no rate limit, instant access
157
+ - Simplicity: Direct HTTP GET, no JSON parsing or base64 decoding
158
+ - Discovery: Cannot auto-discover agent list (requires manifest or hardcoded)
159
+ - Metadata: No commit info, file size, or last modified date
160
+
161
+ Optimization Opportunities:
162
+ 1. Async Downloads: Use aiohttp for parallel agent downloads
163
+ - Estimated speedup: 5-10x for initial sync (10 agents)
164
+ - Effort: 4-6 hours, medium complexity
165
+ - Threshold: Implement when agent count >20
166
+
167
+ 2. Manifest File: Add agents.json to repository for auto-discovery
168
+ - Removes hardcoded agent list
169
+ - Effort: 2 hours
170
+ - Blocks: Requires repository write access
171
+
172
+ Performance:
173
+ - Time Complexity: O(n) where n = number of agents
174
+ - Space Complexity: O(n) for in-memory agent content during sync
175
+ - Expected Performance:
176
+ * First sync (10 agents): ~5-10 seconds
177
+ * Subsequent sync (no changes): ~1-2 seconds (ETag checks only)
178
+ * Partial update (2 of 10 changed): ~2-3 seconds
179
+ """
180
+
181
+ def __init__(
182
+ self,
183
+ source_url: str = "https://raw.githubusercontent.com/bobmatnyc/claude-mpm-agents/main/agents",
184
+ cache_dir: Optional[Path] = None,
185
+ source_id: str = "github-remote",
186
+ ):
187
+ """Initialize Git source sync service.
188
+
189
+ Args:
190
+ source_url: Base URL for raw files (without trailing slash)
191
+ cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/agents/)
192
+ source_id: Unique identifier for this source (for multi-source support)
193
+
194
+ Design Decision: Cache to ~/.claude-mpm/cache/agents/ (Phase 1 of refactoring)
195
+
196
+ Rationale: Separates cached repository structure from deployed agents.
197
+ This allows preserving nested directory structure in cache while
198
+ flattening for deployment. Enables multiple deployment targets
199
+ (user, project) from single cache source.
200
+
201
+ Trade-offs:
202
+ - Storage: Uses 2x disk space (cache + deployment)
203
+ - Performance: Copy operation on deployment (~10ms for 50 agents)
204
+ - Flexibility: Supports project-specific deployments
205
+ - Migration: Requires one-time migration from old cache location
206
+ """
207
+ self.source_url = source_url.rstrip("/")
208
+ self.source_id = source_id
209
+
210
+ # Setup cache directory (Phase 1: Changed to ~/.claude-mpm/cache/agents/)
211
+ if cache_dir:
212
+ self.cache_dir = Path(cache_dir)
213
+ else:
214
+ # Default to ~/.claude-mpm/cache/agents/ (shared cache for nested structure)
215
+ home = Path.home()
216
+ self.cache_dir = home / ".claude-mpm" / "cache" / "agents"
217
+
218
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
219
+
220
+ # Setup HTTP session with connection pooling
221
+ self.session = requests.Session()
222
+ self.session.headers["Accept"] = "text/plain"
223
+
224
+ # Initialize SQLite state tracking (NEW)
225
+ self.sync_state = AgentSyncState()
226
+
227
+ # Register this source
228
+ self.sync_state.register_source(
229
+ source_id=self.source_id, url=self.source_url, enabled=True
230
+ )
231
+
232
+ # Initialize ETag cache (DEPRECATED - kept for backward compatibility)
233
+ etag_cache_file = self.cache_dir / ".etag-cache.json"
234
+ self.etag_cache = ETagCache(etag_cache_file)
235
+
236
+ # Migrate old ETag cache to SQLite if it exists
237
+ if etag_cache_file.exists():
238
+ self._migrate_etag_cache(etag_cache_file)
239
+
240
+ def sync_agents(
241
+ self,
242
+ force_refresh: bool = False,
243
+ show_progress: bool = True,
244
+ progress_prefix: str = "Syncing agents",
245
+ progress_suffix: str = "agents",
246
+ ) -> Dict[str, Any]:
247
+ """Sync agents from remote Git repository with SQLite state tracking.
248
+
249
+ Args:
250
+ force_refresh: Force download even if cache is fresh (bypasses ETag)
251
+ show_progress: Show ASCII progress bar during sync (default: True, auto-detects TTY)
252
+ progress_prefix: Custom prefix for progress bar (default: "Syncing agents")
253
+ progress_suffix: Custom suffix for completion message (default: "agents")
254
+
255
+ Returns:
256
+ Dictionary with sync results:
257
+ {
258
+ "synced": ["agent1.md", "agent2.md"], # New downloads
259
+ "cached": ["agent3.md"], # ETag 304 responses
260
+ "failed": [], # Failed downloads
261
+ "total_downloaded": 2,
262
+ "cache_hits": 1
263
+ }
264
+
265
+ Error Handling:
266
+ - Network errors: Individual agent failures don't stop sync
267
+ - Failed agents added to "failed" list
268
+ - Returns partial success if some agents sync successfully
269
+ """
270
+ logger.info(f"Starting agent sync from {self.source_url}")
271
+ logger.debug(f"Cache directory: {self.cache_dir}")
272
+ logger.debug(f"Force refresh: {force_refresh}")
273
+
274
+ start_time = time.time()
275
+
276
+ results = {
277
+ "synced": [],
278
+ "cached": [],
279
+ "failed": [],
280
+ "total_downloaded": 0,
281
+ "cache_hits": 0,
282
+ }
283
+
284
+ # Get list of agents to sync
285
+ agent_list = self._get_agent_list()
286
+
287
+ # Create progress bar if enabled
288
+ progress_bar = None
289
+ if show_progress:
290
+ progress_bar = create_progress_bar(
291
+ total=len(agent_list), prefix=progress_prefix
292
+ )
293
+
294
+ for idx, agent_filename in enumerate(agent_list, start=1):
295
+ try:
296
+ # Update progress bar with current file
297
+ if progress_bar:
298
+ progress_bar.update(idx, message=agent_filename)
299
+
300
+ url = f"{self.source_url}/{agent_filename}"
301
+ content, status = self._fetch_with_etag(url, force_refresh)
302
+
303
+ if status == 200:
304
+ # New content downloaded - save and track
305
+ self._save_to_cache(agent_filename, content)
306
+
307
+ # Track file with content hash in SQLite
308
+ cache_file = self.cache_dir / agent_filename
309
+ content_sha = get_file_hash(cache_file, algorithm="sha256")
310
+ if content_sha:
311
+ self.sync_state.track_file(
312
+ source_id=self.source_id,
313
+ file_path=agent_filename,
314
+ content_sha=content_sha,
315
+ local_path=str(cache_file),
316
+ file_size=len(content.encode("utf-8")),
317
+ )
318
+
319
+ results["synced"].append(agent_filename)
320
+ results["total_downloaded"] += 1
321
+ logger.debug(f"Downloaded: {agent_filename}")
322
+
323
+ elif status == 304:
324
+ # Not modified - verify hash
325
+ cache_file = self.cache_dir / agent_filename
326
+ if cache_file.exists():
327
+ current_sha = get_file_hash(cache_file, algorithm="sha256")
328
+ if current_sha and self.sync_state.has_file_changed(
329
+ self.source_id, agent_filename, current_sha
330
+ ):
331
+ # Hash mismatch - re-download
332
+ logger.warning(
333
+ f"Hash mismatch for {agent_filename}, re-downloading"
334
+ )
335
+ content, _ = self._fetch_with_etag(url, force_refresh=True)
336
+ if content:
337
+ self._save_to_cache(agent_filename, content)
338
+ # Re-calculate and track hash
339
+ new_sha = get_file_hash(cache_file, algorithm="sha256")
340
+ if new_sha:
341
+ self.sync_state.track_file(
342
+ source_id=self.source_id,
343
+ file_path=agent_filename,
344
+ content_sha=new_sha,
345
+ local_path=str(cache_file),
346
+ file_size=len(content.encode("utf-8")),
347
+ )
348
+ results["synced"].append(agent_filename)
349
+ results["total_downloaded"] += 1
350
+ else:
351
+ results["failed"].append(agent_filename)
352
+ else:
353
+ # Hash matches - true cache hit
354
+ results["cached"].append(agent_filename)
355
+ results["cache_hits"] += 1
356
+ logger.debug(f"Cache hit: {agent_filename}")
357
+ else:
358
+ # Cache file missing - re-download
359
+ logger.warning(
360
+ f"Cache file missing for {agent_filename}, re-downloading"
361
+ )
362
+ content, _ = self._fetch_with_etag(url, force_refresh=True)
363
+ if content:
364
+ self._save_to_cache(agent_filename, content)
365
+ # Track hash
366
+ current_sha = get_file_hash(cache_file, algorithm="sha256")
367
+ if current_sha:
368
+ self.sync_state.track_file(
369
+ source_id=self.source_id,
370
+ file_path=agent_filename,
371
+ content_sha=current_sha,
372
+ local_path=str(cache_file),
373
+ file_size=len(content.encode("utf-8")),
374
+ )
375
+ results["synced"].append(agent_filename)
376
+ results["total_downloaded"] += 1
377
+ else:
378
+ results["failed"].append(agent_filename)
379
+
380
+ else:
381
+ # Error status
382
+ logger.warning(f"Unexpected status {status} for {agent_filename}")
383
+ results["failed"].append(agent_filename)
384
+
385
+ except requests.RequestException as e:
386
+ logger.error(f"Network error downloading {agent_filename}: {e}")
387
+ results["failed"].append(agent_filename)
388
+ # Continue with other agents
389
+ except Exception as e:
390
+ logger.error(f"Unexpected error for {agent_filename}: {e}")
391
+ results["failed"].append(agent_filename)
392
+
393
+ # Record sync result in history
394
+ duration_ms = int((time.time() - start_time) * 1000)
395
+ status = (
396
+ "success"
397
+ if not results["failed"]
398
+ else ("partial" if results["synced"] or results["cached"] else "error")
399
+ )
400
+
401
+ self.sync_state.record_sync_result(
402
+ source_id=self.source_id,
403
+ status=status,
404
+ files_synced=results["total_downloaded"],
405
+ files_cached=results["cache_hits"],
406
+ files_failed=len(results["failed"]),
407
+ duration_ms=duration_ms,
408
+ )
409
+
410
+ # Update source metadata
411
+ self.sync_state.update_source_sync_metadata(source_id=self.source_id)
412
+
413
+ # Finish progress bar with clear breakdown
414
+ if progress_bar:
415
+ downloaded = results["total_downloaded"]
416
+ cached = results["cache_hits"]
417
+ total = downloaded + cached
418
+ failed_count = len(results["failed"])
419
+
420
+ if failed_count > 0:
421
+ progress_bar.finish(
422
+ message=f"Complete: {downloaded} downloaded, {cached} cached, {failed_count} failed ({total} total)"
423
+ )
424
+ # Show breakdown to clarify only changed files were downloaded
425
+ elif cached > 0:
426
+ progress_bar.finish(
427
+ message=f"Complete: {downloaded} downloaded, {cached} cached ({total} total)"
428
+ )
429
+ else:
430
+ # All new downloads (first sync)
431
+ progress_bar.finish(
432
+ message=f"Complete: {downloaded} {progress_suffix} downloaded"
433
+ )
434
+
435
+ # Log summary
436
+ logger.info(
437
+ f"Sync complete: {results['total_downloaded']} downloaded, "
438
+ f"{results['cache_hits']} from cache, {len(results['failed'])} failed"
439
+ )
440
+
441
+ return results
442
+
443
+ def check_for_updates(self) -> Dict[str, bool]:
444
+ """Check if remote repository has updates using ETag.
445
+
446
+ Uses HEAD requests to check ETags without downloading content.
447
+
448
+ Returns:
449
+ Dictionary mapping agent filenames to update status:
450
+ {
451
+ "research.md": True, # Has updates
452
+ "engineer.md": False, # No updates (ETag matches)
453
+ }
454
+
455
+ Performance: ~1-2 seconds for 10 agents (HEAD requests only)
456
+ """
457
+ logger.info("Checking for agent updates")
458
+ updates = {}
459
+
460
+ agent_list = self._get_agent_list()
461
+
462
+ for agent_filename in agent_list:
463
+ try:
464
+ url = f"{self.source_url}/{agent_filename}"
465
+ cached_etag = self.etag_cache.get_etag(url)
466
+
467
+ # Use HEAD request to check ETag without downloading
468
+ response = self.session.head(url, timeout=30)
469
+
470
+ if response.status_code == 200:
471
+ remote_etag = response.headers.get("ETag")
472
+ has_update = remote_etag != cached_etag
473
+ updates[agent_filename] = has_update
474
+
475
+ if has_update:
476
+ logger.info(f"Update available: {agent_filename}")
477
+ else:
478
+ logger.warning(
479
+ f"Could not check {agent_filename}: HTTP {response.status_code}"
480
+ )
481
+ updates[agent_filename] = False
482
+
483
+ except requests.RequestException as e:
484
+ logger.error(f"Network error checking {agent_filename}: {e}")
485
+ updates[agent_filename] = False
486
+
487
+ return updates
488
+
489
+ def download_agent_file(self, filename: str) -> Optional[str]:
490
+ """Download single agent file with ETag caching.
491
+
492
+ Args:
493
+ filename: Agent filename (e.g., "research.md")
494
+
495
+ Returns:
496
+ Agent content as string, or None if download fails
497
+
498
+ Error Handling:
499
+ - Network errors: Returns None, logs error
500
+ - 404 Not Found: Returns None, logs warning
501
+ - Cache fallback: Attempts to load from cache on error
502
+ """
503
+ url = f"{self.source_url}/{filename}"
504
+
505
+ try:
506
+ content, status = self._fetch_with_etag(url)
507
+
508
+ if status == 200:
509
+ self._save_to_cache(filename, content)
510
+ return content
511
+ if status == 304:
512
+ # Load from cache
513
+ return self._load_from_cache(filename)
514
+ logger.warning(f"HTTP {status} for {filename}")
515
+ return None
516
+
517
+ except requests.RequestException as e:
518
+ logger.error(f"Network error downloading {filename}: {e}")
519
+ # Try cache fallback
520
+ return self._load_from_cache(filename)
521
+
522
+ def _fetch_with_etag(
523
+ self, url: str, force_refresh: bool = False
524
+ ) -> Tuple[Optional[str], int]:
525
+ """Fetch URL with ETag caching.
526
+
527
+ Design Decision: Use If-None-Match header for conditional requests
528
+
529
+ Rationale: ETag-based caching is standard HTTP pattern that GitHub
530
+ supports. Reduces bandwidth by 95%+ for unchanged files. Alternative
531
+ was Last-Modified timestamps, but ETags are more reliable for Git
532
+ content (commit hash based).
533
+
534
+ Args:
535
+ url: URL to fetch
536
+ force_refresh: Skip ETag check and force download
537
+
538
+ Returns:
539
+ Tuple of (content, status_code) where:
540
+ - status_code 200: New content downloaded
541
+ - status_code 304: Not modified (use cached)
542
+ - content is None on 304
543
+
544
+ Error Handling:
545
+ - Timeout: 30 second timeout, raises requests.Timeout
546
+ - Connection errors: Raises requests.ConnectionError
547
+ - HTTP errors (4xx, 5xx): Returns (None, status_code)
548
+ """
549
+ headers = {}
550
+
551
+ # Add ETag header if we have cached version and not forcing refresh
552
+ if not force_refresh:
553
+ cached_etag = self.etag_cache.get_etag(url)
554
+ if cached_etag:
555
+ headers["If-None-Match"] = cached_etag
556
+
557
+ response = self.session.get(url, headers=headers, timeout=30)
558
+
559
+ if response.status_code == 304:
560
+ # Not modified - use cached version
561
+ return None, 304
562
+
563
+ if response.status_code == 200:
564
+ # New content - update cache
565
+ content = response.text
566
+ etag = response.headers.get("ETag")
567
+ if etag:
568
+ file_size = len(content.encode("utf-8"))
569
+ self.etag_cache.set_etag(url, etag, file_size)
570
+ return content, 200
571
+
572
+ # Error status
573
+ return None, response.status_code
574
+
575
+ def _save_to_cache(self, filename: str, content: str):
576
+ """Save agent file to cache (Phase 1: preserves nested directory structure).
577
+
578
+ Design Decision: Preserve nested directory structure in cache
579
+
580
+ Rationale: Cache mirrors remote repository structure, allowing
581
+ proper organization and future features (e.g., category browsing).
582
+ Deployment layer flattens to .claude-mpm/agents/ for backward
583
+ compatibility.
584
+
585
+ Args:
586
+ filename: Agent file path (may include directories, e.g., "engineer/core/engineer.md")
587
+ content: File content
588
+
589
+ Error Handling:
590
+ - PermissionError: Logs error but doesn't raise
591
+ - IOError: Logs error but doesn't raise
592
+
593
+ Failure Mode: If cache write fails, agent is still synced in memory
594
+ but will need re-download on next sync (graceful degradation).
595
+ """
596
+ try:
597
+ cache_file = self.cache_dir / filename
598
+ # Create parent directories for nested structure
599
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
600
+ cache_file.write_text(content, encoding="utf-8")
601
+ logger.debug(f"Saved to cache: {filename}")
602
+ except PermissionError as e:
603
+ logger.error(f"Permission denied writing {filename}: {e}")
604
+ except OSError as e:
605
+ logger.error(f"IO error writing {filename}: {e}")
606
+ except Exception as e:
607
+ logger.error(f"Error saving {filename} to cache: {e}")
608
+
609
+ def _load_from_cache(self, filename: str) -> Optional[str]:
610
+ """Load agent file from cache.
611
+
612
+ Args:
613
+ filename: Agent filename
614
+
615
+ Returns:
616
+ Cached content or None if not found
617
+
618
+ Error Handling:
619
+ - FileNotFoundError: Returns None (not in cache)
620
+ - PermissionError: Logs error, returns None
621
+ - IOError: Logs error, returns None
622
+ """
623
+ cache_file = self.cache_dir / filename
624
+
625
+ if not cache_file.exists():
626
+ logger.debug(f"No cached version of {filename}")
627
+ return None
628
+
629
+ try:
630
+ content = cache_file.read_text(encoding="utf-8")
631
+ logger.debug(f"Loaded from cache: {filename}")
632
+ return content
633
+ except PermissionError as e:
634
+ logger.error(f"Permission denied reading {filename}: {e}")
635
+ return None
636
+ except OSError as e:
637
+ logger.error(f"IO error reading {filename}: {e}")
638
+ return None
639
+ except Exception as e:
640
+ logger.error(f"Error loading {filename} from cache: {e}")
641
+ return None
642
+
643
+ def _get_agent_list(self) -> List[str]:
644
+ """Get list of agent file paths to sync (including nested directories).
645
+
646
+ Design Decision: Use Git Tree API instead of Contents API (Phase 1 fix)
647
+
648
+ Rationale: Git Tree API with recursive=1 discovers entire repository
649
+ structure in a single request, solving the "1 agent discovered" issue.
650
+ Contents API only shows top-level files, missing nested directories.
651
+
652
+ Trade-offs:
653
+ - Performance: Single API call vs. 10-50+ recursive calls
654
+ - Rate Limits: 1 request vs. dozens (avoids 403 errors)
655
+ - Discovery: Finds ALL files in nested structure (50+ agents)
656
+ - API Complexity: Requires commit SHA lookup before tree fetch
657
+
658
+ Alternatives Considered:
659
+ 1. Contents API with recursion: 50+ API calls, hits rate limits
660
+ 2. Hardcoded nested paths: Misses new agents, unmaintainable
661
+ 3. Manifest file: Requires repository write access
662
+
663
+ Error Handling:
664
+ - Network errors: Falls back to static list
665
+ - Rate limit exceeded: Falls back to static list
666
+ - JSON parse errors: Falls back to static list
667
+
668
+ Returns:
669
+ List of agent file paths with directory structure
670
+ (e.g., ["research.md", "engineer/core/engineer.md", ...])
671
+ """
672
+ # Extract repository info from source URL
673
+ # URL format: https://raw.githubusercontent.com/owner/repo/branch/path
674
+ try:
675
+ # Parse GitHub URL to extract owner/repo/branch
676
+ url_parts = self.source_url.replace(
677
+ "https://raw.githubusercontent.com/", ""
678
+ ).split("/")
679
+
680
+ if len(url_parts) >= 3:
681
+ owner = url_parts[0]
682
+ repo = url_parts[1]
683
+ branch = url_parts[2]
684
+ base_path = "/".join(url_parts[3:]) if len(url_parts) > 3 else ""
685
+
686
+ logger.debug(
687
+ f"Discovering agents from {owner}/{repo}/{branch} via Git Tree API"
688
+ )
689
+
690
+ # Use Git Tree API for recursive discovery
691
+ agent_files = self._discover_agents_via_tree_api(
692
+ owner, repo, branch, base_path
693
+ )
694
+
695
+ if agent_files:
696
+ logger.info(
697
+ f"Discovered {len(agent_files)} agents via Git Tree API"
698
+ )
699
+ return sorted(agent_files)
700
+
701
+ logger.warning("No agent files found via Tree API, using fallback list")
702
+
703
+ except requests.RequestException as e:
704
+ logger.warning(
705
+ f"Network error fetching agent list from API: {e}, using fallback list"
706
+ )
707
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
708
+ logger.warning(
709
+ f"Error parsing GitHub API response: {e}, using fallback list"
710
+ )
711
+ except Exception as e:
712
+ logger.warning(
713
+ f"Unexpected error fetching agent list: {e}, using fallback list"
714
+ )
715
+
716
+ # Fallback to known agent list if API fails
717
+ logger.debug("Using fallback agent list")
718
+ return [
719
+ "research.md",
720
+ "engineer.md",
721
+ "qa.md",
722
+ "documentation.md",
723
+ "security.md",
724
+ "ops.md",
725
+ "ticketing.md",
726
+ "product_owner.md",
727
+ "version_control.md",
728
+ "project_organizer.md",
729
+ ]
730
+
731
+ def _discover_agents_via_tree_api(
732
+ self, owner: str, repo: str, branch: str, base_path: str = ""
733
+ ) -> List[str]:
734
+ """Discover all agent files using GitHub Git Tree API with recursion.
735
+
736
+ Design Decision: Two-step Tree API pattern (commit SHA → tree)
737
+
738
+ Rationale: Git Tree API requires commit SHA, not branch name.
739
+ Step 1 resolves branch to SHA, Step 2 fetches recursive tree.
740
+ This pattern is standard for GitHub API and handles branch
741
+ references correctly.
742
+
743
+ Algorithm:
744
+ 1. GET /repos/{owner}/{repo}/git/refs/heads/{branch} → commit SHA
745
+ 2. GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1 → all files
746
+ 3. Filter for .md/.json files in agents/ directory
747
+ 4. Exclude README.md and .gitignore
748
+
749
+ Args:
750
+ owner: GitHub owner (e.g., "bobmatnyc")
751
+ repo: Repository name (e.g., "claude-mpm-agents")
752
+ branch: Branch name (e.g., "main")
753
+ base_path: Base path prefix to filter (e.g., "agents")
754
+
755
+ Returns:
756
+ List of agent file paths relative to base_path
757
+ (e.g., ["research.md", "engineer/core/engineer.md"])
758
+
759
+ Error Handling:
760
+ - HTTP 404: Branch or repo not found, raises RequestException
761
+ - HTTP 403: Rate limit exceeded, raises RequestException
762
+ - Timeout: 30 second timeout, raises RequestException
763
+ - Empty tree: Returns empty list (logged as warning)
764
+
765
+ Performance:
766
+ - Time: ~500-800ms for 50+ agents (2 API calls)
767
+ - Rate Limit: Consumes 2 API calls per sync
768
+ - Scalability: Handles repositories with 1000s of files
769
+ """
770
+ # Step 1: Get commit SHA for branch
771
+ refs_url = (
772
+ f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch}"
773
+ )
774
+ logger.debug(f"Fetching commit SHA from {refs_url}")
775
+
776
+ refs_response = self.session.get(
777
+ refs_url, headers={"Accept": "application/vnd.github+json"}, timeout=30
778
+ )
779
+
780
+ if refs_response.status_code == 403:
781
+ logger.warning(
782
+ "GitHub API rate limit exceeded (HTTP 403). "
783
+ "Consider setting GITHUB_TOKEN environment variable."
784
+ )
785
+ raise requests.RequestException("Rate limit exceeded")
786
+
787
+ refs_response.raise_for_status()
788
+ commit_sha = refs_response.json()["object"]["sha"]
789
+ logger.debug(f"Resolved {branch} to commit {commit_sha[:8]}")
790
+
791
+ # Step 2: Get recursive tree for commit
792
+ tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{commit_sha}"
793
+ params = {"recursive": "1"} # Recursively fetch all files
794
+
795
+ logger.debug(f"Fetching recursive tree from {tree_url}")
796
+ tree_response = self.session.get(
797
+ tree_url,
798
+ headers={"Accept": "application/vnd.github+json"},
799
+ params=params,
800
+ timeout=30,
801
+ )
802
+ tree_response.raise_for_status()
803
+
804
+ tree_data = tree_response.json()
805
+ all_items = tree_data.get("tree", [])
806
+
807
+ logger.debug(f"Tree API returned {len(all_items)} total items")
808
+
809
+ # Step 3: Filter for agent files
810
+ agent_files = []
811
+ for item in all_items:
812
+ # Only process files (blobs), not directories (trees)
813
+ if item["type"] != "blob":
814
+ continue
815
+
816
+ path = item["path"]
817
+
818
+ # Filter for files in base_path (e.g., "agents/")
819
+ if base_path and not path.startswith(base_path + "/"):
820
+ continue
821
+
822
+ # Remove base_path prefix for relative paths
823
+ if base_path:
824
+ relative_path = path[len(base_path) + 1 :]
825
+ else:
826
+ relative_path = path
827
+
828
+ # Filter for .md or .json files, exclude README and .gitignore
829
+ if (
830
+ relative_path.endswith(".md") or relative_path.endswith(".json")
831
+ ) and relative_path not in ["README.md", ".gitignore"]:
832
+ agent_files.append(relative_path)
833
+
834
+ logger.debug(f"Filtered to {len(agent_files)} agent files")
835
+ return agent_files
836
+
837
+ def _migrate_etag_cache(self, cache_file: Path):
838
+ """Migrate old ETag cache to SQLite (one-time operation).
839
+
840
+ Args:
841
+ cache_file: Path to old JSON ETag cache file
842
+
843
+ Error Handling:
844
+ - Migration failures are logged but don't stop initialization
845
+ - Old cache is renamed to .migrated to prevent re-migration
846
+ """
847
+ try:
848
+ with cache_file.open() as f:
849
+ old_cache = json.load(f)
850
+
851
+ logger.info(f"Migrating {len(old_cache)} ETag entries to SQLite...")
852
+
853
+ migrated = 0
854
+ for url, metadata in old_cache.items():
855
+ try:
856
+ etag = metadata.get("etag")
857
+ if etag:
858
+ # Store in new system
859
+ self.sync_state.update_source_sync_metadata(
860
+ source_id=self.source_id, etag=etag
861
+ )
862
+ migrated += 1
863
+ except Exception as e:
864
+ logger.error(f"Failed to migrate {url}: {e}")
865
+
866
+ # Rename old cache to prevent re-migration
867
+ backup_file = cache_file.with_suffix(".json.migrated")
868
+ cache_file.rename(backup_file)
869
+
870
+ logger.info(
871
+ f"ETag cache migration complete: {migrated} entries migrated, "
872
+ f"old cache backed up to {backup_file.name}"
873
+ )
874
+
875
+ except json.JSONDecodeError as e:
876
+ logger.error(f"Invalid JSON in ETag cache, skipping migration: {e}")
877
+ except Exception as e:
878
+ logger.error(f"Failed to migrate ETag cache: {e}")
879
+
880
+ def get_cached_agents_dir(self) -> Path:
881
+ """Get directory containing cached agent files.
882
+
883
+ Returns:
884
+ Path to cache directory for integration with MultiSourceAgentDeploymentService
885
+ """
886
+ return self.cache_dir
887
+
888
+ def deploy_agents_to_project(
889
+ self,
890
+ project_dir: Path,
891
+ agent_list: Optional[List[str]] = None,
892
+ force: bool = False,
893
+ ) -> Dict[str, Any]:
894
+ """Deploy agents from cache to project directory (Phase 1 deployment).
895
+
896
+ Design Decision: Copy from cache to project-specific deployment directory
897
+
898
+ Rationale: Separates syncing (cache) from deployment (project-local).
899
+ Allows multiple projects to use same cache with different agent
900
+ configurations. Flattens nested structure for backward compatibility.
901
+
902
+ Trade-offs:
903
+ - Storage: 2x disk usage (cache + deployments)
904
+ - Performance: Copy operation ~10ms for 50 agents
905
+ - Isolation: Each project has independent agent set
906
+ - Flexibility: Can deploy subset of cached agents per project
907
+
908
+ Algorithm:
909
+ 1. Create deployment directory (.claude-mpm/agents/)
910
+ 2. Discover cached agents if list not provided
911
+ 3. For each agent, flatten path and copy to deployment
912
+ 4. Track deployment results (new, updated, skipped)
913
+
914
+ Args:
915
+ project_dir: Project root directory (e.g., /path/to/project)
916
+ agent_list: Optional list of agent paths to deploy (uses all if None)
917
+ force: Force redeployment even if up-to-date
918
+
919
+ Returns:
920
+ Dictionary with deployment results:
921
+ {
922
+ "deployed": ["engineer.md"], # Newly deployed
923
+ "updated": ["research.md"], # Updated existing
924
+ "skipped": ["qa.md"], # Already up-to-date
925
+ "failed": ["broken.md"], # Copy failures
926
+ "deployment_dir": "/path/.claude-mpm/agents"
927
+ }
928
+
929
+ Error Handling:
930
+ - Missing cache files: Logged and added to "failed" list
931
+ - Permission errors: Individual failures don't stop deployment
932
+ - Directory creation: Creates deployment directory if missing
933
+
934
+ Example:
935
+ >>> service = GitSourceSyncService()
936
+ >>> service.sync_agents() # Sync to cache first
937
+ >>> result = service.deploy_agents_to_project(Path("/my/project"))
938
+ >>> print(f"Deployed {len(result['deployed'])} agents")
939
+ """
940
+ import shutil
941
+
942
+ deployment_dir = project_dir / ".claude-mpm" / "agents"
943
+ deployment_dir.mkdir(parents=True, exist_ok=True)
944
+
945
+ results = {
946
+ "deployed": [],
947
+ "updated": [],
948
+ "skipped": [],
949
+ "failed": [],
950
+ "deployment_dir": str(deployment_dir),
951
+ }
952
+
953
+ # Get agents from cache or use provided list
954
+ if agent_list is None:
955
+ agent_list = self._discover_cached_agents()
956
+
957
+ logger.info(
958
+ f"Deploying {len(agent_list)} agents from cache to {deployment_dir}"
959
+ )
960
+
961
+ for agent_path in agent_list:
962
+ try:
963
+ cache_file = self.cache_dir / agent_path
964
+
965
+ if not cache_file.exists():
966
+ logger.warning(f"Cache file not found: {agent_path}")
967
+ results["failed"].append(agent_path)
968
+ continue
969
+
970
+ # Flatten nested path for deployment (engineer/core/engineer.md → engineer.md)
971
+ deploy_filename = Path(agent_path).name
972
+ deploy_file = deployment_dir / deploy_filename
973
+
974
+ # Check if update needed (compare modification times)
975
+ should_deploy = force
976
+ was_existing = deploy_file.exists()
977
+
978
+ if not force and was_existing:
979
+ cache_mtime = cache_file.stat().st_mtime
980
+ deploy_mtime = deploy_file.stat().st_mtime
981
+ should_deploy = cache_mtime > deploy_mtime
982
+
983
+ if not should_deploy and was_existing:
984
+ results["skipped"].append(deploy_filename)
985
+ logger.debug(f"Skipped (up-to-date): {deploy_filename}")
986
+ continue
987
+
988
+ # Copy from cache to deployment
989
+ shutil.copy2(cache_file, deploy_file)
990
+
991
+ # Track result
992
+ if deploy_file.exists():
993
+ if was_existing:
994
+ results["updated"].append(deploy_filename)
995
+ logger.info(f"Updated: {deploy_filename}")
996
+ else:
997
+ results["deployed"].append(deploy_filename)
998
+ logger.info(f"Deployed: {deploy_filename}")
999
+ else:
1000
+ results["failed"].append(deploy_filename)
1001
+ logger.error(f"Failed to deploy: {deploy_filename}")
1002
+
1003
+ except PermissionError as e:
1004
+ logger.error(f"Permission denied deploying {agent_path}: {e}")
1005
+ results["failed"].append(Path(agent_path).name)
1006
+ except OSError as e:
1007
+ logger.error(f"IO error deploying {agent_path}: {e}")
1008
+ results["failed"].append(Path(agent_path).name)
1009
+ except Exception as e:
1010
+ logger.error(f"Unexpected error deploying {agent_path}: {e}")
1011
+ results["failed"].append(Path(agent_path).name)
1012
+
1013
+ # Log summary
1014
+ total_success = len(results["deployed"]) + len(results["updated"])
1015
+ logger.info(
1016
+ f"Deployment complete: {total_success} deployed/updated, "
1017
+ f"{len(results['skipped'])} skipped, {len(results['failed'])} failed"
1018
+ )
1019
+
1020
+ return results
1021
+
1022
+ def _discover_cached_agents(self) -> List[str]:
1023
+ """Discover all agent files currently in cache.
1024
+
1025
+ Scans cache directory for .md and .json files, preserving
1026
+ nested directory structure in returned paths.
1027
+
1028
+ Returns:
1029
+ List of agent file paths relative to cache directory
1030
+ (e.g., ["research.md", "engineer/core/engineer.md"])
1031
+
1032
+ Algorithm:
1033
+ 1. Walk cache directory recursively
1034
+ 2. Find all .md and .json files
1035
+ 3. Convert to paths relative to cache root
1036
+ 4. Filter out README.md and .gitignore
1037
+ """
1038
+ cached_agents = []
1039
+
1040
+ if not self.cache_dir.exists():
1041
+ logger.warning(f"Cache directory does not exist: {self.cache_dir}")
1042
+ return []
1043
+
1044
+ for file_path in self.cache_dir.rglob("*"):
1045
+ if file_path.is_file() and file_path.suffix in {".md", ".json"}:
1046
+ # Get relative path from cache directory
1047
+ relative_path = file_path.relative_to(self.cache_dir)
1048
+ relative_str = str(relative_path)
1049
+
1050
+ # Exclude README and .gitignore
1051
+ if relative_str not in ["README.md", ".gitignore"]:
1052
+ cached_agents.append(relative_str)
1053
+
1054
+ logger.debug(f"Discovered {len(cached_agents)} cached agents")
1055
+ return sorted(cached_agents)