claude-mpm 3.9.11__py3-none-any.whl → 4.0.3__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 (419) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +2 -2
  3. claude_mpm/__main__.py +3 -2
  4. claude_mpm/agents/__init__.py +85 -79
  5. claude_mpm/agents/agent_loader.py +464 -1003
  6. claude_mpm/agents/agent_loader_integration.py +45 -45
  7. claude_mpm/agents/agents_metadata.py +29 -30
  8. claude_mpm/agents/async_agent_loader.py +156 -138
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/base_agent_loader.py +179 -151
  11. claude_mpm/agents/frontmatter_validator.py +229 -130
  12. claude_mpm/agents/schema/agent_schema.json +1 -1
  13. claude_mpm/agents/system_agent_config.py +213 -147
  14. claude_mpm/agents/templates/__init__.py +13 -13
  15. claude_mpm/agents/templates/code_analyzer.json +2 -2
  16. claude_mpm/agents/templates/data_engineer.json +1 -1
  17. claude_mpm/agents/templates/documentation.json +23 -11
  18. claude_mpm/agents/templates/engineer.json +22 -6
  19. claude_mpm/agents/templates/memory_manager.json +1 -1
  20. claude_mpm/agents/templates/ops.json +2 -2
  21. claude_mpm/agents/templates/project_organizer.json +1 -1
  22. claude_mpm/agents/templates/qa.json +1 -1
  23. claude_mpm/agents/templates/refactoring_engineer.json +222 -0
  24. claude_mpm/agents/templates/research.json +20 -14
  25. claude_mpm/agents/templates/security.json +1 -1
  26. claude_mpm/agents/templates/ticketing.json +1 -1
  27. claude_mpm/agents/templates/version_control.json +1 -1
  28. claude_mpm/agents/templates/web_qa.json +3 -1
  29. claude_mpm/agents/templates/web_ui.json +2 -2
  30. claude_mpm/cli/__init__.py +79 -51
  31. claude_mpm/cli/__main__.py +3 -2
  32. claude_mpm/cli/commands/__init__.py +20 -20
  33. claude_mpm/cli/commands/agents.py +279 -247
  34. claude_mpm/cli/commands/aggregate.py +138 -157
  35. claude_mpm/cli/commands/cleanup.py +147 -147
  36. claude_mpm/cli/commands/config.py +93 -76
  37. claude_mpm/cli/commands/info.py +17 -16
  38. claude_mpm/cli/commands/mcp.py +140 -905
  39. claude_mpm/cli/commands/mcp_command_router.py +139 -0
  40. claude_mpm/cli/commands/mcp_config_commands.py +20 -0
  41. claude_mpm/cli/commands/mcp_install_commands.py +20 -0
  42. claude_mpm/cli/commands/mcp_server_commands.py +175 -0
  43. claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
  44. claude_mpm/cli/commands/memory.py +239 -203
  45. claude_mpm/cli/commands/monitor.py +203 -81
  46. claude_mpm/cli/commands/run.py +380 -429
  47. claude_mpm/cli/commands/run_config_checker.py +160 -0
  48. claude_mpm/cli/commands/socketio_monitor.py +235 -0
  49. claude_mpm/cli/commands/tickets.py +305 -197
  50. claude_mpm/cli/parser.py +24 -1156
  51. claude_mpm/cli/parsers/__init__.py +29 -0
  52. claude_mpm/cli/parsers/agents_parser.py +136 -0
  53. claude_mpm/cli/parsers/base_parser.py +331 -0
  54. claude_mpm/cli/parsers/config_parser.py +85 -0
  55. claude_mpm/cli/parsers/mcp_parser.py +152 -0
  56. claude_mpm/cli/parsers/memory_parser.py +138 -0
  57. claude_mpm/cli/parsers/monitor_parser.py +104 -0
  58. claude_mpm/cli/parsers/run_parser.py +147 -0
  59. claude_mpm/cli/parsers/tickets_parser.py +203 -0
  60. claude_mpm/cli/ticket_cli.py +7 -3
  61. claude_mpm/cli/utils.py +55 -37
  62. claude_mpm/cli_module/__init__.py +6 -6
  63. claude_mpm/cli_module/args.py +188 -140
  64. claude_mpm/cli_module/commands.py +79 -70
  65. claude_mpm/cli_module/migration_example.py +38 -60
  66. claude_mpm/config/__init__.py +32 -25
  67. claude_mpm/config/agent_config.py +151 -119
  68. claude_mpm/config/experimental_features.py +71 -73
  69. claude_mpm/config/paths.py +94 -208
  70. claude_mpm/config/socketio_config.py +84 -73
  71. claude_mpm/constants.py +35 -18
  72. claude_mpm/core/__init__.py +9 -6
  73. claude_mpm/core/agent_name_normalizer.py +68 -71
  74. claude_mpm/core/agent_registry.py +372 -521
  75. claude_mpm/core/agent_session_manager.py +74 -63
  76. claude_mpm/core/base_service.py +116 -87
  77. claude_mpm/core/cache.py +119 -153
  78. claude_mpm/core/claude_runner.py +425 -1120
  79. claude_mpm/core/config.py +263 -168
  80. claude_mpm/core/config_aliases.py +69 -61
  81. claude_mpm/core/config_constants.py +292 -0
  82. claude_mpm/core/constants.py +57 -99
  83. claude_mpm/core/container.py +211 -178
  84. claude_mpm/core/exceptions.py +233 -89
  85. claude_mpm/core/factories.py +92 -54
  86. claude_mpm/core/framework_loader.py +378 -220
  87. claude_mpm/core/hook_manager.py +198 -83
  88. claude_mpm/core/hook_performance_config.py +136 -0
  89. claude_mpm/core/injectable_service.py +61 -55
  90. claude_mpm/core/interactive_session.py +165 -155
  91. claude_mpm/core/interfaces.py +221 -195
  92. claude_mpm/core/lazy.py +96 -96
  93. claude_mpm/core/logger.py +133 -107
  94. claude_mpm/core/logging_config.py +185 -157
  95. claude_mpm/core/minimal_framework_loader.py +20 -15
  96. claude_mpm/core/mixins.py +30 -29
  97. claude_mpm/core/oneshot_session.py +215 -181
  98. claude_mpm/core/optimized_agent_loader.py +134 -138
  99. claude_mpm/core/optimized_startup.py +159 -157
  100. claude_mpm/core/pm_hook_interceptor.py +85 -72
  101. claude_mpm/core/service_registry.py +103 -101
  102. claude_mpm/core/session_manager.py +97 -87
  103. claude_mpm/core/socketio_pool.py +212 -158
  104. claude_mpm/core/tool_access_control.py +58 -51
  105. claude_mpm/core/types.py +46 -24
  106. claude_mpm/core/typing_utils.py +166 -82
  107. claude_mpm/core/unified_agent_registry.py +721 -0
  108. claude_mpm/core/unified_config.py +550 -0
  109. claude_mpm/core/unified_paths.py +549 -0
  110. claude_mpm/dashboard/index.html +1 -1
  111. claude_mpm/dashboard/open_dashboard.py +51 -17
  112. claude_mpm/dashboard/static/css/dashboard.css +27 -8
  113. claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
  114. claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
  115. claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
  116. claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
  117. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
  118. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
  119. claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
  120. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
  121. claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
  122. claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
  123. claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
  124. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
  125. claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
  126. claude_mpm/dashboard/static/dist/dashboard.js +2 -0
  127. claude_mpm/dashboard/static/dist/socket-client.js +2 -0
  128. claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
  129. claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
  130. claude_mpm/dashboard/static/js/components/event-viewer.js +74 -70
  131. claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
  132. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +106 -92
  133. claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
  134. claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
  135. claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
  136. claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
  137. claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
  138. claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
  139. claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
  140. claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
  141. claude_mpm/dashboard/static/js/dashboard.js +178 -453
  142. claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
  143. claude_mpm/dashboard/static/js/socket-client.js +120 -54
  144. claude_mpm/dashboard/templates/index.html +40 -50
  145. claude_mpm/experimental/cli_enhancements.py +60 -58
  146. claude_mpm/generators/__init__.py +1 -1
  147. claude_mpm/generators/agent_profile_generator.py +75 -65
  148. claude_mpm/hooks/__init__.py +1 -1
  149. claude_mpm/hooks/base_hook.py +33 -28
  150. claude_mpm/hooks/claude_hooks/__init__.py +1 -1
  151. claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
  152. claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
  153. claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
  154. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
  155. claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
  156. claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
  157. claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
  158. claude_mpm/hooks/memory_integration_hook.py +140 -100
  159. claude_mpm/hooks/tool_call_interceptor.py +89 -76
  160. claude_mpm/hooks/validation_hooks.py +57 -49
  161. claude_mpm/init.py +145 -121
  162. claude_mpm/models/__init__.py +9 -9
  163. claude_mpm/models/agent_definition.py +33 -23
  164. claude_mpm/models/agent_session.py +228 -200
  165. claude_mpm/scripts/__init__.py +1 -1
  166. claude_mpm/scripts/socketio_daemon.py +192 -75
  167. claude_mpm/scripts/socketio_server_manager.py +328 -0
  168. claude_mpm/scripts/start_activity_logging.py +25 -22
  169. claude_mpm/services/__init__.py +68 -43
  170. claude_mpm/services/agent_capabilities_service.py +271 -0
  171. claude_mpm/services/agents/__init__.py +23 -32
  172. claude_mpm/services/agents/deployment/__init__.py +3 -3
  173. claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
  174. claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
  175. claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
  176. claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
  177. claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
  178. claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
  179. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
  180. claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
  181. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
  182. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
  183. claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
  184. claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
  185. claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
  186. claude_mpm/services/agents/deployment/agent_validator.py +352 -0
  187. claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
  188. claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
  189. claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
  190. claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
  191. claude_mpm/services/agents/deployment/config/__init__.py +13 -0
  192. claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
  193. claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
  194. claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
  195. claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
  196. claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
  197. claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
  198. claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
  199. claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
  200. claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
  201. claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
  202. claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
  203. claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
  204. claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
  205. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
  206. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
  207. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
  208. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
  209. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
  210. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
  211. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
  212. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
  213. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
  214. claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
  215. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
  216. claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
  217. claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
  218. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
  219. claude_mpm/services/agents/deployment/results/__init__.py +13 -0
  220. claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
  221. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
  222. claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
  223. claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
  224. claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
  225. claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
  226. claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
  227. claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
  228. claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
  229. claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
  230. claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
  231. claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
  232. claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
  233. claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
  234. claude_mpm/services/agents/loading/__init__.py +2 -2
  235. claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
  236. claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
  237. claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
  238. claude_mpm/services/agents/management/__init__.py +2 -2
  239. claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
  240. claude_mpm/services/agents/management/agent_management_service.py +209 -156
  241. claude_mpm/services/agents/memory/__init__.py +9 -6
  242. claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
  243. claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
  244. claude_mpm/services/agents/memory/analyzer.py +430 -0
  245. claude_mpm/services/agents/memory/content_manager.py +376 -0
  246. claude_mpm/services/agents/memory/template_generator.py +468 -0
  247. claude_mpm/services/agents/registry/__init__.py +7 -10
  248. claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
  249. claude_mpm/services/agents/registry/modification_tracker.py +351 -285
  250. claude_mpm/services/async_session_logger.py +187 -153
  251. claude_mpm/services/claude_session_logger.py +87 -72
  252. claude_mpm/services/command_handler_service.py +217 -0
  253. claude_mpm/services/communication/__init__.py +3 -2
  254. claude_mpm/services/core/__init__.py +50 -97
  255. claude_mpm/services/core/base.py +60 -53
  256. claude_mpm/services/core/interfaces/__init__.py +188 -0
  257. claude_mpm/services/core/interfaces/agent.py +351 -0
  258. claude_mpm/services/core/interfaces/communication.py +343 -0
  259. claude_mpm/services/core/interfaces/infrastructure.py +413 -0
  260. claude_mpm/services/core/interfaces/service.py +434 -0
  261. claude_mpm/services/core/interfaces.py +19 -944
  262. claude_mpm/services/event_aggregator.py +208 -170
  263. claude_mpm/services/exceptions.py +387 -308
  264. claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
  265. claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
  266. claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
  267. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
  268. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
  269. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
  270. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
  271. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
  272. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
  273. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
  274. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
  275. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
  276. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
  277. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
  278. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
  279. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
  280. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
  281. claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
  283. claude_mpm/services/hook_service.py +106 -114
  284. claude_mpm/services/infrastructure/__init__.py +7 -5
  285. claude_mpm/services/infrastructure/context_preservation.py +233 -199
  286. claude_mpm/services/infrastructure/daemon_manager.py +279 -0
  287. claude_mpm/services/infrastructure/logging.py +83 -76
  288. claude_mpm/services/infrastructure/monitoring.py +547 -404
  289. claude_mpm/services/mcp_gateway/__init__.py +30 -13
  290. claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
  291. claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
  292. claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
  293. claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
  294. claude_mpm/services/mcp_gateway/core/__init__.py +13 -20
  295. claude_mpm/services/mcp_gateway/core/base.py +80 -67
  296. claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
  297. claude_mpm/services/mcp_gateway/core/interfaces.py +87 -84
  298. claude_mpm/services/mcp_gateway/main.py +287 -137
  299. claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +97 -94
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
  302. claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
  303. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +105 -110
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +109 -119
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
  309. claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
  310. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
  311. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
  312. claude_mpm/services/memory/__init__.py +2 -2
  313. claude_mpm/services/memory/builder.py +451 -362
  314. claude_mpm/services/memory/cache/__init__.py +2 -2
  315. claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
  316. claude_mpm/services/memory/cache/simple_cache.py +107 -93
  317. claude_mpm/services/memory/indexed_memory.py +195 -193
  318. claude_mpm/services/memory/optimizer.py +267 -234
  319. claude_mpm/services/memory/router.py +571 -263
  320. claude_mpm/services/memory_hook_service.py +237 -0
  321. claude_mpm/services/port_manager.py +223 -0
  322. claude_mpm/services/project/__init__.py +3 -3
  323. claude_mpm/services/project/analyzer.py +451 -305
  324. claude_mpm/services/project/registry.py +262 -240
  325. claude_mpm/services/recovery_manager.py +287 -231
  326. claude_mpm/services/response_tracker.py +87 -67
  327. claude_mpm/services/runner_configuration_service.py +587 -0
  328. claude_mpm/services/session_management_service.py +304 -0
  329. claude_mpm/services/socketio/__init__.py +4 -4
  330. claude_mpm/services/socketio/client_proxy.py +174 -0
  331. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  332. claude_mpm/services/socketio/handlers/base.py +44 -30
  333. claude_mpm/services/socketio/handlers/connection.py +145 -65
  334. claude_mpm/services/socketio/handlers/file.py +123 -108
  335. claude_mpm/services/socketio/handlers/git.py +607 -373
  336. claude_mpm/services/socketio/handlers/hook.py +170 -0
  337. claude_mpm/services/socketio/handlers/memory.py +4 -4
  338. claude_mpm/services/socketio/handlers/project.py +4 -4
  339. claude_mpm/services/socketio/handlers/registry.py +53 -38
  340. claude_mpm/services/socketio/server/__init__.py +18 -0
  341. claude_mpm/services/socketio/server/broadcaster.py +252 -0
  342. claude_mpm/services/socketio/server/core.py +399 -0
  343. claude_mpm/services/socketio/server/main.py +323 -0
  344. claude_mpm/services/socketio_client_manager.py +160 -133
  345. claude_mpm/services/socketio_server.py +36 -1885
  346. claude_mpm/services/subprocess_launcher_service.py +316 -0
  347. claude_mpm/services/system_instructions_service.py +258 -0
  348. claude_mpm/services/ticket_manager.py +19 -533
  349. claude_mpm/services/utility_service.py +285 -0
  350. claude_mpm/services/version_control/__init__.py +18 -21
  351. claude_mpm/services/version_control/branch_strategy.py +20 -10
  352. claude_mpm/services/version_control/conflict_resolution.py +37 -13
  353. claude_mpm/services/version_control/git_operations.py +52 -21
  354. claude_mpm/services/version_control/semantic_versioning.py +92 -53
  355. claude_mpm/services/version_control/version_parser.py +145 -125
  356. claude_mpm/services/version_service.py +270 -0
  357. claude_mpm/storage/__init__.py +2 -2
  358. claude_mpm/storage/state_storage.py +177 -181
  359. claude_mpm/ticket_wrapper.py +2 -2
  360. claude_mpm/utils/__init__.py +2 -2
  361. claude_mpm/utils/agent_dependency_loader.py +453 -243
  362. claude_mpm/utils/config_manager.py +157 -118
  363. claude_mpm/utils/console.py +1 -1
  364. claude_mpm/utils/dependency_cache.py +102 -107
  365. claude_mpm/utils/dependency_manager.py +52 -47
  366. claude_mpm/utils/dependency_strategies.py +131 -96
  367. claude_mpm/utils/environment_context.py +110 -102
  368. claude_mpm/utils/error_handler.py +75 -55
  369. claude_mpm/utils/file_utils.py +80 -67
  370. claude_mpm/utils/framework_detection.py +12 -11
  371. claude_mpm/utils/import_migration_example.py +12 -60
  372. claude_mpm/utils/imports.py +48 -45
  373. claude_mpm/utils/path_operations.py +100 -93
  374. claude_mpm/utils/robust_installer.py +172 -164
  375. claude_mpm/utils/session_logging.py +30 -23
  376. claude_mpm/utils/subprocess_utils.py +99 -61
  377. claude_mpm/validation/__init__.py +1 -1
  378. claude_mpm/validation/agent_validator.py +151 -111
  379. claude_mpm/validation/frontmatter_validator.py +92 -71
  380. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/METADATA +27 -1
  381. claude_mpm-4.0.3.dist-info/RECORD +402 -0
  382. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/entry_points.txt +1 -0
  383. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/licenses/LICENSE +1 -1
  384. claude_mpm/cli/commands/run_guarded.py +0 -511
  385. claude_mpm/config/memory_guardian_config.py +0 -325
  386. claude_mpm/config/memory_guardian_yaml.py +0 -335
  387. claude_mpm/core/config_paths.py +0 -150
  388. claude_mpm/core/memory_aware_runner.py +0 -353
  389. claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
  390. claude_mpm/deployment_paths.py +0 -261
  391. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
  392. claude_mpm/models/state_models.py +0 -433
  393. claude_mpm/services/agent/__init__.py +0 -24
  394. claude_mpm/services/agent/deployment.py +0 -2548
  395. claude_mpm/services/agent/management.py +0 -598
  396. claude_mpm/services/agent/registry.py +0 -813
  397. claude_mpm/services/agents/registry/agent_registry.py +0 -813
  398. claude_mpm/services/communication/socketio.py +0 -1935
  399. claude_mpm/services/communication/websocket.py +0 -479
  400. claude_mpm/services/framework_claude_md_generator.py +0 -624
  401. claude_mpm/services/health_monitor.py +0 -893
  402. claude_mpm/services/infrastructure/graceful_degradation.py +0 -616
  403. claude_mpm/services/infrastructure/health_monitor.py +0 -775
  404. claude_mpm/services/infrastructure/memory_dashboard.py +0 -479
  405. claude_mpm/services/infrastructure/memory_guardian.py +0 -944
  406. claude_mpm/services/infrastructure/restart_protection.py +0 -642
  407. claude_mpm/services/infrastructure/state_manager.py +0 -774
  408. claude_mpm/services/mcp_gateway/manager.py +0 -334
  409. claude_mpm/services/optimized_hook_service.py +0 -542
  410. claude_mpm/services/project_analyzer.py +0 -864
  411. claude_mpm/services/project_registry.py +0 -608
  412. claude_mpm/services/standalone_socketio_server.py +0 -1300
  413. claude_mpm/services/ticket_manager_di.py +0 -318
  414. claude_mpm/services/ticketing_service_original.py +0 -510
  415. claude_mpm/utils/paths.py +0 -395
  416. claude_mpm/utils/platform_memory.py +0 -524
  417. claude_mpm-3.9.11.dist-info/RECORD +0 -306
  418. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/WHEEL +0 -0
  419. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/top_level.txt +0 -0
@@ -1,2548 +0,0 @@
1
- """Agent deployment service for Claude Code native subagents.
2
-
3
- This service handles the complete lifecycle of agent deployment:
4
- 1. Building agent YAML files from JSON templates
5
- 2. Managing versioning and updates
6
- 3. Deploying to Claude Code's .claude/agents directory
7
- 4. Environment configuration for agent discovery
8
- 5. Deployment verification and cleanup
9
-
10
- OPERATIONAL CONSIDERATIONS:
11
- - Deployment is idempotent - safe to run multiple times
12
- - Version checking prevents unnecessary rebuilds (saves I/O)
13
- - Supports force rebuild for troubleshooting
14
- - Maintains backward compatibility with legacy versions
15
- - Handles migration from old serial versioning to semantic versioning
16
-
17
- MONITORING:
18
- - Check logs for deployment status and errors
19
- - Monitor disk space in .claude/agents directory
20
- - Track version migration progress
21
- - Verify agent discovery after deployment
22
-
23
- ROLLBACK PROCEDURES:
24
- - Keep backups of .claude/agents before major updates
25
- - Use clean_deployment() to remove system agents
26
- - User-created agents are preserved during cleanup
27
- - Version tracking allows targeted rollbacks
28
- """
29
-
30
- import os
31
- import shutil
32
- import logging
33
- import time
34
- from pathlib import Path
35
- from typing import Optional, List, Dict, Any
36
-
37
- from claude_mpm.core.logging_config import get_logger, log_operation, log_performance_context
38
- from claude_mpm.core.exceptions import AgentDeploymentError
39
- from claude_mpm.constants import EnvironmentVars, Paths, AgentMetadata
40
- from claude_mpm.config.paths import paths
41
- from claude_mpm.core.config import Config
42
- from claude_mpm.core.constants import (
43
- TimeoutConfig,
44
- SystemLimits,
45
- ResourceLimits
46
- )
47
- from claude_mpm.core.interfaces import AgentDeploymentInterface
48
-
49
-
50
- class AgentDeploymentService(AgentDeploymentInterface):
51
- """Service for deploying Claude Code native agents.
52
-
53
- METRICS COLLECTION OPPORTUNITIES:
54
- This service could collect valuable deployment metrics including:
55
- - Agent deployment frequency and success rates
56
- - Template validation performance
57
- - Version migration patterns
58
- - Deployment duration by agent type
59
- - Cache hit rates for agent templates
60
- - Resource usage during deployment (memory, CPU)
61
- - Agent file sizes and complexity metrics
62
- - Deployment failure reasons and patterns
63
-
64
- DEPLOYMENT PIPELINE:
65
- 1. Initialize with template and base agent paths
66
- 2. Load base agent configuration (shared settings)
67
- 3. Iterate through agent templates
68
- 4. Check version and update requirements
69
- 5. Build YAML files with proper formatting
70
- 6. Deploy to target directory
71
- 7. Set environment variables for discovery
72
- 8. Verify deployment success
73
-
74
- ENVIRONMENT REQUIREMENTS:
75
- - Write access to .claude/agents directory
76
- - Python 3.8+ for pathlib and typing features
77
- - JSON parsing for template files
78
- - YAML generation capabilities
79
- """
80
-
81
- def __init__(self, templates_dir: Optional[Path] = None, base_agent_path: Optional[Path] = None, working_directory: Optional[Path] = None):
82
- """
83
- Initialize agent deployment service.
84
-
85
- Args:
86
- templates_dir: Directory containing agent JSON files
87
- base_agent_path: Path to base_agent.md file
88
- working_directory: User's working directory (for project agents)
89
-
90
- METRICS OPPORTUNITY: Track initialization performance:
91
- - Template directory scan time
92
- - Base agent loading time
93
- - Initial validation overhead
94
- """
95
- self.logger = get_logger(__name__)
96
-
97
- # METRICS: Initialize deployment metrics tracking
98
- # This data structure would be used for collecting deployment telemetry
99
- self._deployment_metrics = {
100
- 'total_deployments': 0,
101
- 'successful_deployments': 0,
102
- 'failed_deployments': 0,
103
- 'migrations_performed': 0,
104
- 'average_deployment_time_ms': 0.0,
105
- 'deployment_times': [], # Keep last 100 for rolling average
106
- 'agent_type_counts': {}, # Track deployments by agent type
107
- 'version_migration_count': 0,
108
- 'template_validation_times': {}, # Track validation performance
109
- 'deployment_errors': {} # Track error types and frequencies
110
- }
111
-
112
- # Determine the actual working directory
113
- # Priority: working_directory param > CLAUDE_MPM_USER_PWD env var > current directory
114
- if working_directory:
115
- self.working_directory = Path(working_directory)
116
- elif 'CLAUDE_MPM_USER_PWD' in os.environ:
117
- self.working_directory = Path(os.environ['CLAUDE_MPM_USER_PWD'])
118
- else:
119
- self.working_directory = Path.cwd()
120
-
121
- self.logger.info(f"Working directory for deployment: {self.working_directory}")
122
-
123
- # Find templates directory using centralized path management
124
- if templates_dir:
125
- self.templates_dir = Path(templates_dir)
126
- else:
127
- # Use centralized paths instead of fragile parent calculations
128
- # For system agents, still use templates subdirectory
129
- # For project/user agents, this should be overridden with actual agents dir
130
- self.templates_dir = paths.agents_dir / "templates"
131
-
132
- # Find base agent file
133
- if base_agent_path:
134
- self.base_agent_path = Path(base_agent_path)
135
- else:
136
- # Use centralized paths for consistency
137
- self.base_agent_path = paths.agents_dir / "base_agent.json"
138
-
139
- self.logger.info(f"Templates directory: {self.templates_dir}")
140
- self.logger.info(f"Base agent path: {self.base_agent_path}")
141
-
142
- def deploy_agents(self, target_dir: Optional[Path] = None, force_rebuild: bool = False, deployment_mode: str = "update", config: Optional[Config] = None, use_async: bool = True) -> Dict[str, Any]:
143
- """
144
- Build and deploy agents by combining base_agent.md with templates.
145
- Also deploys system instructions for PM framework.
146
-
147
- DEPLOYMENT MODES:
148
- - "update": Normal update mode - skip agents with matching versions (default)
149
- - "project": Project deployment mode - always deploy all agents regardless of version
150
-
151
- CONFIGURATION:
152
- The config parameter or default configuration is used to determine:
153
- - Which agents to exclude from deployment
154
- - Case sensitivity for agent name matching
155
- - Whether to exclude agent dependencies
156
-
157
- METRICS COLLECTED:
158
- - Deployment start/end timestamps
159
- - Individual agent deployment durations
160
- - Success/failure rates by agent type
161
- - Version migration statistics
162
- - Template validation performance
163
- - Error type frequencies
164
-
165
- OPERATIONAL FLOW:
166
- 0. Validates and repairs broken frontmatter in existing agents (Step 0)
167
- 1. Validates target directory (creates if needed)
168
- 2. Loads base agent configuration
169
- 3. Discovers all agent templates
170
- 4. For each agent:
171
- - Checks if update needed (version comparison)
172
- - Builds YAML configuration
173
- - Writes to target directory
174
- - Tracks deployment status
175
-
176
- PERFORMANCE CONSIDERATIONS:
177
- - Skips unchanged agents (version-based caching)
178
- - Batch processes all agents in single pass
179
- - Minimal file I/O with in-memory building
180
- - Parallel-safe (no shared state mutations)
181
-
182
- ERROR HANDLING:
183
- - Continues deployment on individual agent failures
184
- - Collects all errors for reporting
185
- - Logs detailed error context
186
- - Returns comprehensive results dict
187
-
188
- MONITORING POINTS:
189
- - Track total deployment time
190
- - Monitor skipped vs updated vs new agents
191
- - Check error rates and patterns
192
- - Verify migration completion
193
-
194
- Args:
195
- target_dir: Target directory for agents (default: .claude/agents/)
196
- force_rebuild: Force rebuild even if agents exist (useful for troubleshooting)
197
- deployment_mode: "update" for version-aware updates, "project" for always deploy
198
- config: Optional configuration object (loads default if not provided)
199
- use_async: Use async operations for 50-70% faster deployment (default: True)
200
-
201
- Returns:
202
- Dictionary with deployment results:
203
- - target_dir: Deployment location
204
- - deployed: List of newly deployed agents
205
- - updated: List of updated agents
206
- - migrated: List of agents migrated to new version format
207
- - skipped: List of unchanged agents
208
- - errors: List of deployment errors
209
- - total: Total number of agents processed
210
- - repaired: List of agents with repaired frontmatter
211
- """
212
- # METRICS: Record deployment start time for performance tracking
213
- deployment_start_time = time.time()
214
-
215
- # Try async deployment for better performance if requested
216
- if use_async:
217
- async_results = self._try_async_deployment(
218
- target_dir=target_dir,
219
- force_rebuild=force_rebuild,
220
- config=config,
221
- deployment_start_time=deployment_start_time
222
- )
223
- if async_results is not None:
224
- return async_results
225
-
226
- # Continue with synchronous deployment
227
- self.logger.info("Using synchronous deployment")
228
-
229
- # Load and process configuration
230
- config, excluded_agents = self._load_deployment_config(config)
231
-
232
- # Determine target agents directory
233
- agents_dir = self._determine_agents_directory(target_dir)
234
-
235
- # Initialize results dictionary
236
- results = self._initialize_deployment_results(agents_dir, deployment_start_time)
237
-
238
- try:
239
- # Create agents directory if needed
240
- agents_dir.mkdir(parents=True, exist_ok=True)
241
-
242
- # STEP 0: Validate and repair broken frontmatter in existing agents
243
- self._repair_existing_agents(agents_dir, results)
244
-
245
- # Log deployment source tier
246
- source_tier = self._determine_source_tier()
247
- self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
248
-
249
- # Note: System instructions are now loaded directly by SimpleClaudeRunner
250
-
251
- # Check if templates directory exists
252
- if not self.templates_dir.exists():
253
- error_msg = f"Agents directory not found: {self.templates_dir}"
254
- self.logger.error(error_msg)
255
- results["errors"].append(error_msg)
256
- return results
257
-
258
- # Convert any existing YAML files to MD format
259
- conversion_results = self._convert_yaml_to_md(agents_dir)
260
- results["converted"] = conversion_results.get("converted", [])
261
-
262
- # Load base agent content
263
- base_agent_data, base_agent_version = self._load_base_agent()
264
-
265
- # Get and filter template files
266
- template_files = self._get_filtered_templates(excluded_agents, config)
267
- results["total"] = len(template_files)
268
-
269
- # Deploy each agent template
270
- for template_file in template_files:
271
- self._deploy_single_agent(
272
- template_file=template_file,
273
- agents_dir=agents_dir,
274
- base_agent_data=base_agent_data,
275
- base_agent_version=base_agent_version,
276
- force_rebuild=force_rebuild,
277
- deployment_mode=deployment_mode,
278
- results=results
279
- )
280
-
281
- self.logger.info(
282
- f"Deployed {len(results['deployed'])} agents, "
283
- f"updated {len(results['updated'])}, "
284
- f"migrated {len(results['migrated'])}, "
285
- f"converted {len(results['converted'])} YAML files, "
286
- f"repaired {len(results['repaired'])} frontmatter, "
287
- f"skipped {len(results['skipped'])}, "
288
- f"errors: {len(results['errors'])}"
289
- )
290
-
291
- except AgentDeploymentError as e:
292
- # Custom error with context already formatted
293
- self.logger.error(str(e))
294
- results["errors"].append(str(e))
295
- except Exception as e:
296
- # Wrap unexpected errors
297
- error_msg = f"Agent deployment failed: {e}"
298
- self.logger.error(error_msg)
299
- results["errors"].append(error_msg)
300
-
301
- # METRICS: Track deployment failure
302
- self._deployment_metrics['failed_deployments'] += 1
303
- error_type = type(e).__name__
304
- self._deployment_metrics['deployment_errors'][error_type] = \
305
- self._deployment_metrics['deployment_errors'].get(error_type, 0) + 1
306
-
307
- # METRICS: Calculate final deployment metrics
308
- deployment_end_time = time.time()
309
- deployment_duration = (deployment_end_time - deployment_start_time) * 1000 # ms
310
-
311
- results["metrics"]["end_time"] = deployment_end_time
312
- results["metrics"]["duration_ms"] = deployment_duration
313
-
314
- # METRICS: Update rolling averages and statistics
315
- self._update_deployment_metrics(deployment_duration, results)
316
-
317
- return results
318
-
319
- def _update_deployment_metrics(self, duration_ms: float, results: Dict[str, Any]) -> None:
320
- """
321
- Update internal deployment metrics.
322
-
323
- METRICS TRACKING:
324
- - Rolling average of deployment times (last 100)
325
- - Success/failure rates
326
- - Agent type distribution
327
- - Version migration patterns
328
- - Error frequency analysis
329
-
330
- This method demonstrates ETL-like processing:
331
- 1. Extract: Gather raw metrics from deployment results
332
- 2. Transform: Calculate averages, rates, and distributions
333
- 3. Load: Store in internal metrics structure for reporting
334
- """
335
- # Update total deployment count
336
- self._deployment_metrics['total_deployments'] += 1
337
-
338
- # Track success/failure
339
- if not results.get('errors'):
340
- self._deployment_metrics['successful_deployments'] += 1
341
- else:
342
- self._deployment_metrics['failed_deployments'] += 1
343
-
344
- # Update rolling average deployment time
345
- self._deployment_metrics['deployment_times'].append(duration_ms)
346
- if len(self._deployment_metrics['deployment_times']) > 100:
347
- # Keep only last 100 for memory efficiency
348
- self._deployment_metrics['deployment_times'] = \
349
- self._deployment_metrics['deployment_times'][-100:]
350
-
351
- # Calculate new average
352
- if self._deployment_metrics['deployment_times']:
353
- self._deployment_metrics['average_deployment_time_ms'] = \
354
- sum(self._deployment_metrics['deployment_times']) / \
355
- len(self._deployment_metrics['deployment_times'])
356
-
357
- def get_deployment_metrics(self) -> Dict[str, Any]:
358
- """
359
- Get current deployment metrics.
360
-
361
- Returns:
362
- Dictionary containing:
363
- - Total deployments and success rates
364
- - Average deployment time
365
- - Agent type distribution
366
- - Migration statistics
367
- - Error analysis
368
-
369
- This demonstrates a metrics API endpoint that could be:
370
- - Exposed via REST API for monitoring tools
371
- - Pushed to time-series databases (Prometheus, InfluxDB)
372
- - Used for dashboards and alerting
373
- - Integrated with AI observability platforms
374
- """
375
- success_rate = 0.0
376
- if self._deployment_metrics['total_deployments'] > 0:
377
- success_rate = (self._deployment_metrics['successful_deployments'] /
378
- self._deployment_metrics['total_deployments']) * 100
379
-
380
- return {
381
- 'total_deployments': self._deployment_metrics['total_deployments'],
382
- 'successful_deployments': self._deployment_metrics['successful_deployments'],
383
- 'failed_deployments': self._deployment_metrics['failed_deployments'],
384
- 'success_rate_percent': success_rate,
385
- 'average_deployment_time_ms': self._deployment_metrics['average_deployment_time_ms'],
386
- 'migrations_performed': self._deployment_metrics['migrations_performed'],
387
- 'agent_type_distribution': self._deployment_metrics['agent_type_counts'].copy(),
388
- 'version_migrations': self._deployment_metrics['version_migration_count'],
389
- 'error_distribution': self._deployment_metrics['deployment_errors'].copy(),
390
- 'recent_deployment_times': self._deployment_metrics['deployment_times'][-10:] # Last 10
391
- }
392
-
393
- def reset_metrics(self) -> None:
394
- """
395
- Reset deployment metrics.
396
-
397
- Useful for:
398
- - Starting fresh metrics collection periods
399
- - Testing and development
400
- - Scheduled metric rotation (e.g., daily reset)
401
- """
402
- self._deployment_metrics = {
403
- 'total_deployments': 0,
404
- 'successful_deployments': 0,
405
- 'failed_deployments': 0,
406
- 'migrations_performed': 0,
407
- 'average_deployment_time_ms': 0.0,
408
- 'deployment_times': [],
409
- 'agent_type_counts': {},
410
- 'version_migration_count': 0,
411
- 'template_validation_times': {},
412
- 'deployment_errors': {}
413
- }
414
- self.logger.info("Deployment metrics reset")
415
-
416
- def _extract_version(self, content: str, version_marker: str) -> int:
417
- """
418
- Extract version number from content.
419
-
420
- Args:
421
- content: File content
422
- version_marker: Version marker to look for (e.g., "AGENT_VERSION:" or "BASE_AGENT_VERSION:")
423
-
424
- Returns:
425
- Version number or 0 if not found
426
- """
427
- import re
428
- pattern = rf"<!-- {version_marker} (\d+) -->"
429
- match = re.search(pattern, content)
430
- if match:
431
- return int(match.group(1))
432
- return 0
433
-
434
- def _build_agent_markdown(self, agent_name: str, template_path: Path, base_agent_data: dict) -> str:
435
- """
436
- Build a complete agent markdown file with YAML frontmatter.
437
-
438
- Args:
439
- agent_name: Name of the agent
440
- template_path: Path to the agent template JSON file
441
- base_agent_data: Base agent data from JSON
442
-
443
- Returns:
444
- Complete agent markdown content with YAML frontmatter
445
- """
446
- import json
447
- from datetime import datetime
448
-
449
- # Read template JSON
450
- template_data = json.loads(template_path.read_text())
451
-
452
- # Extract basic info
453
- # Handle both 'agent_version' (new format) and 'version' (old format)
454
- agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
455
- base_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
456
-
457
- # Format version string as semantic version
458
- # Combine base and agent versions for a unified semantic version
459
- # Use agent version as primary, with base version in metadata
460
- version_string = self._format_version_display(agent_version)
461
-
462
- # Build YAML frontmatter
463
- # Check new format first (metadata.description), then old format
464
- description = (
465
- template_data.get('metadata', {}).get('description') or
466
- template_data.get('configuration_fields', {}).get('description') or
467
- template_data.get('description') or
468
- 'Agent for specialized tasks'
469
- )
470
-
471
- # Get tags from new format (metadata.tags) or old format
472
- tags = (
473
- template_data.get('metadata', {}).get('tags') or
474
- template_data.get('configuration_fields', {}).get('tags') or
475
- template_data.get('tags') or
476
- [agent_name, 'mpm-framework']
477
- )
478
-
479
- # Get tools from capabilities.tools in new format
480
- tools = (
481
- template_data.get('capabilities', {}).get('tools') or
482
- template_data.get('configuration_fields', {}).get('tools') or
483
- ["Read", "Write", "Edit", "Grep", "Glob", "LS"] # Default fallback
484
- )
485
-
486
- # Get model from capabilities.model in new format
487
- model = (
488
- template_data.get('capabilities', {}).get('model') or
489
- template_data.get('configuration_fields', {}).get('model') or
490
- "sonnet" # Default fallback
491
- )
492
-
493
- # Simplify model name for Claude Code
494
- model_map = {
495
- 'claude-4-sonnet-20250514': 'sonnet',
496
- 'claude-sonnet-4-20250514': 'sonnet',
497
- 'claude-opus-4-20250514': 'opus',
498
- 'claude-3-opus-20240229': 'opus',
499
- 'claude-3-haiku-20240307': 'haiku',
500
- 'claude-3.5-sonnet': 'sonnet',
501
- 'claude-3-sonnet': 'sonnet'
502
- }
503
- # Better fallback: extract the model type (opus/sonnet/haiku) from the string
504
- if model not in model_map:
505
- if 'opus' in model.lower():
506
- model = 'opus'
507
- elif 'sonnet' in model.lower():
508
- model = 'sonnet'
509
- elif 'haiku' in model.lower():
510
- model = 'haiku'
511
- else:
512
- # Last resort: try to extract from hyphenated format
513
- model = model_map.get(model, model.split('-')[-1] if '-' in model else model)
514
- else:
515
- model = model_map[model]
516
-
517
- # Get response format from template or use base agent default
518
- response_format = template_data.get('response', {}).get('format', 'structured')
519
-
520
- # Convert lists to space-separated strings for Claude Code compatibility
521
- tags_str = ' '.join(tags) if isinstance(tags, list) else tags
522
-
523
- # Convert tools list to comma-separated string for Claude Code compatibility
524
- # IMPORTANT: No spaces after commas - Claude Code requires exact format
525
- tools_str = ','.join(tools) if isinstance(tools, list) else tools
526
-
527
- # Extract proper agent_id and name from template
528
- agent_id = template_data.get('agent_id', agent_name)
529
- display_name = template_data.get('metadata', {}).get('name', agent_id)
530
-
531
- # Convert agent_id to Claude Code compatible name (replace underscores with hyphens)
532
- # Claude Code requires name to match pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
533
- claude_code_name = agent_id.replace('_', '-').lower()
534
-
535
- # Build frontmatter with only the fields Claude Code uses
536
- frontmatter_lines = [
537
- "---",
538
- f"name: {claude_code_name}",
539
- f"description: {description}",
540
- f"version: {version_string}",
541
- f"base_version: {self._format_version_display(base_version)}",
542
- f"author: claude-mpm", # Identify as system agent for deployment
543
- f"tools: {tools_str}",
544
- f"model: {model}"
545
- ]
546
-
547
- # Add optional fields if present
548
- # Check for color in metadata section (new format) or root (old format)
549
- color = (
550
- template_data.get('metadata', {}).get('color') or
551
- template_data.get('color')
552
- )
553
- if color:
554
- frontmatter_lines.append(f"color: {color}")
555
-
556
- frontmatter_lines.append("---")
557
- frontmatter_lines.append("")
558
- frontmatter_lines.append("")
559
-
560
- frontmatter = '\n'.join(frontmatter_lines)
561
-
562
- # Get the main content (instructions)
563
- # Check multiple possible locations for instructions
564
- content = (
565
- template_data.get('instructions') or
566
- template_data.get('narrative_fields', {}).get('instructions') or
567
- template_data.get('content') or
568
- f"You are the {agent_name} agent. Perform tasks related to {template_data.get('description', 'your specialization')}."
569
- )
570
-
571
- return frontmatter + content
572
-
573
- def _build_agent_yaml(self, agent_name: str, template_path: Path, base_agent_data: dict) -> str:
574
- """
575
- Build a complete agent YAML file by combining base agent and template.
576
- Only includes essential fields for Claude Code best practices.
577
-
578
- Args:
579
- agent_name: Name of the agent
580
- template_path: Path to the agent template JSON file
581
- base_agent_data: Base agent data from JSON
582
-
583
- Returns:
584
- Complete agent YAML content
585
- """
586
- import json
587
-
588
- # Read template JSON
589
- template_data = json.loads(template_path.read_text())
590
-
591
- # Extract capabilities
592
- capabilities = template_data.get('capabilities', {})
593
- metadata = template_data.get('metadata', {})
594
-
595
- # Extract version information
596
- agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
597
- version_string = self._format_version_display(agent_version)
598
-
599
- # Get tools list
600
- tools = capabilities.get('tools', [])
601
- tools_str = ', '.join(tools) if tools else 'Read, Write, Edit, Grep, Glob, LS'
602
-
603
- # Get description
604
- description = (
605
- metadata.get('description') or
606
- template_data.get('description') or
607
- f'{agent_name.title()} agent for specialized tasks'
608
- )
609
-
610
- # Get priority based on agent type
611
- priority_map = {
612
- 'security': 'high',
613
- 'qa': 'high',
614
- 'engineer': 'high',
615
- 'documentation': 'medium',
616
- 'research': 'medium',
617
- 'ops': 'high',
618
- 'data_engineer': 'medium',
619
- 'version_control': 'high'
620
- }
621
- priority = priority_map.get(agent_name, 'medium')
622
-
623
- # Get model
624
- model = capabilities.get('model', 'claude-3-5-sonnet-20241022')
625
-
626
- # Get temperature
627
- temperature = capabilities.get('temperature', 0.3)
628
-
629
- # Build clean YAML frontmatter with only essential fields
630
- yaml_content = f"""---
631
- name: {agent_name}
632
- description: "{description}"
633
- version: "{version_string}"
634
- tools: {tools_str}
635
- priority: {priority}
636
- model: {model}
637
- temperature: {temperature}"""
638
-
639
- # Add allowed_tools if present
640
- if 'allowed_tools' in capabilities:
641
- yaml_content += f"\nallowed_tools: {json.dumps(capabilities['allowed_tools'])}"
642
-
643
- # Add disallowed_tools if present
644
- if 'disallowed_tools' in capabilities:
645
- yaml_content += f"\ndisallowed_tools: {json.dumps(capabilities['disallowed_tools'])}"
646
-
647
- yaml_content += "\n---\n"
648
-
649
- # Get instructions from template
650
- instructions = (
651
- template_data.get('instructions') or
652
- base_agent_data.get('narrative_fields', {}).get('instructions', '')
653
- )
654
-
655
- # Add base instructions if not already included
656
- base_instructions = base_agent_data.get('narrative_fields', {}).get('instructions', '')
657
- if base_instructions and base_instructions not in instructions:
658
- yaml_content += base_instructions + "\n\n---\n\n"
659
-
660
- yaml_content += instructions
661
-
662
- return yaml_content
663
-
664
- def _merge_narrative_fields(self, base_data: dict, template_data: dict) -> dict:
665
- """
666
- Merge narrative fields from base and template, combining arrays.
667
-
668
- Args:
669
- base_data: Base agent data
670
- template_data: Agent template data
671
-
672
- Returns:
673
- Merged narrative fields
674
- """
675
- base_narrative = base_data.get('narrative_fields', {})
676
- template_narrative = template_data.get('narrative_fields', {})
677
-
678
- merged = {}
679
-
680
- # For narrative fields, combine base + template
681
- for field in ['when_to_use', 'specialized_knowledge', 'unique_capabilities']:
682
- base_items = base_narrative.get(field, [])
683
- template_items = template_narrative.get(field, [])
684
- merged[field] = base_items + template_items
685
-
686
- # For instructions, combine with separator
687
- base_instructions = base_narrative.get('instructions', '')
688
- template_instructions = template_narrative.get('instructions', '')
689
-
690
- if base_instructions and template_instructions:
691
- merged['instructions'] = base_instructions + "\n\n---\n\n" + template_instructions
692
- elif template_instructions:
693
- merged['instructions'] = template_instructions
694
- elif base_instructions:
695
- merged['instructions'] = base_instructions
696
- else:
697
- merged['instructions'] = ''
698
-
699
- return merged
700
-
701
- def _merge_configuration_fields(self, base_data: dict, template_data: dict) -> dict:
702
- """
703
- Merge configuration fields, with template overriding base.
704
-
705
- Args:
706
- base_data: Base agent data
707
- template_data: Agent template data
708
-
709
- Returns:
710
- Merged configuration fields
711
- """
712
- base_config = base_data.get('configuration_fields', {})
713
- template_config = template_data.get('configuration_fields', {})
714
-
715
- # Start with base configuration
716
- merged = base_config.copy()
717
-
718
- # Override with template-specific configuration
719
- merged.update(template_config)
720
-
721
- # Also merge in capabilities from new format if not already in config
722
- capabilities = template_data.get('capabilities', {})
723
- if capabilities:
724
- # Map capabilities fields to configuration fields
725
- if 'tools' not in merged and 'tools' in capabilities:
726
- merged['tools'] = capabilities['tools']
727
- if 'max_tokens' not in merged and 'max_tokens' in capabilities:
728
- merged['max_tokens'] = capabilities['max_tokens']
729
- if 'temperature' not in merged and 'temperature' in capabilities:
730
- merged['temperature'] = capabilities['temperature']
731
- if 'timeout' not in merged and 'timeout' in capabilities:
732
- merged['timeout'] = capabilities['timeout']
733
- if 'memory_limit' not in merged and 'memory_limit' in capabilities:
734
- merged['memory_limit'] = capabilities['memory_limit']
735
- if 'cpu_limit' not in merged and 'cpu_limit' in capabilities:
736
- merged['cpu_limit'] = capabilities['cpu_limit']
737
- if 'network_access' not in merged and 'network_access' in capabilities:
738
- merged['network_access'] = capabilities['network_access']
739
- if 'model' not in merged and 'model' in capabilities:
740
- merged['model'] = capabilities['model']
741
-
742
- # Also check metadata for description and tags in new format
743
- metadata = template_data.get('metadata', {})
744
- if metadata:
745
- if 'description' not in merged and 'description' in metadata:
746
- merged['description'] = metadata['description']
747
- if 'tags' not in merged and 'tags' in metadata:
748
- merged['tags'] = metadata['tags']
749
-
750
- return merged
751
-
752
- def set_claude_environment(self, config_dir: Optional[Path] = None) -> Dict[str, str]:
753
- """
754
- Set Claude environment variables for agent discovery.
755
-
756
- OPERATIONAL PURPOSE:
757
- Claude Code discovers agents through environment variables that
758
- point to configuration directories. This method ensures proper
759
- environment setup for agent runtime discovery.
760
-
761
- ENVIRONMENT VARIABLES SET:
762
- 1. CLAUDE_CONFIG_DIR: Root configuration directory path
763
- 2. CLAUDE_MAX_PARALLEL_SUBAGENTS: Concurrency limit (default: 5)
764
- 3. CLAUDE_TIMEOUT: Agent execution timeout (default: 600s)
765
-
766
- DEPLOYMENT CONSIDERATIONS:
767
- - Call after agent deployment for immediate availability
768
- - Environment changes affect current process and children
769
- - Does not persist across system restarts
770
- - Add to shell profile for permanent configuration
771
-
772
- TROUBLESHOOTING:
773
- - Verify with: echo $CLAUDE_CONFIG_DIR
774
- - Check agent discovery: ls $CLAUDE_CONFIG_DIR/agents/
775
- - Monitor timeout issues in production
776
- - Adjust parallel limits based on system resources
777
-
778
- PERFORMANCE TUNING:
779
- - Increase parallel agents for CPU-bound tasks
780
- - Reduce for memory-constrained environments
781
- - Balance timeout with longest expected operations
782
- - Monitor resource usage during parallel execution
783
-
784
- Args:
785
- config_dir: Claude configuration directory (default: .claude/)
786
-
787
- Returns:
788
- Dictionary of environment variables set for verification
789
- """
790
- if not config_dir:
791
- # Use the working directory determined during initialization
792
- config_dir = self.working_directory / Paths.CLAUDE_CONFIG_DIR.value
793
-
794
- env_vars = {}
795
-
796
- # Set Claude configuration directory
797
- env_vars[EnvironmentVars.CLAUDE_CONFIG_DIR.value] = str(config_dir.absolute())
798
-
799
- # Set parallel agent limits
800
- env_vars[EnvironmentVars.CLAUDE_MAX_PARALLEL_SUBAGENTS.value] = EnvironmentVars.DEFAULT_MAX_AGENTS.value
801
-
802
- # Set timeout for agent execution
803
- env_vars[EnvironmentVars.CLAUDE_TIMEOUT.value] = EnvironmentVars.DEFAULT_TIMEOUT.value
804
-
805
- # Apply environment variables
806
- for key, value in env_vars.items():
807
- os.environ[key] = value
808
- self.logger.debug(f"Set environment: {key}={value}")
809
-
810
- return env_vars
811
-
812
- def verify_deployment(self, config_dir: Optional[Path] = None) -> Dict[str, Any]:
813
- """
814
- Verify agent deployment and Claude configuration.
815
-
816
- OPERATIONAL PURPOSE:
817
- Post-deployment verification ensures agents are correctly deployed
818
- and discoverable by Claude Code. Critical for deployment validation
819
- and troubleshooting runtime issues.
820
-
821
- VERIFICATION CHECKS:
822
- 1. Configuration directory exists and is accessible
823
- 2. Agents directory contains expected YAML files
824
- 3. Agent files have valid YAML frontmatter
825
- 4. Version format is current (identifies migration needs)
826
- 5. Environment variables are properly set
827
-
828
- MONITORING INTEGRATION:
829
- - Call after deployment for health checks
830
- - Include in deployment pipelines
831
- - Log results for audit trails
832
- - Alert on missing agents or errors
833
-
834
- TROUBLESHOOTING GUIDE:
835
- - Missing config_dir: Check deployment target path
836
- - No agents found: Verify deployment completed
837
- - Migration needed: Run with force_rebuild
838
- - Environment warnings: Call set_claude_environment()
839
-
840
- RESULT INTERPRETATION:
841
- - agents_found: Successfully deployed agents
842
- - agents_needing_migration: Require version update
843
- - warnings: Non-critical issues to address
844
- - environment: Current runtime configuration
845
-
846
- Args:
847
- config_dir: Claude configuration directory (default: .claude/)
848
-
849
- Returns:
850
- Verification results dictionary:
851
- - config_dir: Checked directory path
852
- - agents_found: List of discovered agents with metadata
853
- - agents_needing_migration: Agents with old version format
854
- - environment: Current environment variables
855
- - warnings: List of potential issues
856
- """
857
- if not config_dir:
858
- # Use the working directory determined during initialization
859
- config_dir = self.working_directory / ".claude"
860
-
861
- results = {
862
- "config_dir": str(config_dir),
863
- "agents_found": [],
864
- "agents_needing_migration": [],
865
- "environment": {},
866
- "warnings": []
867
- }
868
-
869
- # Check configuration directory
870
- if not config_dir.exists():
871
- results["warnings"].append(f"Configuration directory not found: {config_dir}")
872
- return results
873
-
874
- # Check agents directory
875
- agents_dir = config_dir / "agents"
876
- if not agents_dir.exists():
877
- results["warnings"].append(f"Agents directory not found: {agents_dir}")
878
- return results
879
-
880
- # List deployed agents
881
- agent_files = list(agents_dir.glob("*.md"))
882
-
883
- # Get exclusion configuration for logging purposes
884
- try:
885
- from claude_mpm.core.config import Config
886
- config = Config()
887
- excluded_agents = config.get('agent_deployment.excluded_agents', [])
888
- if excluded_agents:
889
- self.logger.debug(f"Note: The following agents are configured for exclusion: {excluded_agents}")
890
- except Exception:
891
- pass # Ignore config loading errors in verification
892
-
893
- for agent_file in agent_files:
894
- try:
895
- # Read first few lines to get agent name from YAML
896
- with open(agent_file, 'r') as f:
897
- lines = f.readlines()[:10]
898
-
899
- agent_info = {
900
- "file": agent_file.name,
901
- "path": str(agent_file)
902
- }
903
-
904
- # Extract name, version, and base_version from YAML frontmatter
905
- version_str = None
906
- base_version_str = None
907
- for line in lines:
908
- if line.startswith("name:"):
909
- agent_info["name"] = line.split(":", 1)[1].strip().strip('"\'')
910
- elif line.startswith("version:"):
911
- version_str = line.split(":", 1)[1].strip().strip('"\'')
912
- agent_info["version"] = version_str
913
- elif line.startswith("base_version:"):
914
- base_version_str = line.split(":", 1)[1].strip().strip('"\'')
915
- agent_info["base_version"] = base_version_str
916
-
917
- # Check if agent needs migration
918
- if version_str and self._is_old_version_format(version_str):
919
- agent_info["needs_migration"] = True
920
- results["agents_needing_migration"].append(agent_info["name"])
921
-
922
- results["agents_found"].append(agent_info)
923
-
924
- except Exception as e:
925
- results["warnings"].append(f"Failed to read {agent_file.name}: {e}")
926
-
927
- # Check environment variables
928
- env_vars = ["CLAUDE_CONFIG_DIR", "CLAUDE_MAX_PARALLEL_SUBAGENTS", "CLAUDE_TIMEOUT"]
929
- for var in env_vars:
930
- value = os.environ.get(var)
931
- if value:
932
- results["environment"][var] = value
933
- else:
934
- results["warnings"].append(f"Environment variable not set: {var}")
935
-
936
- return results
937
-
938
- def deploy_agent(self, agent_name: str, target_dir: Path, force_rebuild: bool = False) -> bool:
939
- """
940
- Deploy a single agent to the specified directory.
941
-
942
- Args:
943
- agent_name: Name of the agent to deploy
944
- target_dir: Target directory for deployment (Path object)
945
- force_rebuild: Whether to force rebuild even if version is current
946
-
947
- Returns:
948
- True if deployment was successful, False otherwise
949
-
950
- WHY: Single agent deployment because:
951
- - Users may want to deploy specific agents only
952
- - Reduces deployment time for targeted updates
953
- - Enables selective agent management in projects
954
-
955
- FIXED: Method now correctly handles all internal calls to:
956
- - _check_agent_needs_update (with 3 arguments)
957
- - _build_agent_markdown (with 3 arguments including base_agent_data)
958
- - Properly loads base_agent_data before building agent content
959
- """
960
- try:
961
- # Find the template file
962
- template_file = self.templates_dir / f"{agent_name}.json"
963
- if not template_file.exists():
964
- self.logger.error(f"Agent template not found: {agent_name}")
965
- return False
966
-
967
- # Ensure target directory exists
968
- agents_dir = target_dir / '.claude' / 'agents'
969
- agents_dir.mkdir(parents=True, exist_ok=True)
970
-
971
- # Build and deploy the agent
972
- target_file = agents_dir / f"{agent_name}.md"
973
-
974
- # Check if update is needed
975
- if not force_rebuild and target_file.exists():
976
- # Load base agent data for version checking
977
- base_agent_data = {}
978
- base_agent_version = (0, 0, 0)
979
- if self.base_agent_path.exists():
980
- try:
981
- import json
982
- base_agent_data = json.loads(self.base_agent_path.read_text())
983
- base_agent_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
984
- except Exception as e:
985
- self.logger.warning(f"Could not load base agent for version check: {e}")
986
-
987
- needs_update, reason = self._check_agent_needs_update(target_file, template_file, base_agent_version)
988
- if not needs_update:
989
- self.logger.info(f"Agent {agent_name} is up to date")
990
- return True
991
- else:
992
- self.logger.info(f"Updating agent {agent_name}: {reason}")
993
-
994
- # Load base agent data for building
995
- base_agent_data = {}
996
- if self.base_agent_path.exists():
997
- try:
998
- import json
999
- base_agent_data = json.loads(self.base_agent_path.read_text())
1000
- except Exception as e:
1001
- self.logger.warning(f"Could not load base agent: {e}")
1002
-
1003
- # Build the agent markdown
1004
- agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
1005
- if not agent_content:
1006
- self.logger.error(f"Failed to build agent content for {agent_name}")
1007
- return False
1008
-
1009
- # Write to target file
1010
- target_file.write_text(agent_content)
1011
- self.logger.info(f"Successfully deployed agent: {agent_name} to {target_file}")
1012
-
1013
- return True
1014
-
1015
- except AgentDeploymentError:
1016
- # Re-raise our custom exceptions
1017
- raise
1018
- except Exception as e:
1019
- # Wrap generic exceptions with context
1020
- raise AgentDeploymentError(
1021
- f"Failed to deploy agent {agent_name}",
1022
- context={"agent_name": agent_name, "error": str(e)}
1023
- ) from e
1024
-
1025
- def list_available_agents(self) -> List[Dict[str, Any]]:
1026
- """
1027
- List available agent templates.
1028
-
1029
- Returns:
1030
- List of agent information dictionaries
1031
- """
1032
- agents = []
1033
-
1034
- if not self.templates_dir.exists():
1035
- self.logger.warning(f"Agents directory not found: {self.templates_dir}")
1036
- return agents
1037
-
1038
- template_files = sorted(self.templates_dir.glob("*.json"))
1039
-
1040
- # Load configuration for exclusions
1041
- try:
1042
- from claude_mpm.core.config import Config
1043
- config = Config()
1044
- excluded_agents = config.get('agent_deployment.excluded_agents', [])
1045
- case_sensitive = config.get('agent_deployment.case_sensitive', False)
1046
-
1047
- # Normalize excluded agents for comparison
1048
- if not case_sensitive:
1049
- excluded_agents = [agent.lower() for agent in excluded_agents]
1050
- except Exception:
1051
- # If config loading fails, use empty exclusion list
1052
- excluded_agents = []
1053
- case_sensitive = False
1054
-
1055
- # Build combined exclusion set
1056
- hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS", "README", "pm", "PM", "project_manager"}
1057
-
1058
- # Filter out excluded agents
1059
- filtered_files = []
1060
- for f in template_files:
1061
- agent_name = f.stem
1062
-
1063
- # Check hardcoded exclusions
1064
- if agent_name in hardcoded_exclusions:
1065
- continue
1066
-
1067
- # Check file patterns
1068
- if agent_name.startswith(".") or agent_name.endswith(".backup"):
1069
- continue
1070
-
1071
- # Check user-configured exclusions
1072
- compare_name = agent_name.lower() if not case_sensitive else agent_name
1073
- if compare_name in excluded_agents:
1074
- continue
1075
-
1076
- filtered_files.append(f)
1077
-
1078
- template_files = filtered_files
1079
-
1080
- for template_file in template_files:
1081
- try:
1082
- agent_name = template_file.stem
1083
- agent_info = {
1084
- "name": agent_name,
1085
- "file": template_file.name,
1086
- "path": str(template_file),
1087
- "size": template_file.stat().st_size,
1088
- "description": f"{agent_name.title()} agent for specialized tasks"
1089
- }
1090
-
1091
- # Try to extract metadata from template JSON
1092
- try:
1093
- import json
1094
- template_data = json.loads(template_file.read_text())
1095
-
1096
- # Handle different schema formats
1097
- if 'metadata' in template_data:
1098
- # New schema format
1099
- metadata = template_data.get('metadata', {})
1100
- agent_info["description"] = metadata.get('description', agent_info["description"])
1101
- agent_info["role"] = metadata.get('specializations', [''])[0] if metadata.get('specializations') else ''
1102
- elif 'configuration_fields' in template_data:
1103
- # Old schema format
1104
- config_fields = template_data.get('configuration_fields', {})
1105
- agent_info["role"] = config_fields.get('primary_role', '')
1106
- agent_info["description"] = config_fields.get('description', agent_info["description"])
1107
-
1108
- # Handle both 'agent_version' (new format) and 'version' (old format)
1109
- version_tuple = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
1110
- agent_info["version"] = self._format_version_display(version_tuple)
1111
-
1112
- except Exception:
1113
- pass # Use defaults if can't parse
1114
-
1115
- agents.append(agent_info)
1116
-
1117
- except Exception as e:
1118
- self.logger.error(f"Failed to read template {template_file.name}: {e}")
1119
-
1120
- return agents
1121
-
1122
- def _check_agent_needs_update(self, deployed_file: Path, template_file: Path, current_base_version: tuple) -> tuple:
1123
- """
1124
- Check if a deployed agent needs to be updated.
1125
-
1126
- OPERATIONAL LOGIC:
1127
- 1. Verifies agent is system-managed (claude-mpm authored)
1128
- 2. Extracts version from deployed YAML frontmatter
1129
- 3. Detects old version formats requiring migration
1130
- 4. Compares semantic versions for update decision
1131
- 5. Returns detailed reason for update/skip decision
1132
-
1133
- VERSION MIGRATION STRATEGY:
1134
- - Old serial format (0002-0005) -> Semantic (2.5.0)
1135
- - Missing versions -> Force update to latest
1136
- - Non-semantic formats -> Trigger migration
1137
- - Preserves user modifications (non-system agents)
1138
-
1139
- PERFORMANCE OPTIMIZATION:
1140
- - Early exit for non-system agents
1141
- - Regex compilation cached by Python
1142
- - Minimal file reads (frontmatter only)
1143
- - Version comparison without full parse
1144
-
1145
- ERROR RECOVERY:
1146
- - Assumes update needed on parse failures
1147
- - Logs warnings for investigation
1148
- - Never blocks deployment pipeline
1149
- - Safe fallback to force update
1150
-
1151
- Args:
1152
- deployed_file: Path to the deployed agent file
1153
- template_file: Path to the template file
1154
- current_base_version: Current base agent version (unused in new strategy)
1155
-
1156
- Returns:
1157
- Tuple of (needs_update: bool, reason: str)
1158
- - needs_update: True if agent should be redeployed
1159
- - reason: Human-readable explanation for decision
1160
- """
1161
- try:
1162
- # Read deployed agent content
1163
- deployed_content = deployed_file.read_text()
1164
-
1165
- # Check if it's a system agent (authored by claude-mpm)
1166
- if "claude-mpm" not in deployed_content:
1167
- return (False, "not a system agent")
1168
-
1169
- # Extract version info from YAML frontmatter
1170
- import re
1171
-
1172
- # Check if using old serial format first
1173
- is_old_format = False
1174
- old_version_str = None
1175
-
1176
- # Try legacy combined format (e.g., "0002-0005")
1177
- legacy_match = re.search(r'^version:\s*["\']?(\d+)-(\d+)["\']?', deployed_content, re.MULTILINE)
1178
- if legacy_match:
1179
- is_old_format = True
1180
- old_version_str = f"{legacy_match.group(1)}-{legacy_match.group(2)}"
1181
- # Convert legacy format to semantic version
1182
- # Treat the agent version (second number) as minor version
1183
- deployed_agent_version = (0, int(legacy_match.group(2)), 0)
1184
- self.logger.info(f"Detected old serial version format: {old_version_str}")
1185
- else:
1186
- # Try to extract semantic version format (e.g., "2.1.0")
1187
- version_match = re.search(r'^version:\s*["\']?v?(\d+)\.(\d+)\.(\d+)["\']?', deployed_content, re.MULTILINE)
1188
- if version_match:
1189
- deployed_agent_version = (int(version_match.group(1)), int(version_match.group(2)), int(version_match.group(3)))
1190
- else:
1191
- # Fallback: try separate fields (very old format)
1192
- agent_version_match = re.search(r"^agent_version:\s*(\d+)", deployed_content, re.MULTILINE)
1193
- if agent_version_match:
1194
- is_old_format = True
1195
- old_version_str = f"agent_version: {agent_version_match.group(1)}"
1196
- deployed_agent_version = (0, int(agent_version_match.group(1)), 0)
1197
- self.logger.info(f"Detected old separate version format: {old_version_str}")
1198
- else:
1199
- # Check for missing version field
1200
- if "version:" not in deployed_content:
1201
- is_old_format = True
1202
- old_version_str = "missing"
1203
- deployed_agent_version = (0, 0, 0)
1204
- self.logger.info("Detected missing version field")
1205
- else:
1206
- deployed_agent_version = (0, 0, 0)
1207
-
1208
- # For base version, we don't need to extract from deployed file anymore
1209
- # as it's tracked in metadata
1210
-
1211
- # Read template to get current agent version
1212
- import json
1213
- template_data = json.loads(template_file.read_text())
1214
-
1215
- # Extract agent version from template (handle both numeric and semantic versioning)
1216
- current_agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
1217
-
1218
- # Compare semantic versions properly
1219
- # Semantic version comparison: compare major, then minor, then patch
1220
- def compare_versions(v1: tuple, v2: tuple) -> int:
1221
- """Compare two version tuples. Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
1222
- for a, b in zip(v1, v2):
1223
- if a < b:
1224
- return -1
1225
- elif a > b:
1226
- return 1
1227
- return 0
1228
-
1229
- # If old format detected, always trigger update for migration
1230
- if is_old_format:
1231
- new_version_str = self._format_version_display(current_agent_version)
1232
- return (True, f"migration needed from old format ({old_version_str}) to semantic version ({new_version_str})")
1233
-
1234
- # Check if agent template version is newer
1235
- if compare_versions(current_agent_version, deployed_agent_version) > 0:
1236
- deployed_str = self._format_version_display(deployed_agent_version)
1237
- current_str = self._format_version_display(current_agent_version)
1238
- return (True, f"agent template updated ({deployed_str} -> {current_str})")
1239
-
1240
- # Note: We no longer check base agent version separately since we're using
1241
- # a unified semantic version for the agent
1242
-
1243
- return (False, "up to date")
1244
-
1245
- except Exception as e:
1246
- self.logger.warning(f"Error checking agent update status: {e}")
1247
- # On error, assume update is needed
1248
- return (True, "version check failed")
1249
-
1250
- def clean_deployment(self, config_dir: Optional[Path] = None) -> Dict[str, Any]:
1251
- """
1252
- Clean up deployed agents.
1253
-
1254
- Args:
1255
- config_dir: Claude configuration directory (default: .claude/)
1256
-
1257
- Returns:
1258
- Cleanup results
1259
- """
1260
- if not config_dir:
1261
- # Use the working directory determined during initialization
1262
- config_dir = self.working_directory / ".claude"
1263
-
1264
- results = {
1265
- "removed": [],
1266
- "errors": []
1267
- }
1268
-
1269
- agents_dir = config_dir / "agents"
1270
- if not agents_dir.exists():
1271
- results["errors"].append(f"Agents directory not found: {agents_dir}")
1272
- return results
1273
-
1274
- # Remove system agents only (identified by claude-mpm author)
1275
- agent_files = list(agents_dir.glob("*.md"))
1276
-
1277
- for agent_file in agent_files:
1278
- try:
1279
- # Check if it's a system agent
1280
- with open(agent_file, 'r') as f:
1281
- content = f.read()
1282
- if "author: claude-mpm" in content or "author: 'claude-mpm'" in content:
1283
- agent_file.unlink()
1284
- results["removed"].append(str(agent_file))
1285
- self.logger.debug(f"Removed agent: {agent_file.name}")
1286
-
1287
- except Exception as e:
1288
- error_msg = f"Failed to remove {agent_file.name}: {e}"
1289
- self.logger.error(error_msg)
1290
- results["errors"].append(error_msg)
1291
- # Not raising AgentDeploymentError here to continue cleanup
1292
-
1293
- return results
1294
-
1295
- def _extract_agent_metadata(self, template_content: str) -> Dict[str, Any]:
1296
- """
1297
- Extract metadata from simplified agent template content.
1298
-
1299
- Args:
1300
- template_content: Agent template markdown content
1301
-
1302
- Returns:
1303
- Dictionary of extracted metadata
1304
- """
1305
- metadata = {}
1306
- lines = template_content.split('\n')
1307
-
1308
- # Extract sections based on the new simplified format
1309
- current_section = None
1310
- section_content = []
1311
-
1312
- for line in lines:
1313
- line = line.strip()
1314
-
1315
- if line.startswith('## When to Use'):
1316
- # Save previous section before starting new one
1317
- if current_section and section_content:
1318
- metadata[current_section] = section_content.copy()
1319
- current_section = 'when_to_use'
1320
- section_content = []
1321
- elif line.startswith('## Specialized Knowledge'):
1322
- # Save previous section before starting new one
1323
- if current_section and section_content:
1324
- metadata[current_section] = section_content.copy()
1325
- current_section = 'specialized_knowledge'
1326
- section_content = []
1327
- elif line.startswith('## Unique Capabilities'):
1328
- # Save previous section before starting new one
1329
- if current_section and section_content:
1330
- metadata[current_section] = section_content.copy()
1331
- current_section = 'unique_capabilities'
1332
- section_content = []
1333
- elif line.startswith('## ') or line.startswith('# '):
1334
- # End of section - save current section
1335
- if current_section and section_content:
1336
- metadata[current_section] = section_content.copy()
1337
- current_section = None
1338
- section_content = []
1339
- elif current_section and line.startswith('- '):
1340
- # Extract list item, removing the "- " prefix
1341
- item = line[2:].strip()
1342
- if item:
1343
- section_content.append(item)
1344
-
1345
- # Handle last section if file ends without another header
1346
- if current_section and section_content:
1347
- metadata[current_section] = section_content.copy()
1348
-
1349
- # Ensure all required fields have defaults
1350
- metadata.setdefault('when_to_use', [])
1351
- metadata.setdefault('specialized_knowledge', [])
1352
- metadata.setdefault('unique_capabilities', [])
1353
-
1354
- return metadata
1355
-
1356
- def _get_agent_tools(self, agent_name: str, metadata: Dict[str, Any]) -> List[str]:
1357
- """
1358
- Get appropriate tools for an agent based on its type.
1359
-
1360
- Args:
1361
- agent_name: Name of the agent
1362
- metadata: Agent metadata
1363
-
1364
- Returns:
1365
- List of tool names
1366
- """
1367
- # Base tools all agents should have
1368
- base_tools = [
1369
- "Read",
1370
- "Write",
1371
- "Edit",
1372
- "MultiEdit",
1373
- "Grep",
1374
- "Glob",
1375
- "LS",
1376
- "TodoWrite"
1377
- ]
1378
-
1379
- # Agent-specific tools
1380
- agent_tools = {
1381
- 'engineer': base_tools + ["Bash", "WebSearch", "WebFetch"],
1382
- 'qa': base_tools + ["Bash", "WebSearch"],
1383
- 'documentation': base_tools + ["WebSearch", "WebFetch"],
1384
- 'research': base_tools + ["WebSearch", "WebFetch", "Bash"],
1385
- 'security': base_tools + ["Bash", "WebSearch", "Grep"],
1386
- 'ops': base_tools + ["Bash", "WebSearch"],
1387
- 'data_engineer': base_tools + ["Bash", "WebSearch"],
1388
- 'version_control': base_tools + ["Bash"]
1389
- }
1390
-
1391
- # Return specific tools or default set
1392
- return agent_tools.get(agent_name, base_tools + ["Bash", "WebSearch"])
1393
-
1394
- def _format_version_display(self, version_tuple: tuple) -> str:
1395
- """
1396
- Format version tuple for display.
1397
-
1398
- Args:
1399
- version_tuple: Tuple of (major, minor, patch)
1400
-
1401
- Returns:
1402
- Formatted version string
1403
- """
1404
- if isinstance(version_tuple, tuple) and len(version_tuple) == 3:
1405
- major, minor, patch = version_tuple
1406
- return f"{major}.{minor}.{patch}"
1407
- else:
1408
- # Fallback for legacy format
1409
- return str(version_tuple)
1410
-
1411
- def _is_old_version_format(self, version_str: str) -> bool:
1412
- """
1413
- Check if a version string is in the old serial format.
1414
-
1415
- Old formats include:
1416
- - Serial format: "0002-0005" (contains hyphen, all digits)
1417
- - Missing version field
1418
- - Non-semantic version formats
1419
-
1420
- Args:
1421
- version_str: Version string to check
1422
-
1423
- Returns:
1424
- True if old format, False if semantic version
1425
- """
1426
- if not version_str:
1427
- return True
1428
-
1429
- import re
1430
-
1431
- # Check for serial format (e.g., "0002-0005")
1432
- if re.match(r'^\d+-\d+$', version_str):
1433
- return True
1434
-
1435
- # Check for semantic version format (e.g., "2.1.0")
1436
- if re.match(r'^v?\d+\.\d+\.\d+$', version_str):
1437
- return False
1438
-
1439
- # Any other format is considered old
1440
- return True
1441
-
1442
- def _parse_version(self, version_value: Any) -> tuple:
1443
- """
1444
- Parse version from various formats to semantic version tuple.
1445
-
1446
- Handles:
1447
- - Integer values: 5 -> (0, 5, 0)
1448
- - String integers: "5" -> (0, 5, 0)
1449
- - Semantic versions: "2.1.0" -> (2, 1, 0)
1450
- - Invalid formats: returns (0, 0, 0)
1451
-
1452
- Args:
1453
- version_value: Version in various formats
1454
-
1455
- Returns:
1456
- Tuple of (major, minor, patch) for comparison
1457
- """
1458
- if isinstance(version_value, int):
1459
- # Legacy integer version - treat as minor version
1460
- return (0, version_value, 0)
1461
-
1462
- if isinstance(version_value, str):
1463
- # Try to parse as simple integer
1464
- if version_value.isdigit():
1465
- return (0, int(version_value), 0)
1466
-
1467
- # Try to parse semantic version (e.g., "2.1.0" or "v2.1.0")
1468
- import re
1469
- sem_ver_match = re.match(r'^v?(\d+)\.(\d+)\.(\d+)', version_value)
1470
- if sem_ver_match:
1471
- major = int(sem_ver_match.group(1))
1472
- minor = int(sem_ver_match.group(2))
1473
- patch = int(sem_ver_match.group(3))
1474
- return (major, minor, patch)
1475
-
1476
- # Try to extract first number from string as minor version
1477
- num_match = re.search(r'(\d+)', version_value)
1478
- if num_match:
1479
- return (0, int(num_match.group(1)), 0)
1480
-
1481
- # Default to 0.0.0 for invalid formats
1482
- return (0, 0, 0)
1483
-
1484
- def _format_yaml_list(self, items: List[str], indent: int) -> str:
1485
- """
1486
- Format a list for YAML with proper indentation.
1487
-
1488
- Args:
1489
- items: List of items
1490
- indent: Number of spaces to indent
1491
-
1492
- Returns:
1493
- Formatted YAML list string
1494
- """
1495
- if not items:
1496
- items = ["No items specified"]
1497
-
1498
- indent_str = " " * indent
1499
- formatted_items = []
1500
-
1501
- for item in items:
1502
- # Escape quotes in the item
1503
- item = item.replace('"', '\\"')
1504
- formatted_items.append(f'{indent_str}- "{item}"')
1505
-
1506
- return '\n'.join(formatted_items)
1507
-
1508
- def _get_agent_specific_config(self, agent_name: str) -> Dict[str, Any]:
1509
- """
1510
- Get agent-specific configuration based on agent type.
1511
-
1512
- Args:
1513
- agent_name: Name of the agent
1514
-
1515
- Returns:
1516
- Dictionary of agent-specific configuration
1517
- """
1518
- # Base configuration all agents share
1519
- base_config = {
1520
- 'timeout': TimeoutConfig.DEFAULT_TIMEOUT,
1521
- 'max_tokens': SystemLimits.MAX_TOKEN_LIMIT,
1522
- 'memory_limit': ResourceLimits.STANDARD_MEMORY_RANGE[0], # Use lower bound of standard memory
1523
- 'cpu_limit': ResourceLimits.STANDARD_CPU_RANGE[1], # Use upper bound of standard CPU
1524
- 'network_access': True,
1525
- }
1526
-
1527
- # Agent-specific configurations
1528
- configs = {
1529
- 'engineer': {
1530
- **base_config,
1531
- 'description': 'Code implementation, development, and inline documentation',
1532
- 'tags': '["engineer", "development", "coding", "implementation"]',
1533
- 'tools': '["Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob", "LS", "WebSearch", "TodoWrite"]',
1534
- 'temperature': 0.2,
1535
- 'when_to_use': ['Code implementation needed', 'Bug fixes required', 'Refactoring tasks'],
1536
- 'specialized_knowledge': ['Programming best practices', 'Design patterns', 'Code optimization'],
1537
- 'unique_capabilities': ['Write production code', 'Debug complex issues', 'Refactor codebases'],
1538
- 'primary_role': 'Code implementation and development',
1539
- 'specializations': '["coding", "debugging", "refactoring", "optimization"]',
1540
- 'authority': 'ALL code implementation decisions',
1541
- },
1542
- 'qa': {
1543
- **base_config,
1544
- 'description': 'Quality assurance, testing, and validation',
1545
- 'tags': '["qa", "testing", "quality", "validation"]',
1546
- 'tools': '["Read", "Write", "Edit", "Bash", "Grep", "Glob", "LS", "TodoWrite"]',
1547
- 'temperature': 0.1,
1548
- 'when_to_use': ['Testing needed', 'Quality validation', 'Test coverage analysis'],
1549
- 'specialized_knowledge': ['Testing methodologies', 'Quality metrics', 'Test automation'],
1550
- 'unique_capabilities': ['Execute test suites', 'Identify edge cases', 'Validate quality'],
1551
- 'primary_role': 'Testing and quality assurance',
1552
- 'specializations': '["testing", "validation", "quality-assurance", "coverage"]',
1553
- 'authority': 'ALL testing and quality decisions',
1554
- },
1555
- 'documentation': {
1556
- **base_config,
1557
- 'description': 'Documentation creation, maintenance, and changelog generation',
1558
- 'tags': '["documentation", "writing", "changelog", "docs"]',
1559
- 'tools': '["Read", "Write", "Edit", "MultiEdit", "Grep", "Glob", "LS", "WebSearch", "TodoWrite"]',
1560
- 'temperature': 0.3,
1561
- 'when_to_use': ['Documentation updates needed', 'Changelog generation', 'README updates'],
1562
- 'specialized_knowledge': ['Technical writing', 'Documentation standards', 'Semantic versioning'],
1563
- 'unique_capabilities': ['Create clear documentation', 'Generate changelogs', 'Maintain docs'],
1564
- 'primary_role': 'Documentation and technical writing',
1565
- 'specializations': '["technical-writing", "changelog", "api-docs", "guides"]',
1566
- 'authority': 'ALL documentation decisions',
1567
- },
1568
- 'research': {
1569
- **base_config,
1570
- 'description': 'Technical research, analysis, and investigation',
1571
- 'tags': '["research", "analysis", "investigation", "evaluation"]',
1572
- 'tools': '["Read", "Grep", "Glob", "LS", "WebSearch", "WebFetch", "TodoWrite"]',
1573
- 'temperature': 0.4,
1574
- 'when_to_use': ['Technical research needed', 'Solution evaluation', 'Best practices investigation'],
1575
- 'specialized_knowledge': ['Research methodologies', 'Technical analysis', 'Evaluation frameworks'],
1576
- 'unique_capabilities': ['Deep investigation', 'Comparative analysis', 'Evidence-based recommendations'],
1577
- 'primary_role': 'Research and technical analysis',
1578
- 'specializations': '["investigation", "analysis", "evaluation", "recommendations"]',
1579
- 'authority': 'ALL research decisions',
1580
- },
1581
- 'security': {
1582
- **base_config,
1583
- 'description': 'Security analysis, vulnerability assessment, and protection',
1584
- 'tags': '["security", "vulnerability", "protection", "audit"]',
1585
- 'tools': '["Read", "Grep", "Glob", "LS", "Bash", "WebSearch", "TodoWrite"]',
1586
- 'temperature': 0.1,
1587
- 'when_to_use': ['Security review needed', 'Vulnerability assessment', 'Security audit'],
1588
- 'specialized_knowledge': ['Security best practices', 'OWASP guidelines', 'Vulnerability patterns'],
1589
- 'unique_capabilities': ['Identify vulnerabilities', 'Security auditing', 'Threat modeling'],
1590
- 'primary_role': 'Security analysis and protection',
1591
- 'specializations': '["vulnerability-assessment", "security-audit", "threat-modeling", "protection"]',
1592
- 'authority': 'ALL security decisions',
1593
- },
1594
- 'ops': {
1595
- **base_config,
1596
- 'description': 'Deployment, operations, and infrastructure management',
1597
- 'tags': '["ops", "deployment", "infrastructure", "devops"]',
1598
- 'tools': '["Read", "Write", "Edit", "Bash", "Grep", "Glob", "LS", "TodoWrite"]',
1599
- 'temperature': 0.2,
1600
- 'when_to_use': ['Deployment configuration', 'Infrastructure setup', 'CI/CD pipeline work'],
1601
- 'specialized_knowledge': ['Deployment best practices', 'Infrastructure as code', 'CI/CD'],
1602
- 'unique_capabilities': ['Configure deployments', 'Manage infrastructure', 'Automate operations'],
1603
- 'primary_role': 'Operations and deployment management',
1604
- 'specializations': '["deployment", "infrastructure", "automation", "monitoring"]',
1605
- 'authority': 'ALL operations decisions',
1606
- },
1607
- 'data_engineer': {
1608
- **base_config,
1609
- 'description': 'Data pipeline management and AI API integrations',
1610
- 'tags': '["data", "pipeline", "etl", "ai-integration"]',
1611
- 'tools': '["Read", "Write", "Edit", "Bash", "Grep", "Glob", "LS", "WebSearch", "TodoWrite"]',
1612
- 'temperature': 0.2,
1613
- 'when_to_use': ['Data pipeline setup', 'Database design', 'AI API integration'],
1614
- 'specialized_knowledge': ['Data architectures', 'ETL processes', 'AI/ML APIs'],
1615
- 'unique_capabilities': ['Design data schemas', 'Build pipelines', 'Integrate AI services'],
1616
- 'primary_role': 'Data engineering and AI integration',
1617
- 'specializations': '["data-pipelines", "etl", "database", "ai-integration"]',
1618
- 'authority': 'ALL data engineering decisions',
1619
- },
1620
- 'version_control': {
1621
- **base_config,
1622
- 'description': 'Git operations, version management, and release coordination',
1623
- 'tags': '["git", "version-control", "release", "branching"]',
1624
- 'tools': '["Read", "Bash", "Grep", "Glob", "LS", "TodoWrite"]',
1625
- 'temperature': 0.1,
1626
- 'network_access': False, # Git operations are local
1627
- 'when_to_use': ['Git operations needed', 'Version bumping', 'Release management'],
1628
- 'specialized_knowledge': ['Git workflows', 'Semantic versioning', 'Release processes'],
1629
- 'unique_capabilities': ['Complex git operations', 'Version management', 'Release coordination'],
1630
- 'primary_role': 'Version control and release management',
1631
- 'specializations': '["git", "versioning", "branching", "releases"]',
1632
- 'authority': 'ALL version control decisions',
1633
- }
1634
- }
1635
-
1636
- # Return the specific config or a default
1637
- return configs.get(agent_name, {
1638
- **base_config,
1639
- 'description': f'{agent_name.title()} agent for specialized tasks',
1640
- 'tags': f'["{agent_name}", "specialized", "mpm"]',
1641
- 'tools': '["Read", "Write", "Edit", "Grep", "Glob", "LS", "TodoWrite"]',
1642
- 'temperature': 0.3,
1643
- 'when_to_use': [f'When {agent_name} expertise is needed'],
1644
- 'specialized_knowledge': [f'{agent_name.title()} domain knowledge'],
1645
- 'unique_capabilities': [f'{agent_name.title()} specialized operations'],
1646
- 'primary_role': f'{agent_name.title()} operations',
1647
- 'specializations': f'["{agent_name}"]',
1648
- 'authority': f'ALL {agent_name} decisions',
1649
- })
1650
-
1651
- def _deploy_system_instructions(self, target_dir: Path, force_rebuild: bool, results: Dict[str, Any]) -> None:
1652
- """
1653
- Deploy system instructions for PM framework.
1654
-
1655
- Args:
1656
- target_dir: Target directory for deployment
1657
- force_rebuild: Force rebuild even if exists
1658
- results: Results dictionary to update
1659
- """
1660
- try:
1661
- # Find the INSTRUCTIONS.md file
1662
- module_path = Path(__file__).parent.parent
1663
- instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
1664
-
1665
- if not instructions_path.exists():
1666
- self.logger.warning(f"System instructions not found: {instructions_path}")
1667
- return
1668
-
1669
- # Target file for system instructions - use CLAUDE.md in user's home .claude directory
1670
- target_file = Path("~/.claude/CLAUDE.md").expanduser()
1671
-
1672
- # Ensure .claude directory exists
1673
- target_file.parent.mkdir(exist_ok=True)
1674
-
1675
- # Check if update needed
1676
- if not force_rebuild and target_file.exists():
1677
- # Compare modification times
1678
- if target_file.stat().st_mtime >= instructions_path.stat().st_mtime:
1679
- results["skipped"].append("CLAUDE.md")
1680
- self.logger.debug("System instructions up to date")
1681
- return
1682
-
1683
- # Read and deploy system instructions
1684
- instructions_content = instructions_path.read_text()
1685
- target_file.write_text(instructions_content)
1686
-
1687
- is_update = target_file.exists()
1688
- if is_update:
1689
- results["updated"].append({
1690
- "name": "CLAUDE.md",
1691
- "template": str(instructions_path),
1692
- "target": str(target_file)
1693
- })
1694
- self.logger.info("Updated system instructions")
1695
- else:
1696
- results["deployed"].append({
1697
- "name": "CLAUDE.md",
1698
- "template": str(instructions_path),
1699
- "target": str(target_file)
1700
- })
1701
- self.logger.info("Deployed system instructions")
1702
-
1703
- except Exception as e:
1704
- error_msg = f"Failed to deploy system instructions: {e}"
1705
- self.logger.error(error_msg)
1706
- results["errors"].append(error_msg)
1707
- # Not raising AgentDeploymentError as this is non-critical
1708
-
1709
- def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
1710
- """
1711
- Convert existing YAML agent files to MD format with YAML frontmatter.
1712
-
1713
- This method handles backward compatibility by finding existing .yaml
1714
- agent files and converting them to .md format expected by Claude Code.
1715
-
1716
- Args:
1717
- target_dir: Directory containing agent files
1718
-
1719
- Returns:
1720
- Dictionary with conversion results:
1721
- - converted: List of converted files
1722
- - errors: List of conversion errors
1723
- - skipped: List of files that didn't need conversion
1724
- """
1725
- results = {
1726
- "converted": [],
1727
- "errors": [],
1728
- "skipped": []
1729
- }
1730
-
1731
- try:
1732
- # Find existing YAML agent files
1733
- yaml_files = list(target_dir.glob("*.yaml"))
1734
-
1735
- if not yaml_files:
1736
- self.logger.debug("No YAML files found to convert")
1737
- return results
1738
-
1739
- self.logger.info(f"Found {len(yaml_files)} YAML files to convert to MD format")
1740
-
1741
- for yaml_file in yaml_files:
1742
- try:
1743
- agent_name = yaml_file.stem
1744
- md_file = target_dir / f"{agent_name}.md"
1745
-
1746
- # Skip if MD file already exists (unless it's older than YAML)
1747
- if md_file.exists():
1748
- # Check modification times for safety
1749
- yaml_mtime = yaml_file.stat().st_mtime
1750
- md_mtime = md_file.stat().st_mtime
1751
-
1752
- if md_mtime >= yaml_mtime:
1753
- results["skipped"].append({
1754
- "yaml_file": str(yaml_file),
1755
- "md_file": str(md_file),
1756
- "reason": "MD file already exists and is newer"
1757
- })
1758
- continue
1759
- else:
1760
- # MD file is older, proceed with conversion
1761
- self.logger.info(f"MD file {md_file.name} is older than YAML, converting...")
1762
-
1763
- # Read YAML content
1764
- yaml_content = yaml_file.read_text()
1765
-
1766
- # Convert YAML to MD with YAML frontmatter
1767
- md_content = self._convert_yaml_content_to_md(yaml_content, agent_name)
1768
-
1769
- # Write MD file
1770
- md_file.write_text(md_content)
1771
-
1772
- # Create backup of YAML file before removing (for safety)
1773
- backup_file = target_dir / f"{agent_name}.yaml.backup"
1774
- try:
1775
- yaml_file.rename(backup_file)
1776
- self.logger.debug(f"Created backup: {backup_file.name}")
1777
- except Exception as backup_error:
1778
- self.logger.warning(f"Failed to create backup for {yaml_file.name}: {backup_error}")
1779
- # Still remove the original YAML file even if backup fails
1780
- yaml_file.unlink()
1781
-
1782
- results["converted"].append({
1783
- "from": str(yaml_file),
1784
- "to": str(md_file),
1785
- "agent": agent_name
1786
- })
1787
-
1788
- self.logger.info(f"Converted {yaml_file.name} to {md_file.name}")
1789
-
1790
- except Exception as e:
1791
- error_msg = f"Failed to convert {yaml_file.name}: {e}"
1792
- self.logger.error(error_msg)
1793
- results["errors"].append(error_msg)
1794
-
1795
- except Exception as e:
1796
- error_msg = f"YAML to MD conversion failed: {e}"
1797
- self.logger.error(error_msg)
1798
- results["errors"].append(error_msg)
1799
-
1800
- return results
1801
-
1802
- def _convert_yaml_content_to_md(self, yaml_content: str, agent_name: str) -> str:
1803
- """
1804
- Convert YAML agent content to MD format with YAML frontmatter.
1805
-
1806
- Args:
1807
- yaml_content: Original YAML content
1808
- agent_name: Name of the agent
1809
-
1810
- Returns:
1811
- Markdown content with YAML frontmatter
1812
- """
1813
- import re
1814
- from datetime import datetime
1815
-
1816
- # Extract YAML frontmatter and content
1817
- yaml_parts = yaml_content.split('---', 2)
1818
-
1819
- if len(yaml_parts) < 3:
1820
- # No proper YAML frontmatter, treat entire content as instructions
1821
- frontmatter = f"""---
1822
- name: {agent_name}
1823
- description: "Agent for specialized tasks"
1824
- version: "1.0.0"
1825
- author: "claude-mpm@anthropic.com"
1826
- created: "{datetime.now().isoformat()}Z"
1827
- updated: "{datetime.now().isoformat()}Z"
1828
- tags: ["{agent_name}", "mpm-framework"]
1829
- tools: ["Read", "Write", "Edit", "Grep", "Glob", "LS"]
1830
- metadata:
1831
- deployment_type: "system"
1832
- converted_from: "yaml"
1833
- ---
1834
-
1835
- """
1836
- return frontmatter + yaml_content.strip()
1837
-
1838
- # Parse existing frontmatter
1839
- yaml_frontmatter = yaml_parts[1].strip()
1840
- instructions = yaml_parts[2].strip()
1841
-
1842
- # Extract key fields from YAML frontmatter
1843
- name = agent_name
1844
- description = self._extract_yaml_field(yaml_frontmatter, 'description') or f"{agent_name.title()} agent for specialized tasks"
1845
- version = self._extract_yaml_field(yaml_frontmatter, 'version') or "1.0.0"
1846
- tools_line = self._extract_yaml_field(yaml_frontmatter, 'tools') or "Read, Write, Edit, Grep, Glob, LS"
1847
-
1848
- # Convert tools string to list format
1849
- if isinstance(tools_line, str):
1850
- if tools_line.startswith('[') and tools_line.endswith(']'):
1851
- # Already in list format
1852
- tools_list = tools_line
1853
- else:
1854
- # Convert comma-separated to list
1855
- tools = [tool.strip() for tool in tools_line.split(',')]
1856
- tools_list = str(tools)
1857
- else:
1858
- tools_list = str(tools_line) if tools_line else '["Read", "Write", "Edit", "Grep", "Glob", "LS"]'
1859
-
1860
- # Build new YAML frontmatter
1861
- new_frontmatter = f"""---
1862
- name: {name}
1863
- description: "{description}"
1864
- version: "{version}"
1865
- author: "claude-mpm@anthropic.com"
1866
- created: "{datetime.now().isoformat()}Z"
1867
- updated: "{datetime.now().isoformat()}Z"
1868
- tags: ["{agent_name}", "mpm-framework"]
1869
- tools: {tools_list}
1870
- metadata:
1871
- deployment_type: "system"
1872
- converted_from: "yaml"
1873
- ---
1874
-
1875
- """
1876
-
1877
- return new_frontmatter + instructions
1878
-
1879
- def _extract_yaml_field(self, yaml_content: str, field_name: str) -> str:
1880
- """
1881
- Extract a field value from YAML content.
1882
-
1883
- Args:
1884
- yaml_content: YAML content string
1885
- field_name: Field name to extract
1886
-
1887
- Returns:
1888
- Field value or None if not found
1889
- """
1890
- import re
1891
-
1892
- try:
1893
- # Match field with quoted or unquoted values
1894
- pattern = rf'^{field_name}:\s*["\']?(.*?)["\']?\s*$'
1895
- match = re.search(pattern, yaml_content, re.MULTILINE)
1896
-
1897
- if match:
1898
- return match.group(1).strip()
1899
-
1900
- # Try with alternative spacing patterns
1901
- pattern = rf'^{field_name}\s*:\s*(.+)$'
1902
- match = re.search(pattern, yaml_content, re.MULTILINE)
1903
-
1904
- if match:
1905
- value = match.group(1).strip()
1906
- # Remove quotes if present
1907
- if (value.startswith('"') and value.endswith('"')) or \
1908
- (value.startswith("'") and value.endswith("'")):
1909
- value = value[1:-1]
1910
- return value
1911
-
1912
- except Exception as e:
1913
- self.logger.warning(f"Error extracting YAML field '{field_name}': {e}")
1914
-
1915
- return None
1916
-
1917
- def _try_async_deployment(self, target_dir: Optional[Path], force_rebuild: bool,
1918
- config: Optional[Config], deployment_start_time: float) -> Optional[Dict[str, Any]]:
1919
- """
1920
- Try to use async deployment for better performance.
1921
-
1922
- WHY: Async deployment is 50-70% faster than synchronous deployment
1923
- by using concurrent operations for file I/O and processing.
1924
-
1925
- Args:
1926
- target_dir: Target directory for deployment
1927
- force_rebuild: Whether to force rebuild
1928
- config: Configuration object
1929
- deployment_start_time: Start time for metrics
1930
-
1931
- Returns:
1932
- Deployment results if successful, None if async not available
1933
- """
1934
- try:
1935
- from .async_agent_deployment import deploy_agents_async_wrapper
1936
- self.logger.info("Using async deployment for improved performance")
1937
-
1938
- # Run async deployment
1939
- results = deploy_agents_async_wrapper(
1940
- templates_dir=self.templates_dir,
1941
- base_agent_path=self.base_agent_path,
1942
- working_directory=self.working_directory,
1943
- target_dir=target_dir,
1944
- force_rebuild=force_rebuild,
1945
- config=config
1946
- )
1947
-
1948
- # Add metrics about async vs sync
1949
- if 'metrics' in results:
1950
- results['metrics']['deployment_method'] = 'async'
1951
- duration_ms = results['metrics'].get('duration_ms', 0)
1952
- self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
1953
-
1954
- # Update internal metrics
1955
- self._deployment_metrics['total_deployments'] += 1
1956
- if not results.get('errors'):
1957
- self._deployment_metrics['successful_deployments'] += 1
1958
- else:
1959
- self._deployment_metrics['failed_deployments'] += 1
1960
-
1961
- return results
1962
-
1963
- except ImportError:
1964
- self.logger.warning("Async deployment not available, falling back to sync")
1965
- return None
1966
- except Exception as e:
1967
- self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
1968
- return None
1969
-
1970
- def _load_deployment_config(self, config: Optional[Config]) -> tuple:
1971
- """
1972
- Load and process deployment configuration.
1973
-
1974
- WHY: Centralized configuration loading reduces duplication
1975
- and ensures consistent handling of exclusion settings.
1976
-
1977
- Args:
1978
- config: Optional configuration object
1979
-
1980
- Returns:
1981
- Tuple of (config, excluded_agents)
1982
- """
1983
- # Load configuration if not provided
1984
- if config is None:
1985
- config = Config()
1986
-
1987
- # Get agent exclusion configuration
1988
- excluded_agents = config.get('agent_deployment.excluded_agents', [])
1989
- case_sensitive = config.get('agent_deployment.case_sensitive', False)
1990
- exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
1991
-
1992
- # Normalize excluded agents list for comparison
1993
- if not case_sensitive:
1994
- excluded_agents = [agent.lower() for agent in excluded_agents]
1995
-
1996
- # Log exclusion configuration if agents are being excluded
1997
- if excluded_agents:
1998
- self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
1999
- self.logger.debug(f"Case sensitive matching: {case_sensitive}")
2000
- self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
2001
-
2002
- return config, excluded_agents
2003
-
2004
- def _determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
2005
- """
2006
- Determine the correct agents directory based on input.
2007
-
2008
- WHY: Different deployment scenarios require different directory
2009
- structures. This method centralizes the logic for consistency.
2010
-
2011
- Args:
2012
- target_dir: Optional target directory
2013
-
2014
- Returns:
2015
- Path to agents directory
2016
- """
2017
- if not target_dir:
2018
- # Default to working directory's .claude/agents directory (not home)
2019
- # This ensures we deploy to the user's project directory
2020
- return self.working_directory / ".claude" / "agents"
2021
-
2022
- # If target_dir provided, use it directly (caller decides structure)
2023
- target_dir = Path(target_dir)
2024
-
2025
- # Check if this is already an agents directory
2026
- if target_dir.name == "agents":
2027
- # Already an agents directory, use as-is
2028
- return target_dir
2029
- elif target_dir.name == ".claude-mpm":
2030
- # .claude-mpm directory, add agents subdirectory
2031
- return target_dir / "agents"
2032
- elif target_dir.name == ".claude":
2033
- # .claude directory, add agents subdirectory
2034
- return target_dir / "agents"
2035
- else:
2036
- # Assume it's a project directory, add .claude/agents
2037
- return target_dir / ".claude" / "agents"
2038
-
2039
- def _initialize_deployment_results(self, agents_dir: Path, deployment_start_time: float) -> Dict[str, Any]:
2040
- """
2041
- Initialize the deployment results dictionary.
2042
-
2043
- WHY: Consistent result structure ensures all deployment
2044
- operations return the same format for easier processing.
2045
-
2046
- Args:
2047
- agents_dir: Target agents directory
2048
- deployment_start_time: Start time for metrics
2049
-
2050
- Returns:
2051
- Initialized results dictionary
2052
- """
2053
- return {
2054
- "target_dir": str(agents_dir),
2055
- "deployed": [],
2056
- "errors": [],
2057
- "skipped": [],
2058
- "updated": [],
2059
- "migrated": [], # Track agents migrated from old format
2060
- "converted": [], # Track YAML to MD conversions
2061
- "repaired": [], # Track agents with repaired frontmatter
2062
- "total": 0,
2063
- # METRICS: Add detailed timing and performance data to results
2064
- "metrics": {
2065
- "start_time": deployment_start_time,
2066
- "end_time": None,
2067
- "duration_ms": None,
2068
- "agent_timings": {}, # Track individual agent deployment times
2069
- "validation_times": {}, # Track template validation times
2070
- "resource_usage": {} # Could track memory/CPU if needed
2071
- }
2072
- }
2073
-
2074
- def _repair_existing_agents(self, agents_dir: Path, results: Dict[str, Any]) -> None:
2075
- """
2076
- Validate and repair broken frontmatter in existing agents.
2077
-
2078
- WHY: Ensures all existing agents have valid YAML frontmatter
2079
- before deployment, preventing runtime errors in Claude Code.
2080
-
2081
- Args:
2082
- agents_dir: Directory containing agent files
2083
- results: Results dictionary to update
2084
- """
2085
- repair_results = self._validate_and_repair_existing_agents(agents_dir)
2086
- if repair_results["repaired"]:
2087
- results["repaired"] = repair_results["repaired"]
2088
- self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
2089
- for agent_name in repair_results["repaired"]:
2090
- self.logger.debug(f" - Repaired: {agent_name}")
2091
-
2092
- def _determine_source_tier(self) -> str:
2093
- """
2094
- Determine the source tier for logging.
2095
-
2096
- WHY: Understanding which tier (SYSTEM/USER/PROJECT) agents
2097
- are being deployed from helps with debugging and auditing.
2098
-
2099
- Returns:
2100
- Source tier string
2101
- """
2102
- if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2103
- return "PROJECT"
2104
- elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
2105
- return "USER"
2106
- return "SYSTEM"
2107
-
2108
- def _load_base_agent(self) -> tuple:
2109
- """
2110
- Load base agent content and version.
2111
-
2112
- WHY: Base agent contains shared configuration that all agents
2113
- inherit, reducing duplication and ensuring consistency.
2114
-
2115
- Returns:
2116
- Tuple of (base_agent_data, base_agent_version)
2117
- """
2118
- base_agent_data = {}
2119
- base_agent_version = (0, 0, 0)
2120
-
2121
- if self.base_agent_path.exists():
2122
- try:
2123
- import json
2124
- base_agent_data = json.loads(self.base_agent_path.read_text())
2125
- # Handle both 'base_version' (new format) and 'version' (old format)
2126
- # MIGRATION PATH: Supporting both formats during transition period
2127
- base_agent_version = self._parse_version(
2128
- base_agent_data.get('base_version') or base_agent_data.get('version', 0)
2129
- )
2130
- self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
2131
- except Exception as e:
2132
- # NON-FATAL: Base agent is optional enhancement, not required
2133
- self.logger.warning(f"Could not load base agent: {e}")
2134
-
2135
- return base_agent_data, base_agent_version
2136
-
2137
- def _get_filtered_templates(self, excluded_agents: list, config: Config) -> list:
2138
- """
2139
- Get and filter template files based on exclusion rules.
2140
-
2141
- WHY: Centralized filtering logic ensures consistent exclusion
2142
- handling across different deployment scenarios.
2143
-
2144
- Args:
2145
- excluded_agents: List of agents to exclude
2146
- config: Configuration object
2147
-
2148
- Returns:
2149
- List of filtered template files
2150
- """
2151
- # Get all template files
2152
- template_files = list(self.templates_dir.glob("*.json"))
2153
-
2154
- # Build the combined exclusion set
2155
- # Start with hardcoded exclusions (these are ALWAYS excluded)
2156
- hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS",
2157
- "README", "pm", "PM", "project_manager"}
2158
-
2159
- # Get case sensitivity setting
2160
- case_sensitive = config.get('agent_deployment.case_sensitive', False)
2161
-
2162
- # Filter out excluded agents
2163
- filtered_files = []
2164
- excluded_count = 0
2165
-
2166
- for f in template_files:
2167
- agent_name = f.stem
2168
-
2169
- # Check hardcoded exclusions (always case-sensitive)
2170
- if agent_name in hardcoded_exclusions:
2171
- self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
2172
- excluded_count += 1
2173
- continue
2174
-
2175
- # Check file patterns
2176
- if agent_name.startswith(".") or agent_name.endswith(".backup"):
2177
- self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
2178
- excluded_count += 1
2179
- continue
2180
-
2181
- # Check user-configured exclusions
2182
- compare_name = agent_name.lower() if not case_sensitive else agent_name
2183
- if compare_name in excluded_agents:
2184
- self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
2185
- excluded_count += 1
2186
- continue
2187
-
2188
- # Agent is not excluded, add to filtered list
2189
- filtered_files.append(f)
2190
-
2191
- if excluded_count > 0:
2192
- self.logger.info(f"Excluded {excluded_count} agents from deployment")
2193
-
2194
- return filtered_files
2195
-
2196
- def _deploy_single_agent(self, template_file: Path, agents_dir: Path,
2197
- base_agent_data: dict, base_agent_version: tuple,
2198
- force_rebuild: bool, deployment_mode: str,
2199
- results: Dict[str, Any]) -> None:
2200
- """
2201
- Deploy a single agent template.
2202
-
2203
- WHY: Extracting single agent deployment logic reduces complexity
2204
- and makes the main deployment loop more readable.
2205
-
2206
- Args:
2207
- template_file: Agent template file
2208
- agents_dir: Target agents directory
2209
- base_agent_data: Base agent data
2210
- base_agent_version: Base agent version
2211
- force_rebuild: Whether to force rebuild
2212
- deployment_mode: Deployment mode (update/project)
2213
- results: Results dictionary to update
2214
- """
2215
- try:
2216
- # METRICS: Track individual agent deployment time
2217
- agent_start_time = time.time()
2218
-
2219
- agent_name = template_file.stem
2220
- target_file = agents_dir / f"{agent_name}.md"
2221
-
2222
- # Check if agent needs update
2223
- needs_update, is_migration, reason = self._check_update_status(
2224
- target_file, template_file, base_agent_version,
2225
- force_rebuild, deployment_mode
2226
- )
2227
-
2228
- # Skip if exists and doesn't need update (only in update mode)
2229
- if target_file.exists() and not needs_update and deployment_mode != "project":
2230
- results["skipped"].append(agent_name)
2231
- self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
2232
- return
2233
-
2234
- # Build the agent file as markdown with YAML frontmatter
2235
- agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
2236
-
2237
- # Write the agent file
2238
- is_update = target_file.exists()
2239
- target_file.write_text(agent_content)
2240
-
2241
- # Record metrics and update results
2242
- self._record_agent_deployment(
2243
- agent_name, template_file, target_file,
2244
- is_update, is_migration, reason,
2245
- agent_start_time, results
2246
- )
2247
-
2248
- except AgentDeploymentError as e:
2249
- # Re-raise our custom exceptions
2250
- self.logger.error(str(e))
2251
- results["errors"].append(str(e))
2252
- except Exception as e:
2253
- # Wrap generic exceptions with context
2254
- error_msg = f"Failed to build {template_file.name}: {e}"
2255
- self.logger.error(error_msg)
2256
- results["errors"].append(error_msg)
2257
-
2258
- def _check_update_status(self, target_file: Path, template_file: Path,
2259
- base_agent_version: tuple, force_rebuild: bool,
2260
- deployment_mode: str) -> tuple:
2261
- """
2262
- Check if agent needs update and determine status.
2263
-
2264
- WHY: Centralized update checking logic ensures consistent
2265
- version comparison and migration detection.
2266
-
2267
- Args:
2268
- target_file: Target agent file
2269
- template_file: Template file
2270
- base_agent_version: Base agent version
2271
- force_rebuild: Whether to force rebuild
2272
- deployment_mode: Deployment mode
2273
-
2274
- Returns:
2275
- Tuple of (needs_update, is_migration, reason)
2276
- """
2277
- needs_update = force_rebuild
2278
- is_migration = False
2279
- reason = ""
2280
-
2281
- # In project deployment mode, always deploy regardless of version
2282
- if deployment_mode == "project":
2283
- if target_file.exists():
2284
- needs_update = True
2285
- self.logger.debug(f"Project deployment mode: will deploy {template_file.stem}")
2286
- else:
2287
- needs_update = True
2288
- elif not needs_update and target_file.exists():
2289
- # In update mode, check version compatibility
2290
- needs_update, reason = self._check_agent_needs_update(
2291
- target_file, template_file, base_agent_version
2292
- )
2293
- if needs_update:
2294
- # Check if this is a migration from old format
2295
- if "migration needed" in reason:
2296
- is_migration = True
2297
- self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
2298
- else:
2299
- self.logger.info(f"Agent {template_file.stem} needs update: {reason}")
2300
-
2301
- return needs_update, is_migration, reason
2302
-
2303
- def _record_agent_deployment(self, agent_name: str, template_file: Path,
2304
- target_file: Path, is_update: bool,
2305
- is_migration: bool, reason: str,
2306
- agent_start_time: float, results: Dict[str, Any]) -> None:
2307
- """
2308
- Record deployment metrics and update results.
2309
-
2310
- WHY: Centralized metrics recording ensures consistent tracking
2311
- of deployment performance and statistics.
2312
-
2313
- Args:
2314
- agent_name: Name of the agent
2315
- template_file: Template file
2316
- target_file: Target file
2317
- is_update: Whether this is an update
2318
- is_migration: Whether this is a migration
2319
- reason: Update/migration reason
2320
- agent_start_time: Start time for this agent
2321
- results: Results dictionary to update
2322
- """
2323
- # METRICS: Record deployment time for this agent
2324
- agent_deployment_time = (time.time() - agent_start_time) * 1000 # Convert to ms
2325
- results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
2326
-
2327
- # METRICS: Update agent type deployment counts
2328
- self._deployment_metrics['agent_type_counts'][agent_name] = \
2329
- self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
2330
-
2331
- deployment_info = {
2332
- "name": agent_name,
2333
- "template": str(template_file),
2334
- "target": str(target_file),
2335
- "deployment_time_ms": agent_deployment_time
2336
- }
2337
-
2338
- if is_migration:
2339
- deployment_info["reason"] = reason
2340
- results["migrated"].append(deployment_info)
2341
- self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
2342
-
2343
- # METRICS: Track migration statistics
2344
- self._deployment_metrics['migrations_performed'] += 1
2345
- self._deployment_metrics['version_migration_count'] += 1
2346
-
2347
- elif is_update:
2348
- results["updated"].append(deployment_info)
2349
- self.logger.debug(f"Updated agent: {agent_name}")
2350
- else:
2351
- results["deployed"].append(deployment_info)
2352
- self.logger.debug(f"Built and deployed agent: {agent_name}")
2353
-
2354
- def _validate_and_repair_existing_agents(self, agents_dir: Path) -> Dict[str, Any]:
2355
- """
2356
- Validate and repair broken frontmatter in existing agent files.
2357
-
2358
- This method scans existing .claude/agents/*.md files and validates their
2359
- frontmatter. If the frontmatter is broken or missing, it attempts to repair
2360
- it or marks the agent for replacement during deployment.
2361
-
2362
- WHY: Ensures all existing agents have valid YAML frontmatter before deployment,
2363
- preventing runtime errors in Claude Code when loading agents.
2364
-
2365
- Args:
2366
- agents_dir: Directory containing agent .md files
2367
-
2368
- Returns:
2369
- Dictionary with validation results:
2370
- - repaired: List of agent names that were repaired
2371
- - replaced: List of agent names marked for replacement
2372
- - errors: List of validation errors
2373
- """
2374
- results = {
2375
- "repaired": [],
2376
- "replaced": [],
2377
- "errors": []
2378
- }
2379
-
2380
- try:
2381
- # Import frontmatter validator
2382
- from claude_mpm.agents.frontmatter_validator import FrontmatterValidator
2383
- validator = FrontmatterValidator()
2384
-
2385
- # Find existing agent files
2386
- agent_files = list(agents_dir.glob("*.md"))
2387
-
2388
- if not agent_files:
2389
- self.logger.debug("No existing agent files to validate")
2390
- return results
2391
-
2392
- self.logger.debug(f"Validating frontmatter in {len(agent_files)} existing agents")
2393
-
2394
- for agent_file in agent_files:
2395
- try:
2396
- agent_name = agent_file.stem
2397
-
2398
- # Read agent file content
2399
- content = agent_file.read_text()
2400
-
2401
- # Check if this is a system agent (authored by claude-mpm)
2402
- # Only repair system agents, leave user agents alone
2403
- if "author: claude-mpm" not in content and "author: 'claude-mpm'" not in content:
2404
- self.logger.debug(f"Skipping validation for user agent: {agent_name}")
2405
- continue
2406
-
2407
- # Extract and validate frontmatter
2408
- if not content.startswith("---"):
2409
- # No frontmatter at all - mark for replacement
2410
- self.logger.warning(f"Agent {agent_name} has no frontmatter, marking for replacement")
2411
- results["replaced"].append(agent_name)
2412
- # Delete the file so it will be recreated
2413
- agent_file.unlink()
2414
- continue
2415
-
2416
- # Try to extract frontmatter
2417
- try:
2418
- end_marker = content.find("\n---\n", 4)
2419
- if end_marker == -1:
2420
- end_marker = content.find("\n---\r\n", 4)
2421
-
2422
- if end_marker == -1:
2423
- # Broken frontmatter - mark for replacement
2424
- self.logger.warning(f"Agent {agent_name} has broken frontmatter, marking for replacement")
2425
- results["replaced"].append(agent_name)
2426
- # Delete the file so it will be recreated
2427
- agent_file.unlink()
2428
- continue
2429
-
2430
- # Validate frontmatter with the validator
2431
- validation_result = validator.validate_file(agent_file)
2432
-
2433
- if not validation_result.is_valid:
2434
- # Check if it can be corrected
2435
- if validation_result.corrected_frontmatter:
2436
- # Apply corrections
2437
- correction_result = validator.correct_file(agent_file, dry_run=False)
2438
- if correction_result.corrections:
2439
- results["repaired"].append(agent_name)
2440
- self.logger.info(f"Repaired frontmatter for agent {agent_name}")
2441
- for correction in correction_result.corrections:
2442
- self.logger.debug(f" - {correction}")
2443
- else:
2444
- # Cannot be corrected - mark for replacement
2445
- self.logger.warning(f"Agent {agent_name} has invalid frontmatter that cannot be repaired, marking for replacement")
2446
- results["replaced"].append(agent_name)
2447
- # Delete the file so it will be recreated
2448
- agent_file.unlink()
2449
- elif validation_result.warnings:
2450
- # Has warnings but is valid
2451
- for warning in validation_result.warnings:
2452
- self.logger.debug(f"Agent {agent_name} warning: {warning}")
2453
-
2454
- except Exception as e:
2455
- # Any error in parsing - mark for replacement
2456
- self.logger.warning(f"Failed to parse frontmatter for {agent_name}: {e}, marking for replacement")
2457
- results["replaced"].append(agent_name)
2458
- # Delete the file so it will be recreated
2459
- try:
2460
- agent_file.unlink()
2461
- except Exception:
2462
- pass
2463
-
2464
- except Exception as e:
2465
- error_msg = f"Failed to validate agent {agent_file.name}: {e}"
2466
- self.logger.error(error_msg)
2467
- results["errors"].append(error_msg)
2468
-
2469
- except ImportError:
2470
- self.logger.warning("FrontmatterValidator not available, skipping validation")
2471
- except Exception as e:
2472
- error_msg = f"Agent validation failed: {e}"
2473
- self.logger.error(error_msg)
2474
- results["errors"].append(error_msg)
2475
-
2476
- return results
2477
-
2478
- # ================================================================================
2479
- # Interface Adapter Methods
2480
- # ================================================================================
2481
- # These methods adapt the existing implementation to comply with AgentDeploymentInterface
2482
-
2483
- def validate_agent(self, agent_path: Path) -> tuple[bool, List[str]]:
2484
- """Validate agent configuration and structure.
2485
-
2486
- WHY: This adapter method provides interface compliance while leveraging
2487
- the existing validation logic in _check_agent_needs_update and other methods.
2488
-
2489
- Args:
2490
- agent_path: Path to agent configuration file
2491
-
2492
- Returns:
2493
- Tuple of (is_valid, list_of_errors)
2494
- """
2495
- errors = []
2496
-
2497
- try:
2498
- if not agent_path.exists():
2499
- return False, [f"Agent file not found: {agent_path}"]
2500
-
2501
- content = agent_path.read_text()
2502
-
2503
- # Check YAML frontmatter format
2504
- if not content.startswith("---"):
2505
- errors.append("Missing YAML frontmatter")
2506
-
2507
- # Extract and validate version
2508
- import re
2509
- version_match = re.search(r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE)
2510
- if not version_match:
2511
- errors.append("Missing version field in frontmatter")
2512
-
2513
- # Check for required fields
2514
- required_fields = ['name', 'description', 'tools']
2515
- for field in required_fields:
2516
- field_match = re.search(rf'^{field}:\s*.+$', content, re.MULTILINE)
2517
- if not field_match:
2518
- errors.append(f"Missing required field: {field}")
2519
-
2520
- # If no errors, validation passed
2521
- return len(errors) == 0, errors
2522
-
2523
- except Exception as e:
2524
- return False, [f"Validation error: {str(e)}"]
2525
-
2526
- def get_deployment_status(self) -> Dict[str, Any]:
2527
- """Get current deployment status and metrics.
2528
-
2529
- WHY: This adapter method provides interface compliance by wrapping
2530
- verify_deployment and adding deployment metrics.
2531
-
2532
- Returns:
2533
- Dictionary with deployment status information
2534
- """
2535
- # Get verification results
2536
- verification = self.verify_deployment()
2537
-
2538
- # Add deployment metrics
2539
- status = {
2540
- "deployment_metrics": self._deployment_metrics.copy(),
2541
- "verification": verification,
2542
- "agents_deployed": len(verification.get("agents_found", [])),
2543
- "agents_needing_migration": len(verification.get("agents_needing_migration", [])),
2544
- "has_warnings": len(verification.get("warnings", [])) > 0,
2545
- "environment_configured": bool(verification.get("environment", {}))
2546
- }
2547
-
2548
- return status