claude-mpm 4.1.1__py3-none-any.whl → 4.1.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 (389) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/__main__.py +1 -1
  4. claude_mpm/agents/BASE_PM.md +74 -46
  5. claude_mpm/agents/INSTRUCTIONS.md +11 -153
  6. claude_mpm/agents/WORKFLOW.md +61 -321
  7. claude_mpm/agents/__init__.py +11 -11
  8. claude_mpm/agents/agent_loader.py +23 -20
  9. claude_mpm/agents/agent_loader_integration.py +1 -1
  10. claude_mpm/agents/agents_metadata.py +27 -0
  11. claude_mpm/agents/async_agent_loader.py +5 -8
  12. claude_mpm/agents/base_agent_loader.py +36 -25
  13. claude_mpm/agents/frontmatter_validator.py +6 -6
  14. claude_mpm/agents/schema/agent_schema.json +1 -1
  15. claude_mpm/agents/system_agent_config.py +9 -9
  16. claude_mpm/agents/templates/api_qa.json +47 -2
  17. claude_mpm/agents/templates/engineer.json +33 -11
  18. claude_mpm/agents/templates/imagemagick.json +256 -0
  19. claude_mpm/agents/templates/qa.json +41 -2
  20. claude_mpm/agents/templates/ticketing.json +5 -5
  21. claude_mpm/agents/templates/web_qa.json +50 -2
  22. claude_mpm/cli/__init__.py +51 -46
  23. claude_mpm/cli/__main__.py +1 -1
  24. claude_mpm/cli/commands/__init__.py +10 -12
  25. claude_mpm/cli/commands/agent_manager.py +186 -181
  26. claude_mpm/cli/commands/agents.py +648 -1098
  27. claude_mpm/cli/commands/aggregate.py +30 -29
  28. claude_mpm/cli/commands/cleanup.py +50 -44
  29. claude_mpm/cli/commands/cleanup_orphaned_agents.py +25 -25
  30. claude_mpm/cli/commands/config.py +162 -127
  31. claude_mpm/cli/commands/doctor.py +52 -62
  32. claude_mpm/cli/commands/info.py +37 -25
  33. claude_mpm/cli/commands/mcp.py +3 -7
  34. claude_mpm/cli/commands/mcp_command_router.py +14 -18
  35. claude_mpm/cli/commands/mcp_install_commands.py +28 -23
  36. claude_mpm/cli/commands/mcp_pipx_config.py +58 -49
  37. claude_mpm/cli/commands/mcp_server_commands.py +23 -17
  38. claude_mpm/cli/commands/memory.py +339 -967
  39. claude_mpm/cli/commands/monitor.py +117 -88
  40. claude_mpm/cli/commands/run.py +233 -542
  41. claude_mpm/cli/commands/socketio_monitor.py +17 -19
  42. claude_mpm/cli/commands/tickets.py +92 -92
  43. claude_mpm/cli/parser.py +1 -5
  44. claude_mpm/cli/parsers/__init__.py +1 -1
  45. claude_mpm/cli/parsers/agent_manager_parser.py +50 -98
  46. claude_mpm/cli/parsers/agents_parser.py +2 -3
  47. claude_mpm/cli/parsers/base_parser.py +7 -5
  48. claude_mpm/cli/parsers/mcp_parser.py +4 -2
  49. claude_mpm/cli/parsers/monitor_parser.py +26 -18
  50. claude_mpm/cli/shared/__init__.py +10 -10
  51. claude_mpm/cli/shared/argument_patterns.py +57 -71
  52. claude_mpm/cli/shared/base_command.py +61 -53
  53. claude_mpm/cli/shared/error_handling.py +62 -58
  54. claude_mpm/cli/shared/output_formatters.py +78 -77
  55. claude_mpm/cli/startup_logging.py +280 -172
  56. claude_mpm/cli/utils.py +10 -11
  57. claude_mpm/cli_module/__init__.py +1 -1
  58. claude_mpm/cli_module/args.py +1 -1
  59. claude_mpm/cli_module/migration_example.py +5 -5
  60. claude_mpm/config/__init__.py +9 -9
  61. claude_mpm/config/agent_config.py +15 -14
  62. claude_mpm/config/experimental_features.py +4 -4
  63. claude_mpm/config/paths.py +0 -1
  64. claude_mpm/config/socketio_config.py +5 -6
  65. claude_mpm/constants.py +1 -2
  66. claude_mpm/core/__init__.py +8 -8
  67. claude_mpm/core/agent_name_normalizer.py +1 -1
  68. claude_mpm/core/agent_registry.py +22 -29
  69. claude_mpm/core/agent_session_manager.py +3 -3
  70. claude_mpm/core/base_service.py +7 -15
  71. claude_mpm/core/cache.py +4 -6
  72. claude_mpm/core/claude_runner.py +85 -113
  73. claude_mpm/core/config.py +43 -28
  74. claude_mpm/core/config_aliases.py +0 -9
  75. claude_mpm/core/config_constants.py +52 -30
  76. claude_mpm/core/constants.py +0 -1
  77. claude_mpm/core/container.py +18 -27
  78. claude_mpm/core/exceptions.py +2 -2
  79. claude_mpm/core/factories.py +10 -12
  80. claude_mpm/core/framework_loader.py +500 -680
  81. claude_mpm/core/hook_manager.py +26 -22
  82. claude_mpm/core/hook_performance_config.py +58 -47
  83. claude_mpm/core/injectable_service.py +1 -1
  84. claude_mpm/core/interactive_session.py +61 -152
  85. claude_mpm/core/interfaces.py +1 -100
  86. claude_mpm/core/lazy.py +5 -5
  87. claude_mpm/core/log_manager.py +587 -0
  88. claude_mpm/core/logger.py +125 -8
  89. claude_mpm/core/logging_config.py +15 -17
  90. claude_mpm/core/minimal_framework_loader.py +5 -8
  91. claude_mpm/core/oneshot_session.py +15 -33
  92. claude_mpm/core/optimized_agent_loader.py +4 -6
  93. claude_mpm/core/optimized_startup.py +2 -1
  94. claude_mpm/core/output_style_manager.py +147 -106
  95. claude_mpm/core/pm_hook_interceptor.py +0 -1
  96. claude_mpm/core/service_registry.py +11 -8
  97. claude_mpm/core/session_manager.py +1 -2
  98. claude_mpm/core/shared/__init__.py +1 -1
  99. claude_mpm/core/shared/config_loader.py +101 -97
  100. claude_mpm/core/shared/path_resolver.py +72 -68
  101. claude_mpm/core/shared/singleton_manager.py +56 -50
  102. claude_mpm/core/socketio_pool.py +26 -6
  103. claude_mpm/core/tool_access_control.py +4 -5
  104. claude_mpm/core/typing_utils.py +50 -59
  105. claude_mpm/core/unified_agent_registry.py +14 -19
  106. claude_mpm/core/unified_config.py +4 -6
  107. claude_mpm/core/unified_paths.py +197 -109
  108. claude_mpm/dashboard/open_dashboard.py +2 -4
  109. claude_mpm/experimental/cli_enhancements.py +51 -36
  110. claude_mpm/generators/agent_profile_generator.py +2 -4
  111. claude_mpm/hooks/base_hook.py +1 -2
  112. claude_mpm/hooks/claude_hooks/connection_pool.py +72 -26
  113. claude_mpm/hooks/claude_hooks/event_handlers.py +99 -154
  114. claude_mpm/hooks/claude_hooks/hook_handler.py +110 -720
  115. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +104 -77
  116. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  117. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  118. claude_mpm/hooks/claude_hooks/memory_integration.py +2 -4
  119. claude_mpm/hooks/claude_hooks/response_tracking.py +15 -11
  120. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  121. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  122. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  123. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  124. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  125. claude_mpm/hooks/claude_hooks/tool_analysis.py +12 -18
  126. claude_mpm/hooks/memory_integration_hook.py +5 -5
  127. claude_mpm/hooks/tool_call_interceptor.py +1 -1
  128. claude_mpm/hooks/validation_hooks.py +4 -4
  129. claude_mpm/init.py +4 -9
  130. claude_mpm/models/__init__.py +2 -2
  131. claude_mpm/models/agent_session.py +11 -14
  132. claude_mpm/scripts/mcp_server.py +20 -11
  133. claude_mpm/scripts/mcp_wrapper.py +5 -5
  134. claude_mpm/scripts/mpm_doctor.py +321 -0
  135. claude_mpm/scripts/socketio_daemon.py +28 -25
  136. claude_mpm/scripts/socketio_daemon_hardened.py +298 -258
  137. claude_mpm/scripts/socketio_server_manager.py +116 -95
  138. claude_mpm/services/__init__.py +49 -49
  139. claude_mpm/services/agent_capabilities_service.py +12 -18
  140. claude_mpm/services/agents/__init__.py +22 -22
  141. claude_mpm/services/agents/agent_builder.py +140 -119
  142. claude_mpm/services/agents/deployment/__init__.py +3 -3
  143. claude_mpm/services/agents/deployment/agent_config_provider.py +9 -9
  144. claude_mpm/services/agents/deployment/agent_configuration_manager.py +19 -20
  145. claude_mpm/services/agents/deployment/agent_definition_factory.py +1 -5
  146. claude_mpm/services/agents/deployment/agent_deployment.py +129 -511
  147. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -8
  148. claude_mpm/services/agents/deployment/agent_environment_manager.py +2 -7
  149. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +6 -10
  150. claude_mpm/services/agents/deployment/agent_format_converter.py +11 -15
  151. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +2 -3
  152. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +5 -5
  153. claude_mpm/services/agents/deployment/agent_metrics_collector.py +13 -19
  154. claude_mpm/services/agents/deployment/agent_restore_handler.py +0 -1
  155. claude_mpm/services/agents/deployment/agent_template_builder.py +26 -35
  156. claude_mpm/services/agents/deployment/agent_validator.py +0 -1
  157. claude_mpm/services/agents/deployment/agent_version_manager.py +7 -9
  158. claude_mpm/services/agents/deployment/agent_versioning.py +3 -3
  159. claude_mpm/services/agents/deployment/agents_directory_resolver.py +6 -7
  160. claude_mpm/services/agents/deployment/async_agent_deployment.py +51 -38
  161. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  162. claude_mpm/services/agents/deployment/config/__init__.py +1 -1
  163. claude_mpm/services/agents/deployment/config/deployment_config.py +7 -8
  164. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  165. claude_mpm/services/agents/deployment/deployment_type_detector.py +1 -1
  166. claude_mpm/services/agents/deployment/deployment_wrapper.py +18 -18
  167. claude_mpm/services/agents/deployment/facade/__init__.py +1 -1
  168. claude_mpm/services/agents/deployment/facade/deployment_executor.py +0 -3
  169. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -4
  170. claude_mpm/services/agents/deployment/interface_adapter.py +5 -7
  171. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +345 -276
  172. claude_mpm/services/agents/deployment/pipeline/__init__.py +2 -2
  173. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +1 -1
  174. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +6 -4
  175. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +3 -3
  176. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +2 -2
  177. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +14 -13
  178. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +0 -1
  179. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +1 -1
  180. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +8 -9
  181. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +1 -1
  182. claude_mpm/services/agents/deployment/processors/__init__.py +1 -1
  183. claude_mpm/services/agents/deployment/processors/agent_processor.py +20 -16
  184. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +5 -12
  185. claude_mpm/services/agents/deployment/results/__init__.py +1 -1
  186. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +1 -1
  187. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  188. claude_mpm/services/agents/deployment/strategies/__init__.py +2 -2
  189. claude_mpm/services/agents/deployment/strategies/base_strategy.py +1 -7
  190. claude_mpm/services/agents/deployment/strategies/project_strategy.py +1 -4
  191. claude_mpm/services/agents/deployment/strategies/system_strategy.py +2 -3
  192. claude_mpm/services/agents/deployment/strategies/user_strategy.py +3 -7
  193. claude_mpm/services/agents/deployment/validation/__init__.py +1 -1
  194. claude_mpm/services/agents/deployment/validation/agent_validator.py +1 -1
  195. claude_mpm/services/agents/deployment/validation/template_validator.py +2 -2
  196. claude_mpm/services/agents/deployment/validation/validation_result.py +2 -6
  197. claude_mpm/services/agents/loading/__init__.py +1 -1
  198. claude_mpm/services/agents/loading/agent_profile_loader.py +6 -12
  199. claude_mpm/services/agents/loading/base_agent_manager.py +5 -5
  200. claude_mpm/services/agents/loading/framework_agent_loader.py +2 -4
  201. claude_mpm/services/agents/management/__init__.py +1 -1
  202. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -3
  203. claude_mpm/services/agents/management/agent_management_service.py +5 -9
  204. claude_mpm/services/agents/memory/__init__.py +4 -4
  205. claude_mpm/services/agents/memory/agent_memory_manager.py +157 -503
  206. claude_mpm/services/agents/memory/agent_persistence_service.py +0 -2
  207. claude_mpm/services/agents/memory/content_manager.py +44 -38
  208. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  209. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  210. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  211. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  212. claude_mpm/services/agents/memory/template_generator.py +4 -6
  213. claude_mpm/services/agents/registry/__init__.py +11 -7
  214. claude_mpm/services/agents/registry/deployed_agent_discovery.py +30 -27
  215. claude_mpm/services/agents/registry/modification_tracker.py +3 -6
  216. claude_mpm/services/async_session_logger.py +1 -2
  217. claude_mpm/services/claude_session_logger.py +1 -2
  218. claude_mpm/services/cli/__init__.py +18 -0
  219. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  220. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  221. claude_mpm/services/cli/agent_listing_service.py +463 -0
  222. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  223. claude_mpm/services/cli/agent_validation_service.py +589 -0
  224. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  225. claude_mpm/services/cli/memory_crud_service.py +617 -0
  226. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  227. claude_mpm/services/cli/session_manager.py +513 -0
  228. claude_mpm/services/cli/socketio_manager.py +498 -0
  229. claude_mpm/services/cli/startup_checker.py +370 -0
  230. claude_mpm/services/command_deployment_service.py +173 -0
  231. claude_mpm/services/command_handler_service.py +20 -22
  232. claude_mpm/services/core/__init__.py +25 -25
  233. claude_mpm/services/core/base.py +0 -5
  234. claude_mpm/services/core/cache_manager.py +311 -0
  235. claude_mpm/services/core/interfaces/__init__.py +32 -32
  236. claude_mpm/services/core/interfaces/agent.py +0 -21
  237. claude_mpm/services/core/interfaces/communication.py +0 -27
  238. claude_mpm/services/core/interfaces/infrastructure.py +0 -56
  239. claude_mpm/services/core/interfaces/service.py +0 -29
  240. claude_mpm/services/core/memory_manager.py +637 -0
  241. claude_mpm/services/core/path_resolver.py +498 -0
  242. claude_mpm/services/core/service_container.py +520 -0
  243. claude_mpm/services/core/service_interfaces.py +436 -0
  244. claude_mpm/services/diagnostics/__init__.py +1 -1
  245. claude_mpm/services/diagnostics/checks/__init__.py +6 -6
  246. claude_mpm/services/diagnostics/checks/agent_check.py +152 -97
  247. claude_mpm/services/diagnostics/checks/base_check.py +12 -16
  248. claude_mpm/services/diagnostics/checks/claude_desktop_check.py +84 -81
  249. claude_mpm/services/diagnostics/checks/common_issues_check.py +99 -91
  250. claude_mpm/services/diagnostics/checks/configuration_check.py +82 -77
  251. claude_mpm/services/diagnostics/checks/filesystem_check.py +67 -68
  252. claude_mpm/services/diagnostics/checks/installation_check.py +254 -94
  253. claude_mpm/services/diagnostics/checks/mcp_check.py +90 -88
  254. claude_mpm/services/diagnostics/checks/monitor_check.py +75 -76
  255. claude_mpm/services/diagnostics/checks/startup_log_check.py +67 -73
  256. claude_mpm/services/diagnostics/diagnostic_runner.py +67 -59
  257. claude_mpm/services/diagnostics/doctor_reporter.py +107 -70
  258. claude_mpm/services/diagnostics/models.py +21 -19
  259. claude_mpm/services/event_aggregator.py +10 -17
  260. claude_mpm/services/event_bus/__init__.py +1 -1
  261. claude_mpm/services/event_bus/config.py +54 -35
  262. claude_mpm/services/event_bus/event_bus.py +76 -71
  263. claude_mpm/services/event_bus/relay.py +74 -64
  264. claude_mpm/services/events/__init__.py +11 -11
  265. claude_mpm/services/events/consumers/__init__.py +3 -3
  266. claude_mpm/services/events/consumers/dead_letter.py +71 -63
  267. claude_mpm/services/events/consumers/logging.py +39 -37
  268. claude_mpm/services/events/consumers/metrics.py +56 -57
  269. claude_mpm/services/events/consumers/socketio.py +82 -81
  270. claude_mpm/services/events/core.py +110 -99
  271. claude_mpm/services/events/interfaces.py +56 -72
  272. claude_mpm/services/events/producers/__init__.py +1 -1
  273. claude_mpm/services/events/producers/hook.py +38 -38
  274. claude_mpm/services/events/producers/system.py +46 -44
  275. claude_mpm/services/exceptions.py +81 -80
  276. claude_mpm/services/framework_claude_md_generator/__init__.py +2 -4
  277. claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -5
  278. claude_mpm/services/framework_claude_md_generator/content_validator.py +1 -1
  279. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +4 -4
  280. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +0 -1
  281. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +0 -2
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +4 -5
  283. claude_mpm/services/hook_service.py +6 -9
  284. claude_mpm/services/infrastructure/__init__.py +1 -1
  285. claude_mpm/services/infrastructure/context_preservation.py +8 -12
  286. claude_mpm/services/infrastructure/monitoring.py +21 -23
  287. claude_mpm/services/mcp_gateway/__init__.py +37 -37
  288. claude_mpm/services/mcp_gateway/auto_configure.py +95 -103
  289. claude_mpm/services/mcp_gateway/config/__init__.py +1 -1
  290. claude_mpm/services/mcp_gateway/config/config_loader.py +23 -25
  291. claude_mpm/services/mcp_gateway/config/config_schema.py +5 -5
  292. claude_mpm/services/mcp_gateway/config/configuration.py +9 -6
  293. claude_mpm/services/mcp_gateway/core/__init__.py +10 -10
  294. claude_mpm/services/mcp_gateway/core/base.py +0 -3
  295. claude_mpm/services/mcp_gateway/core/interfaces.py +1 -38
  296. claude_mpm/services/mcp_gateway/core/process_pool.py +99 -93
  297. claude_mpm/services/mcp_gateway/core/singleton_manager.py +65 -62
  298. claude_mpm/services/mcp_gateway/core/startup_verification.py +75 -74
  299. claude_mpm/services/mcp_gateway/main.py +2 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +5 -8
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +1 -1
  302. claude_mpm/services/mcp_gateway/server/__init__.py +1 -1
  303. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +12 -19
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +4 -3
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +79 -71
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +2 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +5 -6
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +13 -22
  309. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +79 -78
  310. claude_mpm/services/mcp_gateway/tools/hello_world.py +12 -14
  311. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +42 -49
  312. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +51 -55
  313. claude_mpm/services/memory/__init__.py +3 -3
  314. claude_mpm/services/memory/builder.py +3 -6
  315. claude_mpm/services/memory/cache/__init__.py +1 -1
  316. claude_mpm/services/memory/cache/shared_prompt_cache.py +3 -5
  317. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  318. claude_mpm/services/memory/indexed_memory.py +5 -7
  319. claude_mpm/services/memory/optimizer.py +7 -10
  320. claude_mpm/services/memory/router.py +8 -9
  321. claude_mpm/services/memory_hook_service.py +48 -34
  322. claude_mpm/services/monitor_build_service.py +77 -73
  323. claude_mpm/services/port_manager.py +130 -108
  324. claude_mpm/services/project/analyzer.py +12 -10
  325. claude_mpm/services/project/registry.py +11 -11
  326. claude_mpm/services/recovery_manager.py +10 -19
  327. claude_mpm/services/response_tracker.py +0 -1
  328. claude_mpm/services/runner_configuration_service.py +19 -20
  329. claude_mpm/services/session_management_service.py +7 -11
  330. claude_mpm/services/shared/__init__.py +1 -1
  331. claude_mpm/services/shared/async_service_base.py +58 -50
  332. claude_mpm/services/shared/config_service_base.py +73 -67
  333. claude_mpm/services/shared/lifecycle_service_base.py +82 -78
  334. claude_mpm/services/shared/manager_base.py +94 -82
  335. claude_mpm/services/shared/service_factory.py +96 -98
  336. claude_mpm/services/socketio/__init__.py +3 -3
  337. claude_mpm/services/socketio/client_proxy.py +5 -5
  338. claude_mpm/services/socketio/event_normalizer.py +199 -181
  339. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  340. claude_mpm/services/socketio/handlers/base.py +5 -4
  341. claude_mpm/services/socketio/handlers/connection.py +163 -136
  342. claude_mpm/services/socketio/handlers/file.py +13 -14
  343. claude_mpm/services/socketio/handlers/git.py +12 -7
  344. claude_mpm/services/socketio/handlers/hook.py +49 -44
  345. claude_mpm/services/socketio/handlers/memory.py +0 -1
  346. claude_mpm/services/socketio/handlers/project.py +0 -1
  347. claude_mpm/services/socketio/handlers/registry.py +37 -19
  348. claude_mpm/services/socketio/migration_utils.py +98 -84
  349. claude_mpm/services/socketio/server/__init__.py +1 -1
  350. claude_mpm/services/socketio/server/broadcaster.py +81 -87
  351. claude_mpm/services/socketio/server/core.py +65 -54
  352. claude_mpm/services/socketio/server/eventbus_integration.py +95 -56
  353. claude_mpm/services/socketio/server/main.py +64 -38
  354. claude_mpm/services/socketio_client_manager.py +10 -12
  355. claude_mpm/services/subprocess_launcher_service.py +4 -7
  356. claude_mpm/services/system_instructions_service.py +13 -14
  357. claude_mpm/services/ticket_manager.py +2 -2
  358. claude_mpm/services/utility_service.py +5 -13
  359. claude_mpm/services/version_control/__init__.py +16 -16
  360. claude_mpm/services/version_control/branch_strategy.py +5 -8
  361. claude_mpm/services/version_control/conflict_resolution.py +9 -23
  362. claude_mpm/services/version_control/git_operations.py +5 -7
  363. claude_mpm/services/version_control/semantic_versioning.py +16 -17
  364. claude_mpm/services/version_control/version_parser.py +13 -18
  365. claude_mpm/services/version_service.py +10 -11
  366. claude_mpm/storage/__init__.py +1 -1
  367. claude_mpm/storage/state_storage.py +22 -28
  368. claude_mpm/utils/__init__.py +6 -6
  369. claude_mpm/utils/agent_dependency_loader.py +47 -33
  370. claude_mpm/utils/config_manager.py +11 -14
  371. claude_mpm/utils/dependency_cache.py +1 -1
  372. claude_mpm/utils/dependency_manager.py +13 -17
  373. claude_mpm/utils/dependency_strategies.py +8 -10
  374. claude_mpm/utils/environment_context.py +3 -9
  375. claude_mpm/utils/error_handler.py +3 -13
  376. claude_mpm/utils/file_utils.py +1 -1
  377. claude_mpm/utils/path_operations.py +8 -12
  378. claude_mpm/utils/robust_installer.py +110 -33
  379. claude_mpm/utils/subprocess_utils.py +5 -6
  380. claude_mpm/validation/agent_validator.py +3 -6
  381. claude_mpm/validation/frontmatter_validator.py +1 -1
  382. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  383. claude_mpm-4.1.3.dist-info/RECORD +528 -0
  384. claude_mpm/cli/commands/run_config_checker.py +0 -160
  385. claude_mpm-4.1.1.dist-info/RECORD +0 -494
  386. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  387. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  388. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  389. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,10 @@
1
1
  """Framework loader for Claude MPM."""
2
2
 
3
3
  import logging
4
- import os
5
- import sys
6
4
  import time
7
5
  from datetime import datetime
8
6
  from pathlib import Path
9
- from typing import Any, Dict, Optional, Set, Tuple
7
+ from typing import Any, Dict, Optional
10
8
 
11
9
  # Import resource handling for packaged installations
12
10
  try:
@@ -28,6 +26,32 @@ AgentRegistryAdapter = safe_import(
28
26
  "claude_mpm.core.agent_registry", "core.agent_registry", ["AgentRegistryAdapter"]
29
27
  )
30
28
 
29
+ # Import the service container and interfaces
30
+ try:
31
+ from claude_mpm.services.core.cache_manager import CacheManager
32
+ from claude_mpm.services.core.memory_manager import MemoryManager
33
+ from claude_mpm.services.core.path_resolver import PathResolver
34
+ from claude_mpm.services.core.service_container import (
35
+ ServiceContainer,
36
+ get_global_container,
37
+ )
38
+ from claude_mpm.services.core.service_interfaces import (
39
+ ICacheManager,
40
+ IMemoryManager,
41
+ IPathResolver,
42
+ )
43
+ except ImportError:
44
+ # Fallback for development environments
45
+ from ..services.core.cache_manager import CacheManager
46
+ from ..services.core.memory_manager import MemoryManager
47
+ from ..services.core.path_resolver import PathResolver
48
+ from ..services.core.service_container import ServiceContainer, get_global_container
49
+ from ..services.core.service_interfaces import (
50
+ ICacheManager,
51
+ IMemoryManager,
52
+ IPathResolver,
53
+ )
54
+
31
55
 
32
56
  class FrameworkLoader:
33
57
  """
@@ -38,33 +62,33 @@ class FrameworkLoader:
38
62
  2. Loading custom instructions from .claude-mpm/ directories
39
63
  3. Preparing agent definitions
40
64
  4. Formatting for injection
41
-
65
+
42
66
  Custom Instructions Loading:
43
67
  The framework loader supports custom instructions through .claude-mpm/ directories.
44
68
  It NEVER reads from .claude/ directories to avoid conflicts with Claude Code.
45
-
69
+
46
70
  File Loading Precedence (highest to lowest):
47
-
71
+
48
72
  INSTRUCTIONS.md:
49
73
  1. Project: ./.claude-mpm/INSTRUCTIONS.md
50
74
  2. User: ~/.claude-mpm/INSTRUCTIONS.md
51
75
  3. System: (built-in framework instructions)
52
-
76
+
53
77
  WORKFLOW.md:
54
78
  1. Project: ./.claude-mpm/WORKFLOW.md
55
79
  2. User: ~/.claude-mpm/WORKFLOW.md
56
80
  3. System: src/claude_mpm/agents/WORKFLOW.md
57
-
81
+
58
82
  MEMORY.md:
59
83
  1. Project: ./.claude-mpm/MEMORY.md
60
84
  2. User: ~/.claude-mpm/MEMORY.md
61
85
  3. System: src/claude_mpm/agents/MEMORY.md
62
-
86
+
63
87
  Actual Memories:
64
88
  - User: ~/.claude-mpm/memories/PM_memories.md
65
89
  - Project: ./.claude-mpm/memories/PM_memories.md (overrides user)
66
90
  - Agent memories: *_memories.md files (only loaded if agent is deployed)
67
-
91
+
68
92
  Important Notes:
69
93
  - Project-level files always override user-level files
70
94
  - User-level files always override system defaults
@@ -73,7 +97,10 @@ class FrameworkLoader:
73
97
  """
74
98
 
75
99
  def __init__(
76
- self, framework_path: Optional[Path] = None, agents_dir: Optional[Path] = None
100
+ self,
101
+ framework_path: Optional[Path] = None,
102
+ agents_dir: Optional[Path] = None,
103
+ service_container: Optional[ServiceContainer] = None,
77
104
  ):
78
105
  """
79
106
  Initialize framework loader.
@@ -81,241 +108,151 @@ class FrameworkLoader:
81
108
  Args:
82
109
  framework_path: Explicit path to framework (auto-detected if None)
83
110
  agents_dir: Custom agents directory (overrides framework agents)
111
+ service_container: Optional service container for dependency injection
84
112
  """
85
113
  self.logger = get_logger("framework_loader")
86
- self.framework_path = framework_path or self._detect_framework_path()
87
114
  self.agents_dir = agents_dir
88
115
  self.framework_version = None
89
116
  self.framework_last_modified = None
90
-
91
- # Performance optimization: Initialize caches
92
- self._agent_capabilities_cache: Optional[str] = None
93
- self._agent_capabilities_cache_time: float = 0
94
- self._deployed_agents_cache: Optional[Set[str]] = None
95
- self._deployed_agents_cache_time: float = 0
96
- self._agent_metadata_cache: Dict[str, Tuple[Optional[Dict[str, Any]], float]] = {}
97
- self._memories_cache: Optional[Dict[str, Any]] = None
98
- self._memories_cache_time: float = 0
99
-
100
- # Cache TTL settings (in seconds)
101
- self.CAPABILITIES_CACHE_TTL = 60 # 60 seconds for capabilities
102
- self.DEPLOYED_AGENTS_CACHE_TTL = 30 # 30 seconds for deployed agents
103
- self.METADATA_CACHE_TTL = 60 # 60 seconds for agent metadata
104
- self.MEMORIES_CACHE_TTL = 60 # 60 seconds for memories
105
-
117
+
118
+ # Use provided container or get global container
119
+ self.container = service_container or get_global_container()
120
+
121
+ # Register services if not already registered
122
+ if not self.container.is_registered(ICacheManager):
123
+ self.container.register(ICacheManager, CacheManager, True) # singleton=True
124
+
125
+ if not self.container.is_registered(IPathResolver):
126
+ # PathResolver depends on CacheManager, so resolve it first
127
+ cache_manager = self.container.resolve(ICacheManager)
128
+ path_resolver = PathResolver(cache_manager=cache_manager)
129
+ self.container.register_instance(IPathResolver, path_resolver)
130
+
131
+ if not self.container.is_registered(IMemoryManager):
132
+ # MemoryManager depends on both CacheManager and PathResolver
133
+ cache_manager = self.container.resolve(ICacheManager)
134
+ path_resolver = self.container.resolve(IPathResolver)
135
+ memory_manager = MemoryManager(
136
+ cache_manager=cache_manager, path_resolver=path_resolver
137
+ )
138
+ self.container.register_instance(IMemoryManager, memory_manager)
139
+
140
+ # Resolve services from container
141
+ self._cache_manager = self.container.resolve(ICacheManager)
142
+ self._path_resolver = self.container.resolve(IPathResolver)
143
+ self._memory_manager = self.container.resolve(IMemoryManager)
144
+
145
+ # Initialize framework path using PathResolver
146
+ self.framework_path = (
147
+ framework_path or self._path_resolver.detect_framework_path()
148
+ )
149
+
150
+ # Keep TTL constants for backward compatibility
151
+ # These are implementation-specific, so we use defaults if not available
152
+ if hasattr(self._cache_manager, "capabilities_ttl"):
153
+ self.CAPABILITIES_CACHE_TTL = self._cache_manager.capabilities_ttl
154
+ self.DEPLOYED_AGENTS_CACHE_TTL = self._cache_manager.deployed_agents_ttl
155
+ self.METADATA_CACHE_TTL = self._cache_manager.metadata_ttl
156
+ self.MEMORIES_CACHE_TTL = self._cache_manager.memories_ttl
157
+ else:
158
+ # Default TTL values
159
+ self.CAPABILITIES_CACHE_TTL = 60
160
+ self.DEPLOYED_AGENTS_CACHE_TTL = 30
161
+ self.METADATA_CACHE_TTL = 60
162
+ self.MEMORIES_CACHE_TTL = 60
163
+
106
164
  self.framework_content = self._load_framework_content()
107
165
 
108
166
  # Initialize agent registry
109
167
  self.agent_registry = AgentRegistryAdapter(self.framework_path)
110
-
168
+
111
169
  # Initialize output style manager (must be after content is loaded)
112
170
  self.output_style_manager = None
113
171
  # Defer initialization until first use to ensure content is loaded
114
-
172
+
115
173
  def clear_all_caches(self) -> None:
116
174
  """Clear all caches to force reload on next access."""
117
- self.logger.info("Clearing all framework loader caches")
118
- self._agent_capabilities_cache = None
119
- self._agent_capabilities_cache_time = 0
120
- self._deployed_agents_cache = None
121
- self._deployed_agents_cache_time = 0
122
- self._agent_metadata_cache.clear()
123
- self._memories_cache = None
124
- self._memories_cache_time = 0
125
-
175
+ self._cache_manager.clear_all()
176
+
126
177
  def clear_agent_caches(self) -> None:
127
178
  """Clear agent-related caches (capabilities, deployed agents, metadata)."""
128
- self.logger.info("Clearing agent-related caches")
129
- self._agent_capabilities_cache = None
130
- self._agent_capabilities_cache_time = 0
131
- self._deployed_agents_cache = None
132
- self._deployed_agents_cache_time = 0
133
- self._agent_metadata_cache.clear()
134
-
179
+ self._cache_manager.clear_agent_caches()
180
+
135
181
  def clear_memory_caches(self) -> None:
136
182
  """Clear memory-related caches."""
137
- self.logger.info("Clearing memory caches")
138
- self._memories_cache = None
139
- self._memories_cache_time = 0
183
+ self._cache_manager.clear_memory_caches()
140
184
 
141
185
  def _initialize_output_style(self) -> None:
142
186
  """Initialize output style management and deploy if applicable."""
143
187
  try:
144
188
  from claude_mpm.core.output_style_manager import OutputStyleManager
145
-
189
+
146
190
  self.output_style_manager = OutputStyleManager()
147
-
191
+
148
192
  # Log detailed output style status
149
193
  self._log_output_style_status()
150
-
194
+
151
195
  # Extract and save output style content (pass self to reuse loaded content)
152
- output_style_content = self.output_style_manager.extract_output_style_content(framework_loader=self)
153
- output_style_path = self.output_style_manager.save_output_style(output_style_content)
154
-
196
+ output_style_content = (
197
+ self.output_style_manager.extract_output_style_content(
198
+ framework_loader=self
199
+ )
200
+ )
201
+ self.output_style_manager.save_output_style(output_style_content)
202
+
155
203
  # Deploy to Claude Code if supported
156
- deployed = self.output_style_manager.deploy_output_style(output_style_content)
157
-
204
+ deployed = self.output_style_manager.deploy_output_style(
205
+ output_style_content
206
+ )
207
+
158
208
  if deployed:
159
209
  self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
160
210
  else:
161
- self.logger.info("📝 Output style will be injected into instructions for older Claude versions")
162
-
211
+ self.logger.info(
212
+ "📝 Output style will be injected into instructions for older Claude versions"
213
+ )
214
+
163
215
  except Exception as e:
164
216
  self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
165
217
  # Continue without output style management
166
-
218
+
167
219
  def _log_output_style_status(self) -> None:
168
220
  """Log comprehensive output style status information."""
169
221
  if not self.output_style_manager:
170
222
  return
171
-
223
+
172
224
  # Claude version detection
173
225
  claude_version = self.output_style_manager.claude_version
174
226
  if claude_version:
175
227
  self.logger.info(f"Claude Code version detected: {claude_version}")
176
-
228
+
177
229
  # Check if version supports output styles
178
230
  if self.output_style_manager.supports_output_styles():
179
231
  self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
180
-
232
+
181
233
  # Check deployment status
182
234
  output_style_path = self.output_style_manager.output_style_path
183
235
  if output_style_path.exists():
184
- self.logger.info(f"📁 Output style file exists: {output_style_path}")
236
+ self.logger.info(
237
+ f"📁 Output style file exists: {output_style_path}"
238
+ )
185
239
  else:
186
- self.logger.info(f"📝 Output style will be created at: {output_style_path}")
187
-
240
+ self.logger.info(
241
+ f"📝 Output style will be created at: {output_style_path}"
242
+ )
243
+
188
244
  else:
189
- self.logger.info(f"⚠️ Claude Code {claude_version} does not support output styles (< 1.0.83)")
190
- self.logger.info("📝 Output style content will be injected into framework instructions")
245
+ self.logger.info(
246
+ f"⚠️ Claude Code {claude_version} does not support output styles (< 1.0.83)"
247
+ )
248
+ self.logger.info(
249
+ "📝 Output style content will be injected into framework instructions"
250
+ )
191
251
  else:
192
252
  self.logger.info("⚠️ Claude Code not detected or version unknown")
193
- self.logger.info("📝 Output style content will be injected into framework instructions as fallback")
194
-
195
- def _detect_framework_path(self) -> Optional[Path]:
196
- """Auto-detect claude-mpm framework using unified path management."""
197
- try:
198
- # Use the unified path manager for consistent detection
199
- from ..core.unified_paths import get_path_manager, DeploymentContext
200
-
201
- path_manager = get_path_manager()
202
- deployment_context = path_manager._deployment_context
203
-
204
- # Check if we're in a packaged installation
205
- if deployment_context in [DeploymentContext.PIP_INSTALL, DeploymentContext.PIPX_INSTALL, DeploymentContext.SYSTEM_PACKAGE]:
206
- self.logger.info(f"Running from packaged installation (context: {deployment_context})")
207
- # Return a marker path to indicate packaged installation
208
- return Path("__PACKAGED__")
209
- elif deployment_context == DeploymentContext.DEVELOPMENT:
210
- # Development mode - use framework root
211
- framework_root = path_manager.framework_root
212
- if (framework_root / "src" / "claude_mpm" / "agents").exists():
213
- self.logger.info(f"Using claude-mpm development installation at: {framework_root}")
214
- return framework_root
215
- elif deployment_context == DeploymentContext.EDITABLE_INSTALL:
216
- # Editable install - similar to development
217
- framework_root = path_manager.framework_root
218
- if (framework_root / "src" / "claude_mpm" / "agents").exists():
219
- self.logger.info(f"Using claude-mpm editable installation at: {framework_root}")
220
- return framework_root
221
-
222
- except Exception as e:
223
- self.logger.warning(f"Failed to use unified path manager for framework detection: {e}")
224
- # Fall back to original detection logic
225
- pass
226
-
227
- # Fallback: Original detection logic for compatibility
228
- try:
229
- # Check if the package is installed
230
- import claude_mpm
231
- package_file = Path(claude_mpm.__file__)
232
-
233
- # For packaged installations, we don't need a framework path
234
- # since we'll use importlib.resources to load files
235
- if 'site-packages' in str(package_file) or 'dist-packages' in str(package_file):
236
- self.logger.info(f"Running from packaged installation at: {package_file.parent}")
237
- # Return a marker path to indicate packaged installation
238
- return Path("__PACKAGED__")
239
- except ImportError:
240
- pass
241
-
242
- # Then check if we're in claude-mpm project (development mode)
243
- current_file = Path(__file__)
244
- if "claude-mpm" in str(current_file):
245
- # We're running from claude-mpm, use its agents
246
- for parent in current_file.parents:
247
- if parent.name == "claude-mpm":
248
- if (parent / "src" / "claude_mpm" / "agents").exists():
249
- self.logger.info(f"Using claude-mpm at: {parent}")
250
- return parent
251
- break
252
-
253
- # Otherwise check common locations for claude-mpm
254
- candidates = [
255
- # Current directory (if we're already in claude-mpm)
256
- Path.cwd(),
257
- # Development location
258
- Path.home() / "Projects" / "claude-mpm",
259
- # Current directory subdirectory
260
- Path.cwd() / "claude-mpm",
261
- ]
262
-
263
- for candidate in candidates:
264
- if candidate and candidate.exists():
265
- # Check for claude-mpm agents directory
266
- if (candidate / "src" / "claude_mpm" / "agents").exists():
267
- self.logger.info(f"Found claude-mpm at: {candidate}")
268
- return candidate
269
-
270
- self.logger.warning("Framework not found, will use minimal instructions")
271
- return None
272
-
273
- def _get_npm_global_path(self) -> Optional[Path]:
274
- """Get npm global installation path."""
275
- try:
276
- import subprocess
277
-
278
- result = subprocess.run(
279
- ["npm", "root", "-g"], capture_output=True, text=True, timeout=5
253
+ self.logger.info(
254
+ "📝 Output style content will be injected into framework instructions as fallback"
280
255
  )
281
- if result.returncode == 0:
282
- npm_root = Path(result.stdout.strip())
283
- return npm_root / "@bobmatnyc" / "claude-multiagent-pm"
284
- except:
285
- pass
286
- return None
287
-
288
- def _discover_framework_paths(
289
- self,
290
- ) -> tuple[Optional[Path], Optional[Path], Optional[Path]]:
291
- """
292
- Discover agent directories based on priority.
293
-
294
- Returns:
295
- Tuple of (agents_dir, templates_dir, main_dir)
296
- """
297
- agents_dir = None
298
- templates_dir = None
299
- main_dir = None
300
-
301
- if self.agents_dir and self.agents_dir.exists():
302
- agents_dir = self.agents_dir
303
- self.logger.info(f"Using custom agents directory: {agents_dir}")
304
- elif self.framework_path and self.framework_path != Path("__PACKAGED__"):
305
- # Prioritize templates directory over main agents directory
306
- templates_dir = (
307
- self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
308
- )
309
- main_dir = self.framework_path / "src" / "claude_mpm" / "agents"
310
-
311
- if templates_dir.exists() and any(templates_dir.glob("*.md")):
312
- agents_dir = templates_dir
313
- self.logger.info(f"Using agents from templates directory: {agents_dir}")
314
- elif main_dir.exists() and any(main_dir.glob("*.md")):
315
- agents_dir = main_dir
316
- self.logger.info(f"Using agents from main directory: {agents_dir}")
317
-
318
- return agents_dir, templates_dir, main_dir
319
256
 
320
257
  def _try_load_file(self, file_path: Path, file_type: str) -> Optional[str]:
321
258
  """
@@ -361,29 +298,6 @@ class FrameworkLoader:
361
298
  self.logger.error(f"Failed to load {file_type}: {e}")
362
299
  return None
363
300
 
364
- def _migrate_memory_file(self, old_path: Path, new_path: Path) -> None:
365
- """
366
- Migrate memory file from old naming convention to new.
367
-
368
- WHY: Supports backward compatibility by automatically migrating from
369
- the old {agent_id}_agent.md and {agent_id}.md formats to the new {agent_id}_memories.md format.
370
-
371
- Args:
372
- old_path: Path to the old file
373
- new_path: Path to the new file
374
- """
375
- if old_path.exists() and not new_path.exists():
376
- try:
377
- # Read content from old file
378
- content = old_path.read_text(encoding="utf-8")
379
- # Write to new file
380
- new_path.write_text(content, encoding="utf-8")
381
- # Remove old file
382
- old_path.unlink()
383
- self.logger.info(f"Migrated memory file from {old_path.name} to {new_path.name}")
384
- except Exception as e:
385
- self.logger.error(f"Failed to migrate memory file {old_path.name}: {e}")
386
-
387
301
  def _load_instructions_file(self, content: Dict[str, Any]) -> None:
388
302
  """
389
303
  Load custom INSTRUCTIONS.md from .claude-mpm directories.
@@ -391,7 +305,7 @@ class FrameworkLoader:
391
305
  Precedence (highest to lowest):
392
306
  1. Project-specific: ./.claude-mpm/INSTRUCTIONS.md
393
307
  2. User-specific: ~/.claude-mpm/INSTRUCTIONS.md
394
-
308
+
395
309
  NOTE: We do NOT load CLAUDE.md files since Claude Code already picks them up automatically.
396
310
  This prevents duplication of instructions.
397
311
 
@@ -407,9 +321,11 @@ class FrameworkLoader:
407
321
  if loaded_content:
408
322
  content["custom_instructions"] = loaded_content
409
323
  content["custom_instructions_level"] = "project"
410
- self.logger.info("Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md")
324
+ self.logger.info(
325
+ "Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md"
326
+ )
411
327
  return
412
-
328
+
413
329
  # Check for user-specific INSTRUCTIONS.md
414
330
  user_instructions_path = Path.home() / ".claude-mpm" / "INSTRUCTIONS.md"
415
331
  if user_instructions_path.exists():
@@ -419,7 +335,9 @@ class FrameworkLoader:
419
335
  if loaded_content:
420
336
  content["custom_instructions"] = loaded_content
421
337
  content["custom_instructions_level"] = "user"
422
- self.logger.info("Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md")
338
+ self.logger.info(
339
+ "Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md"
340
+ )
423
341
  return
424
342
 
425
343
  def _load_workflow_instructions(self, content: Dict[str, Any]) -> None:
@@ -430,7 +348,7 @@ class FrameworkLoader:
430
348
  1. Project-specific: ./.claude-mpm/WORKFLOW.md
431
349
  2. User-specific: ~/.claude-mpm/WORKFLOW.md
432
350
  3. System default: src/claude_mpm/agents/WORKFLOW.md or packaged
433
-
351
+
434
352
  NOTE: We do NOT load from .claude/ directories to avoid conflicts.
435
353
 
436
354
  Args:
@@ -445,9 +363,11 @@ class FrameworkLoader:
445
363
  if loaded_content:
446
364
  content["workflow_instructions"] = loaded_content
447
365
  content["workflow_instructions_level"] = "project"
448
- self.logger.info("Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md")
366
+ self.logger.info(
367
+ "Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md"
368
+ )
449
369
  return
450
-
370
+
451
371
  # Check for user-specific WORKFLOW.md (medium priority)
452
372
  user_workflow_path = Path.home() / ".claude-mpm" / "WORKFLOW.md"
453
373
  if user_workflow_path.exists():
@@ -457,7 +377,9 @@ class FrameworkLoader:
457
377
  if loaded_content:
458
378
  content["workflow_instructions"] = loaded_content
459
379
  content["workflow_instructions_level"] = "user"
460
- self.logger.info("Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md")
380
+ self.logger.info(
381
+ "Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md"
382
+ )
461
383
  return
462
384
 
463
385
  # Fall back to system workflow (lowest priority)
@@ -482,7 +404,7 @@ class FrameworkLoader:
482
404
  1. Project-specific: ./.claude-mpm/MEMORY.md
483
405
  2. User-specific: ~/.claude-mpm/MEMORY.md
484
406
  3. System default: src/claude_mpm/agents/MEMORY.md or packaged
485
-
407
+
486
408
  NOTE: We do NOT load from .claude/ directories to avoid conflicts.
487
409
 
488
410
  Args:
@@ -497,9 +419,11 @@ class FrameworkLoader:
497
419
  if loaded_content:
498
420
  content["memory_instructions"] = loaded_content
499
421
  content["memory_instructions_level"] = "project"
500
- self.logger.info("Using project-specific memory instructions from .claude-mpm/MEMORY.md")
422
+ self.logger.info(
423
+ "Using project-specific memory instructions from .claude-mpm/MEMORY.md"
424
+ )
501
425
  return
502
-
426
+
503
427
  # Check for user-specific MEMORY.md (medium priority)
504
428
  user_memory_path = Path.home() / ".claude-mpm" / "MEMORY.md"
505
429
  if user_memory_path.exists():
@@ -509,7 +433,9 @@ class FrameworkLoader:
509
433
  if loaded_content:
510
434
  content["memory_instructions"] = loaded_content
511
435
  content["memory_instructions_level"] = "user"
512
- self.logger.info("Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md")
436
+ self.logger.info(
437
+ "Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md"
438
+ )
513
439
  return
514
440
 
515
441
  # Fall back to system memory instructions (lowest priority)
@@ -525,357 +451,65 @@ class FrameworkLoader:
525
451
  content["memory_instructions"] = loaded_content
526
452
  content["memory_instructions_level"] = "system"
527
453
  self.logger.info("Using system memory instructions")
528
-
454
+
529
455
  def _get_deployed_agents(self) -> set:
530
456
  """
531
457
  Get a set of deployed agent names from .claude/agents/ directories.
532
458
  Uses caching to avoid repeated filesystem scans.
533
-
459
+
534
460
  Returns:
535
461
  Set of agent names (file stems) that are deployed
536
462
  """
537
- # Check if cache is valid
538
- current_time = time.time()
539
- if (self._deployed_agents_cache is not None and
540
- current_time - self._deployed_agents_cache_time < self.DEPLOYED_AGENTS_CACHE_TTL):
541
- self.logger.debug(f"Using cached deployed agents (age: {current_time - self._deployed_agents_cache_time:.1f}s)")
542
- return self._deployed_agents_cache
543
-
463
+ # Try to get from cache first
464
+ cached = self._cache_manager.get_deployed_agents()
465
+ if cached is not None:
466
+ return cached
467
+
544
468
  # Cache miss or expired - perform actual scan
545
469
  self.logger.debug("Scanning for deployed agents (cache miss or expired)")
546
470
  deployed = set()
547
-
471
+
548
472
  # Check multiple locations for deployed agents
549
473
  agents_dirs = [
550
474
  Path.cwd() / ".claude" / "agents", # Project-specific agents
551
475
  Path.home() / ".claude" / "agents", # User's system agents
552
476
  ]
553
-
477
+
554
478
  for agents_dir in agents_dirs:
555
479
  if agents_dir.exists():
556
480
  for agent_file in agents_dir.glob("*.md"):
557
481
  if not agent_file.name.startswith("."):
558
482
  # Use stem to get agent name without extension
559
483
  deployed.add(agent_file.stem)
560
- self.logger.debug(f"Found deployed agent: {agent_file.stem} in {agents_dir}")
561
-
484
+ self.logger.debug(
485
+ f"Found deployed agent: {agent_file.stem} in {agents_dir}"
486
+ )
487
+
562
488
  self.logger.debug(f"Total deployed agents found: {len(deployed)}")
563
-
489
+
564
490
  # Update cache
565
- self._deployed_agents_cache = deployed
566
- self._deployed_agents_cache_time = current_time
567
-
491
+ self._cache_manager.set_deployed_agents(deployed)
492
+
568
493
  return deployed
569
-
494
+
570
495
  def _load_actual_memories(self, content: Dict[str, Any]) -> None:
571
496
  """
572
- Load actual memories from both user and project directories.
573
- Uses caching to avoid repeated file I/O operations.
574
-
575
- Loading order:
576
- 1. User-level memories from ~/.claude-mpm/memories/ (global defaults)
577
- 2. Project-level memories from ./.claude-mpm/memories/ (overrides user)
578
-
579
- This loads:
580
- 1. PM memories from PM_memories.md (always loaded)
581
- 2. Agent memories from <agent>_memories.md (only if agent is deployed)
582
-
497
+ Load actual memories using the MemoryManager service.
498
+
499
+ This method delegates all memory loading operations to the MemoryManager,
500
+ which handles caching, aggregation, deduplication, and legacy format migration.
501
+
583
502
  Args:
584
503
  content: Dictionary to update with actual memories
585
504
  """
586
- # Check if cache is valid
587
- current_time = time.time()
588
- if (self._memories_cache is not None and
589
- current_time - self._memories_cache_time < self.MEMORIES_CACHE_TTL):
590
- cache_age = current_time - self._memories_cache_time
591
- self.logger.debug(f"Using cached memories (age: {cache_age:.1f}s)")
592
-
593
- # Apply cached memories to content
594
- if "actual_memories" in self._memories_cache:
595
- content["actual_memories"] = self._memories_cache["actual_memories"]
596
- if "agent_memories" in self._memories_cache:
597
- content["agent_memories"] = self._memories_cache["agent_memories"]
598
- return
599
-
600
- # Cache miss or expired - perform actual loading
601
- self.logger.debug("Loading memories from disk (cache miss or expired)")
602
-
603
- # Define memory directories in priority order (user first, then project)
604
- user_memories_dir = Path.home() / ".claude-mpm" / "memories"
605
- project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
606
-
607
- # Check for deployed agents
608
- deployed_agents = self._get_deployed_agents()
609
-
610
- # Track loading statistics
611
- loaded_count = 0
612
- skipped_count = 0
613
-
614
- # Dictionary to store aggregated memories
615
- pm_memories = []
616
- agent_memories_dict = {}
617
-
618
- # Load memories from user directory first
619
- if user_memories_dir.exists():
620
- self.logger.info(f"Loading user-level memory files from: {user_memories_dir}")
621
- loaded, skipped = self._load_memories_from_directory(
622
- user_memories_dir, deployed_agents, pm_memories, agent_memories_dict, "user"
623
- )
624
- loaded_count += loaded
625
- skipped_count += skipped
626
- else:
627
- self.logger.debug(f"No user memories directory found at: {user_memories_dir}")
628
-
629
- # Load memories from project directory (overrides user memories)
630
- if project_memories_dir.exists():
631
- self.logger.info(f"Loading project-level memory files from: {project_memories_dir}")
632
- loaded, skipped = self._load_memories_from_directory(
633
- project_memories_dir, deployed_agents, pm_memories, agent_memories_dict, "project"
634
- )
635
- loaded_count += loaded
636
- skipped_count += skipped
637
- else:
638
- self.logger.debug(f"No project memories directory found at: {project_memories_dir}")
639
-
640
- # Aggregate PM memories
641
- if pm_memories:
642
- aggregated_pm = self._aggregate_memories(pm_memories)
643
- content["actual_memories"] = aggregated_pm
644
- memory_size = len(aggregated_pm.encode('utf-8'))
645
- self.logger.info(f"Aggregated PM memory ({memory_size:,} bytes) from {len(pm_memories)} source(s)")
646
-
647
- # Store agent memories (already aggregated per agent)
648
- if agent_memories_dict:
649
- content["agent_memories"] = agent_memories_dict
650
- for agent_name, memory_content in agent_memories_dict.items():
651
- memory_size = len(memory_content.encode('utf-8'))
652
- self.logger.debug(f"Aggregated {agent_name} memory: {memory_size:,} bytes")
653
-
654
- # Update cache with loaded memories
655
- self._memories_cache = {}
656
- if "actual_memories" in content:
657
- self._memories_cache["actual_memories"] = content["actual_memories"]
658
- if "agent_memories" in content:
659
- self._memories_cache["agent_memories"] = content["agent_memories"]
660
- self._memories_cache_time = current_time
661
-
662
- # Log detailed summary
663
- if loaded_count > 0 or skipped_count > 0:
664
- # Count unique agents with memories
665
- agent_count = len(agent_memories_dict) if agent_memories_dict else 0
666
- pm_loaded = bool(content.get("actual_memories"))
667
-
668
- summary_parts = []
669
- if pm_loaded:
670
- summary_parts.append("PM memory loaded")
671
- if agent_count > 0:
672
- summary_parts.append(f"{agent_count} agent memories loaded")
673
- if skipped_count > 0:
674
- summary_parts.append(f"{skipped_count} non-deployed agent memories skipped")
675
-
676
- self.logger.info(f"Memory loading complete: {' | '.join(summary_parts)}")
677
-
678
- # Log deployed agents for reference
679
- if len(deployed_agents) > 0:
680
- self.logger.debug(f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}")
681
-
682
- def _load_memories_from_directory(
683
- self,
684
- memories_dir: Path,
685
- deployed_agents: set,
686
- pm_memories: list,
687
- agent_memories_dict: dict,
688
- source: str
689
- ) -> tuple[int, int]:
690
- """
691
- Load memories from a specific directory.
692
-
693
- Args:
694
- memories_dir: Directory to load memories from
695
- deployed_agents: Set of deployed agent names
696
- pm_memories: List to append PM memories to
697
- agent_memories_dict: Dict to store agent memories
698
- source: Source label ("user" or "project")
699
-
700
- Returns:
701
- Tuple of (loaded_count, skipped_count)
702
- """
703
- loaded_count = 0
704
- skipped_count = 0
705
-
706
- # Load PM memories (always loaded)
707
- # Support migration from both old formats
708
- pm_memory_path = memories_dir / "PM_memories.md"
709
- old_pm_path = memories_dir / "PM.md"
710
-
711
- # Migrate from old PM.md if needed
712
- if not pm_memory_path.exists() and old_pm_path.exists():
713
- try:
714
- old_pm_path.rename(pm_memory_path)
715
- self.logger.info(f"Migrated PM.md to PM_memories.md")
716
- except Exception as e:
717
- self.logger.error(f"Failed to migrate PM.md: {e}")
718
- pm_memory_path = old_pm_path # Fall back to old path
719
- if pm_memory_path.exists():
720
- loaded_content = self._try_load_file(
721
- pm_memory_path, f"PM memory ({source})"
722
- )
723
- if loaded_content:
724
- pm_memories.append({
725
- "source": source,
726
- "content": loaded_content,
727
- "path": pm_memory_path
728
- })
729
- memory_size = len(loaded_content.encode('utf-8'))
730
- self.logger.info(f"Loaded {source} PM memory: {pm_memory_path} ({memory_size:,} bytes)")
731
- loaded_count += 1
732
-
733
- # First, migrate any old format memory files to new format
734
- # This handles backward compatibility for existing installations
735
- for old_file in memories_dir.glob("*.md"):
736
- # Skip files already in correct format and special files
737
- if old_file.name.endswith("_memories.md") or old_file.name in ["PM.md", "README.md"]:
738
- continue
739
-
740
- # Determine new name based on old format
741
- if old_file.stem.endswith("_agent"):
742
- # Old format: {agent_name}_agent.md -> {agent_name}_memories.md
743
- agent_name = old_file.stem[:-6] # Remove "_agent" suffix
744
- new_path = memories_dir / f"{agent_name}_memories.md"
745
- if not new_path.exists():
746
- self._migrate_memory_file(old_file, new_path)
747
- else:
748
- # Intermediate format: {agent_name}.md -> {agent_name}_memories.md
749
- agent_name = old_file.stem
750
- new_path = memories_dir / f"{agent_name}_memories.md"
751
- if not new_path.exists():
752
- self._migrate_memory_file(old_file, new_path)
753
-
754
- # Load agent memories (only for deployed agents)
755
- # Only process *_memories.md files to avoid README.md and other docs
756
- for memory_file in memories_dir.glob("*_memories.md"):
757
- # Skip PM_memories.md as we already handled it
758
- if memory_file.name == "PM_memories.md":
759
- continue
760
-
761
- # Extract agent name from file (remove "_memories" suffix)
762
- agent_name = memory_file.stem[:-9] # Remove "_memories" suffix
763
-
764
- # Check if agent is deployed
765
- if agent_name in deployed_agents:
766
- loaded_content = self._try_load_file(
767
- memory_file, f"agent memory: {agent_name} ({source})"
768
- )
769
- if loaded_content:
770
- # Store or merge agent memories
771
- if agent_name not in agent_memories_dict:
772
- agent_memories_dict[agent_name] = []
773
-
774
- # If it's a list, append the new memory entry
775
- if isinstance(agent_memories_dict[agent_name], list):
776
- agent_memories_dict[agent_name].append({
777
- "source": source,
778
- "content": loaded_content,
779
- "path": memory_file
780
- })
781
-
782
- memory_size = len(loaded_content.encode('utf-8'))
783
- self.logger.info(f"Loaded {source} memory for {agent_name}: {memory_file.name} ({memory_size:,} bytes)")
784
- loaded_count += 1
785
- else:
786
- # Provide more detailed logging about why the memory was skipped
787
- self.logger.info(f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed)")
788
- # Also log a debug message with available agents for diagnostics
789
- if agent_name.replace('_', '-') in deployed_agents or agent_name.replace('-', '_') in deployed_agents:
790
- # Detect naming mismatches
791
- alt_name = agent_name.replace('_', '-') if '_' in agent_name else agent_name.replace('-', '_')
792
- if alt_name in deployed_agents:
793
- self.logger.warning(
794
- f"Naming mismatch detected: Memory file uses '{agent_name}' but deployed agent is '{alt_name}'. "
795
- f"Consider renaming {memory_file.name} to {alt_name}_memories.md"
796
- )
797
- skipped_count += 1
798
-
799
- # After loading all memories for this directory, aggregate agent memories
800
- for agent_name in list(agent_memories_dict.keys()):
801
- if isinstance(agent_memories_dict[agent_name], list) and agent_memories_dict[agent_name]:
802
- # Aggregate memories for this agent
803
- aggregated = self._aggregate_memories(agent_memories_dict[agent_name])
804
- agent_memories_dict[agent_name] = aggregated
805
-
806
- return loaded_count, skipped_count
807
-
808
- def _aggregate_memories(self, memory_entries: list) -> str:
809
- """
810
- Aggregate multiple memory entries into a single memory string.
811
-
812
- Strategy:
813
- - Simplified to support list-based memories only
814
- - Preserve all unique bullet-point items (lines starting with -)
815
- - Remove exact duplicates
816
- - Project-level memories take precedence over user-level
817
-
818
- Args:
819
- memory_entries: List of memory entries with source, content, and path
820
-
821
- Returns:
822
- Aggregated memory content as a string
823
- """
824
- if not memory_entries:
825
- return ""
826
-
827
- # If only one entry, return it as-is
828
- if len(memory_entries) == 1:
829
- return memory_entries[0]["content"]
830
-
831
- # Parse all memories into a simple list
832
- all_items = {} # Dict to track items and their source
833
- metadata_lines = []
834
- agent_id = None
835
-
836
- for entry in memory_entries:
837
- content = entry["content"]
838
- source = entry["source"]
839
-
840
- for line in content.split('\n'):
841
- # Check for header to extract agent_id
842
- if line.startswith('# Agent Memory:'):
843
- agent_id = line.replace('# Agent Memory:', '').strip()
844
- # Check for metadata lines
845
- elif line.startswith('<!-- ') and line.endswith(' -->'):
846
- # Only keep metadata from project source or if not already present
847
- if source == "project" or line not in metadata_lines:
848
- metadata_lines.append(line)
849
- # Check for list items
850
- elif line.strip().startswith('-'):
851
- # Normalize the item for comparison
852
- item_text = line.strip()
853
- normalized = item_text.lstrip('- ').strip().lower()
854
-
855
- # Add item if new or if project source overrides user source
856
- if normalized not in all_items or source == "project":
857
- all_items[normalized] = (item_text, source)
858
-
859
- # Build aggregated content as simple list
860
- lines = []
861
-
862
- # Add header
863
- if agent_id:
864
- lines.append(f"# Agent Memory: {agent_id}")
865
- else:
866
- lines.append("# Agent Memory")
867
-
868
- # Add latest timestamp from metadata
869
- from datetime import datetime
870
- lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
871
- lines.append("")
872
-
873
- # Add all unique items (sorted for consistency)
874
- for normalized_key in sorted(all_items.keys()):
875
- item_text, source = all_items[normalized_key]
876
- lines.append(item_text)
877
-
878
- return '\n'.join(lines)
505
+ # Use MemoryManager to load all memories
506
+ memories = self._memory_manager.load_memories()
507
+
508
+ # Apply loaded memories to content
509
+ if "actual_memories" in memories:
510
+ content["actual_memories"] = memories["actual_memories"]
511
+ if "agent_memories" in memories:
512
+ content["agent_memories"] = memories["agent_memories"]
879
513
 
880
514
  def _load_single_agent(
881
515
  self, agent_file: Path
@@ -972,7 +606,7 @@ class FrameworkLoader:
972
606
 
973
607
  if not self.framework_path:
974
608
  return content
975
-
609
+
976
610
  # Check if this is a packaged installation
977
611
  if self.framework_path == Path("__PACKAGED__"):
978
612
  # Load files using importlib.resources for packaged installations
@@ -981,7 +615,11 @@ class FrameworkLoader:
981
615
  # Load from filesystem for development mode
982
616
  # Load framework's INSTRUCTIONS.md
983
617
  framework_instructions_path = (
984
- self.framework_path / "src" / "claude_mpm" / "agents" / "INSTRUCTIONS.md"
618
+ self.framework_path
619
+ / "src"
620
+ / "claude_mpm"
621
+ / "agents"
622
+ / "INSTRUCTIONS.md"
985
623
  )
986
624
  if framework_instructions_path.exists():
987
625
  loaded_content = self._try_load_file(
@@ -993,12 +631,14 @@ class FrameworkLoader:
993
631
  # Add framework version to content
994
632
  if self.framework_version:
995
633
  content["instructions_version"] = self.framework_version
996
- content[
997
- "version"
998
- ] = self.framework_version # Update main version key
634
+ content["version"] = (
635
+ self.framework_version
636
+ ) # Update main version key
999
637
  # Add modification timestamp to content
1000
638
  if self.framework_last_modified:
1001
- content["instructions_last_modified"] = self.framework_last_modified
639
+ content["instructions_last_modified"] = (
640
+ self.framework_last_modified
641
+ )
1002
642
 
1003
643
  # Load BASE_PM.md for core framework requirements
1004
644
  base_pm_path = (
@@ -1016,33 +656,40 @@ class FrameworkLoader:
1016
656
 
1017
657
  # Load MEMORY.md - check for project-specific first, then system
1018
658
  self._load_memory_instructions(content)
1019
-
659
+
1020
660
  # Load actual memories from .claude-mpm/memories/PM_memories.md
1021
661
  self._load_actual_memories(content)
1022
662
 
1023
- # Discover agent directories
1024
- agents_dir, templates_dir, main_dir = self._discover_framework_paths()
663
+ # Discover agent directories using PathResolver
664
+ agents_dir, templates_dir, main_dir = self._path_resolver.discover_agent_paths(
665
+ agents_dir=self.agents_dir, framework_path=self.framework_path
666
+ )
1025
667
 
1026
668
  # Load agents from discovered directory
1027
669
  self._load_agents_directory(content, agents_dir, templates_dir, main_dir)
1028
670
 
1029
671
  return content
1030
-
672
+
1031
673
  def _load_packaged_framework_content(self, content: Dict[str, Any]) -> None:
1032
674
  """Load framework content from packaged installation using importlib.resources."""
1033
675
  if not files:
1034
- self.logger.warning("importlib.resources not available, cannot load packaged framework")
676
+ self.logger.warning(
677
+ "importlib.resources not available, cannot load packaged framework"
678
+ )
1035
679
  self.logger.debug(f"files variable is: {files}")
1036
680
  # Try alternative import methods
1037
681
  try:
1038
682
  from importlib import resources
683
+
1039
684
  self.logger.info("Using importlib.resources as fallback")
1040
685
  self._load_packaged_framework_content_fallback(content, resources)
1041
686
  return
1042
687
  except ImportError:
1043
- self.logger.error("No importlib.resources available, using minimal framework")
688
+ self.logger.error(
689
+ "No importlib.resources available, using minimal framework"
690
+ )
1044
691
  return
1045
-
692
+
1046
693
  try:
1047
694
  # Load INSTRUCTIONS.md
1048
695
  instructions_content = self._load_packaged_file("INSTRUCTIONS.md")
@@ -1050,43 +697,51 @@ class FrameworkLoader:
1050
697
  content["framework_instructions"] = instructions_content
1051
698
  content["loaded"] = True
1052
699
  # Extract and store version/timestamp metadata
1053
- self._extract_metadata_from_content(instructions_content, "INSTRUCTIONS.md")
700
+ self._extract_metadata_from_content(
701
+ instructions_content, "INSTRUCTIONS.md"
702
+ )
1054
703
  if self.framework_version:
1055
704
  content["instructions_version"] = self.framework_version
1056
705
  content["version"] = self.framework_version
1057
706
  if self.framework_last_modified:
1058
707
  content["instructions_last_modified"] = self.framework_last_modified
1059
-
708
+
1060
709
  # Load BASE_PM.md
1061
710
  base_pm_content = self._load_packaged_file("BASE_PM.md")
1062
711
  if base_pm_content:
1063
712
  content["base_pm_instructions"] = base_pm_content
1064
-
713
+
1065
714
  # Load WORKFLOW.md
1066
715
  workflow_content = self._load_packaged_file("WORKFLOW.md")
1067
716
  if workflow_content:
1068
717
  content["workflow_instructions"] = workflow_content
1069
718
  content["project_workflow"] = "system"
1070
-
719
+
1071
720
  # Load MEMORY.md
1072
721
  memory_content = self._load_packaged_file("MEMORY.md")
1073
722
  if memory_content:
1074
723
  content["memory_instructions"] = memory_content
1075
724
  content["project_memory"] = "system"
1076
-
725
+
1077
726
  except Exception as e:
1078
727
  self.logger.error(f"Failed to load packaged framework content: {e}")
1079
728
 
1080
- def _load_packaged_framework_content_fallback(self, content: Dict[str, Any], resources) -> None:
729
+ def _load_packaged_framework_content_fallback(
730
+ self, content: Dict[str, Any], resources
731
+ ) -> None:
1081
732
  """Load framework content using importlib.resources fallback."""
1082
733
  try:
1083
734
  # Load INSTRUCTIONS.md
1084
- instructions_content = self._load_packaged_file_fallback("INSTRUCTIONS.md", resources)
735
+ instructions_content = self._load_packaged_file_fallback(
736
+ "INSTRUCTIONS.md", resources
737
+ )
1085
738
  if instructions_content:
1086
739
  content["framework_instructions"] = instructions_content
1087
740
  content["loaded"] = True
1088
741
  # Extract and store version/timestamp metadata
1089
- self._extract_metadata_from_content(instructions_content, "INSTRUCTIONS.md")
742
+ self._extract_metadata_from_content(
743
+ instructions_content, "INSTRUCTIONS.md"
744
+ )
1090
745
  if self.framework_version:
1091
746
  content["instructions_version"] = self.framework_version
1092
747
  content["version"] = self.framework_version
@@ -1099,7 +754,9 @@ class FrameworkLoader:
1099
754
  content["base_pm_instructions"] = base_pm_content
1100
755
 
1101
756
  # Load WORKFLOW.md
1102
- workflow_content = self._load_packaged_file_fallback("WORKFLOW.md", resources)
757
+ workflow_content = self._load_packaged_file_fallback(
758
+ "WORKFLOW.md", resources
759
+ )
1103
760
  if workflow_content:
1104
761
  content["workflow_instructions"] = workflow_content
1105
762
  content["project_workflow"] = "system"
@@ -1111,7 +768,9 @@ class FrameworkLoader:
1111
768
  content["project_memory"] = "system"
1112
769
 
1113
770
  except Exception as e:
1114
- self.logger.error(f"Failed to load packaged framework content with fallback: {e}")
771
+ self.logger.error(
772
+ f"Failed to load packaged framework content with fallback: {e}"
773
+ )
1115
774
 
1116
775
  def _load_packaged_file_fallback(self, filename: str, resources) -> Optional[str]:
1117
776
  """Load a file from the packaged installation using importlib.resources fallback."""
@@ -1119,52 +778,52 @@ class FrameworkLoader:
1119
778
  # Try different resource loading methods
1120
779
  try:
1121
780
  # Method 1: resources.read_text (Python 3.9+)
1122
- content = resources.read_text('claude_mpm.agents', filename)
781
+ content = resources.read_text("claude_mpm.agents", filename)
1123
782
  self.logger.info(f"Loaded {filename} from package using read_text")
1124
783
  return content
1125
784
  except AttributeError:
1126
785
  # Method 2: resources.files (Python 3.9+)
1127
- agents_files = resources.files('claude_mpm.agents')
786
+ agents_files = resources.files("claude_mpm.agents")
1128
787
  file_path = agents_files / filename
1129
788
  if file_path.is_file():
1130
789
  content = file_path.read_text()
1131
790
  self.logger.info(f"Loaded {filename} from package using files")
1132
791
  return content
1133
- else:
1134
- self.logger.warning(f"File {filename} not found in package")
1135
- return None
792
+ self.logger.warning(f"File {filename} not found in package")
793
+ return None
1136
794
  except Exception as e:
1137
- self.logger.error(f"Failed to load {filename} from package with fallback: {e}")
795
+ self.logger.error(
796
+ f"Failed to load {filename} from package with fallback: {e}"
797
+ )
1138
798
  return None
1139
799
 
1140
800
  def _load_packaged_file(self, filename: str) -> Optional[str]:
1141
801
  """Load a file from the packaged installation."""
1142
802
  try:
1143
803
  # Use importlib.resources to load file from package
1144
- agents_package = files('claude_mpm.agents')
804
+ agents_package = files("claude_mpm.agents")
1145
805
  file_path = agents_package / filename
1146
-
806
+
1147
807
  if file_path.is_file():
1148
808
  content = file_path.read_text()
1149
809
  self.logger.info(f"Loaded {filename} from package")
1150
810
  return content
1151
- else:
1152
- self.logger.warning(f"File {filename} not found in package")
1153
- return None
811
+ self.logger.warning(f"File {filename} not found in package")
812
+ return None
1154
813
  except Exception as e:
1155
814
  self.logger.error(f"Failed to load {filename} from package: {e}")
1156
815
  return None
1157
-
816
+
1158
817
  def _extract_metadata_from_content(self, content: str, filename: str) -> None:
1159
818
  """Extract metadata from content string."""
1160
819
  import re
1161
-
820
+
1162
821
  # Extract version
1163
822
  version_match = re.search(r"<!-- FRAMEWORK_VERSION: (\d+) -->", content)
1164
823
  if version_match and "INSTRUCTIONS.md" in filename:
1165
824
  self.framework_version = version_match.group(1)
1166
825
  self.logger.info(f"Framework version: {self.framework_version}")
1167
-
826
+
1168
827
  # Extract timestamp
1169
828
  timestamp_match = re.search(r"<!-- LAST_MODIFIED: ([^>]+) -->", content)
1170
829
  if timestamp_match and "INSTRUCTIONS.md" in filename:
@@ -1178,12 +837,58 @@ class FrameworkLoader:
1178
837
  Returns:
1179
838
  Complete framework instructions ready for injection
1180
839
  """
840
+ # Import LogManager for prompt logging
841
+ try:
842
+ from .log_manager import get_log_manager
843
+
844
+ log_manager = get_log_manager()
845
+ except ImportError:
846
+ log_manager = None
847
+
848
+ # Generate the instructions
1181
849
  if self.framework_content["loaded"]:
1182
850
  # Build framework from components
1183
- return self._format_full_framework()
851
+ instructions = self._format_full_framework()
1184
852
  else:
1185
853
  # Use minimal fallback
1186
- return self._format_minimal_framework()
854
+ instructions = self._format_minimal_framework()
855
+
856
+ # Log the system prompt if LogManager is available
857
+ if log_manager:
858
+ try:
859
+ import asyncio
860
+ import os
861
+
862
+ # Get or create event loop
863
+ try:
864
+ loop = asyncio.get_running_loop()
865
+ except RuntimeError:
866
+ loop = asyncio.new_event_loop()
867
+ asyncio.set_event_loop(loop)
868
+
869
+ # Prepare metadata
870
+ metadata = {
871
+ "framework_version": self.framework_version,
872
+ "framework_loaded": self.framework_content.get("loaded", False),
873
+ "session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
874
+ "instructions_length": len(instructions),
875
+ }
876
+
877
+ # Log the prompt asynchronously
878
+ if loop.is_running():
879
+ asyncio.create_task(
880
+ log_manager.log_prompt("system_prompt", instructions, metadata)
881
+ )
882
+ else:
883
+ loop.run_until_complete(
884
+ log_manager.log_prompt("system_prompt", instructions, metadata)
885
+ )
886
+
887
+ self.logger.debug("System prompt logged to prompts directory")
888
+ except Exception as e:
889
+ self.logger.debug(f"Could not log system prompt: {e}")
890
+
891
+ return instructions
1187
892
 
1188
893
  def _strip_metadata_comments(self, content: str) -> str:
1189
894
  """Strip metadata HTML comments from content.
@@ -1201,12 +906,10 @@ class FrameworkLoader:
1201
906
  content,
1202
907
  )
1203
908
  # Also remove any leading blank lines that might result
1204
- cleaned = cleaned.lstrip("\n")
1205
- return cleaned
909
+ return cleaned.lstrip("\n")
1206
910
 
1207
911
  def _format_full_framework(self) -> str:
1208
912
  """Format full framework instructions."""
1209
- from datetime import datetime
1210
913
 
1211
914
  # Initialize output style manager on first use (ensures content is loaded)
1212
915
  if self.output_style_manager is None:
@@ -1217,7 +920,9 @@ class FrameworkLoader:
1217
920
  if self.output_style_manager:
1218
921
  inject_output_style = self.output_style_manager.should_inject_content()
1219
922
  if inject_output_style:
1220
- self.logger.info("Injecting output style content into instructions for Claude < 1.0.83")
923
+ self.logger.info(
924
+ "Injecting output style content into instructions for Claude < 1.0.83"
925
+ )
1221
926
 
1222
927
  # If we have the full framework INSTRUCTIONS.md, use it
1223
928
  if self.framework_content.get("framework_instructions"):
@@ -1227,10 +932,12 @@ class FrameworkLoader:
1227
932
 
1228
933
  # Note: We don't add working directory CLAUDE.md here since Claude Code
1229
934
  # already picks it up automatically. This prevents duplication.
1230
-
935
+
1231
936
  # Add custom INSTRUCTIONS.md if present (overrides or extends framework instructions)
1232
937
  if self.framework_content.get("custom_instructions"):
1233
- level = self.framework_content.get("custom_instructions_level", "unknown")
938
+ level = self.framework_content.get(
939
+ "custom_instructions_level", "unknown"
940
+ )
1234
941
  instructions += f"\n\n## Custom PM Instructions ({level} level)\n\n"
1235
942
  instructions += "**The following custom instructions override or extend the framework defaults:**\n\n"
1236
943
  instructions += self._strip_metadata_comments(
@@ -1243,7 +950,9 @@ class FrameworkLoader:
1243
950
  workflow_content = self._strip_metadata_comments(
1244
951
  self.framework_content["workflow_instructions"]
1245
952
  )
1246
- level = self.framework_content.get("workflow_instructions_level", "system")
953
+ level = self.framework_content.get(
954
+ "workflow_instructions_level", "system"
955
+ )
1247
956
  if level != "system":
1248
957
  instructions += f"\n\n## Workflow Instructions ({level} level)\n\n"
1249
958
  instructions += "**The following workflow instructions override system defaults:**\n\n"
@@ -1254,26 +963,28 @@ class FrameworkLoader:
1254
963
  memory_content = self._strip_metadata_comments(
1255
964
  self.framework_content["memory_instructions"]
1256
965
  )
1257
- level = self.framework_content.get("memory_instructions_level", "system")
966
+ level = self.framework_content.get(
967
+ "memory_instructions_level", "system"
968
+ )
1258
969
  if level != "system":
1259
970
  instructions += f"\n\n## Memory Instructions ({level} level)\n\n"
1260
971
  instructions += "**The following memory instructions override system defaults:**\n\n"
1261
972
  instructions += f"{memory_content}\n"
1262
-
973
+
1263
974
  # Add actual PM memories after memory instructions
1264
975
  if self.framework_content.get("actual_memories"):
1265
976
  instructions += "\n\n## Current PM Memories\n\n"
1266
977
  instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
1267
978
  instructions += self.framework_content["actual_memories"]
1268
979
  instructions += "\n"
1269
-
980
+
1270
981
  # Add agent memories if available
1271
982
  if self.framework_content.get("agent_memories"):
1272
983
  agent_memories = self.framework_content["agent_memories"]
1273
984
  if agent_memories:
1274
985
  instructions += "\n\n## Agent Memories\n\n"
1275
986
  instructions += "**The following are accumulated memories from specialized agents:**\n\n"
1276
-
987
+
1277
988
  for agent_name in sorted(agent_memories.keys()):
1278
989
  memory_content = agent_memories[agent_name]
1279
990
  if memory_content:
@@ -1296,10 +1007,12 @@ class FrameworkLoader:
1296
1007
  self.framework_content["base_pm_instructions"]
1297
1008
  )
1298
1009
  instructions += f"\n\n{base_pm}"
1299
-
1010
+
1300
1011
  # Inject output style content if needed (for Claude < 1.0.83)
1301
1012
  if inject_output_style and self.output_style_manager:
1302
- output_style_content = self.output_style_manager.get_injectable_content(framework_loader=self)
1013
+ output_style_content = self.output_style_manager.get_injectable_content(
1014
+ framework_loader=self
1015
+ )
1303
1016
  if output_style_content:
1304
1017
  instructions += "\n\n## Output Style Configuration\n"
1305
1018
  instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
@@ -1307,9 +1020,7 @@ class FrameworkLoader:
1307
1020
  instructions += "\n"
1308
1021
 
1309
1022
  # Clean up any trailing whitespace
1310
- instructions = instructions.rstrip() + "\n"
1311
-
1312
- return instructions
1023
+ return instructions.rstrip() + "\n"
1313
1024
 
1314
1025
  # Otherwise fall back to generating framework
1315
1026
  instructions = """# Claude MPM Framework Instructions
@@ -1432,23 +1143,21 @@ Extract tickets from these patterns:
1432
1143
  def _generate_agent_capabilities_section(self) -> str:
1433
1144
  """Generate dynamic agent capabilities section from deployed agents.
1434
1145
  Uses caching to avoid repeated file I/O and parsing operations."""
1435
-
1436
- # Check if cache is valid
1146
+
1147
+ # Try to get from cache first
1148
+ cached_capabilities = self._cache_manager.get_capabilities()
1149
+ if cached_capabilities is not None:
1150
+ return cached_capabilities
1151
+
1152
+ # Will be used for updating cache later
1437
1153
  current_time = time.time()
1438
- if (self._agent_capabilities_cache is not None and
1439
- current_time - self._agent_capabilities_cache_time < self.CAPABILITIES_CACHE_TTL):
1440
- cache_age = current_time - self._agent_capabilities_cache_time
1441
- self.logger.debug(f"Using cached agent capabilities (age: {cache_age:.1f}s)")
1442
- return self._agent_capabilities_cache
1443
-
1154
+
1444
1155
  # Cache miss or expired - generate capabilities
1445
1156
  self.logger.debug("Generating agent capabilities (cache miss or expired)")
1446
-
1157
+
1447
1158
  try:
1448
1159
  from pathlib import Path
1449
1160
 
1450
- import yaml
1451
-
1452
1161
  # Read directly from deployed agents in .claude/agents/
1453
1162
  # Check multiple locations for deployed agents
1454
1163
  # Priority order: project > user home > fallback
@@ -1456,47 +1165,55 @@ Extract tickets from these patterns:
1456
1165
  Path.cwd() / ".claude" / "agents", # Project-specific agents
1457
1166
  Path.home() / ".claude" / "agents", # User's system agents
1458
1167
  ]
1459
-
1168
+
1460
1169
  # Collect agents from all directories with proper precedence
1461
1170
  # Project agents override user agents with the same name
1462
1171
  all_agents = {} # key: agent_id, value: (agent_data, priority)
1463
-
1172
+
1464
1173
  for priority, potential_dir in enumerate(agents_dirs):
1465
1174
  if potential_dir.exists() and any(potential_dir.glob("*.md")):
1466
1175
  self.logger.debug(f"Found agents directory at: {potential_dir}")
1467
-
1176
+
1468
1177
  # Collect agents from this directory
1469
1178
  for agent_file in potential_dir.glob("*.md"):
1470
1179
  if agent_file.name.startswith("."):
1471
1180
  continue
1472
-
1181
+
1473
1182
  # Parse agent metadata (with caching)
1474
1183
  agent_data = self._parse_agent_metadata(agent_file)
1475
1184
  if agent_data:
1476
1185
  agent_id = agent_data["id"]
1477
1186
  # Only add if not already present (project has priority 0, user has priority 1)
1478
1187
  # Lower priority number wins (project > user)
1479
- if agent_id not in all_agents or priority < all_agents[agent_id][1]:
1188
+ if (
1189
+ agent_id not in all_agents
1190
+ or priority < all_agents[agent_id][1]
1191
+ ):
1480
1192
  all_agents[agent_id] = (agent_data, priority)
1481
- self.logger.debug(f"Added/Updated agent {agent_id} from {potential_dir} (priority {priority})")
1193
+ self.logger.debug(
1194
+ f"Added/Updated agent {agent_id} from {potential_dir} (priority {priority})"
1195
+ )
1482
1196
 
1483
1197
  if not all_agents:
1484
1198
  self.logger.warning(f"No agents found in any location: {agents_dirs}")
1485
1199
  result = self._get_fallback_capabilities()
1486
1200
  # Cache the fallback result too
1487
- self._agent_capabilities_cache = result
1488
- self._agent_capabilities_cache_time = current_time
1201
+ self._cache_manager.set_capabilities(result)
1489
1202
  return result
1490
-
1203
+
1491
1204
  # Log agent collection summary
1492
1205
  project_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 0]
1493
1206
  user_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 1]
1494
-
1207
+
1495
1208
  if project_agents:
1496
- self.logger.info(f"Loaded {len(project_agents)} project agents: {', '.join(sorted(project_agents))}")
1209
+ self.logger.info(
1210
+ f"Loaded {len(project_agents)} project agents: {', '.join(sorted(project_agents))}"
1211
+ )
1497
1212
  if user_agents:
1498
- self.logger.info(f"Loaded {len(user_agents)} user agents: {', '.join(sorted(user_agents))}")
1499
-
1213
+ self.logger.info(
1214
+ f"Loaded {len(user_agents)} user agents: {', '.join(sorted(user_agents))}"
1215
+ )
1216
+
1500
1217
  # Build capabilities section
1501
1218
  section = "\n\n## Available Agent Capabilities\n\n"
1502
1219
 
@@ -1506,8 +1223,7 @@ Extract tickets from these patterns:
1506
1223
  if not deployed_agents:
1507
1224
  result = self._get_fallback_capabilities()
1508
1225
  # Cache the fallback result
1509
- self._agent_capabilities_cache = result
1510
- self._agent_capabilities_cache_time = current_time
1226
+ self._cache_manager.set_capabilities(result)
1511
1227
  return result
1512
1228
 
1513
1229
  # Sort agents alphabetically by ID
@@ -1528,6 +1244,33 @@ Extract tickets from these patterns:
1528
1244
  section += f"\n### {display_name} (`{agent['id']}`)\n"
1529
1245
  section += f"{agent['description']}\n"
1530
1246
 
1247
+ # Add routing information if available
1248
+ if agent.get("routing"):
1249
+ routing = agent["routing"]
1250
+
1251
+ # Format routing hints for PM usage
1252
+ routing_hints = []
1253
+
1254
+ if routing.get("keywords"):
1255
+ # Show first 5 keywords for brevity
1256
+ keywords = routing["keywords"][:5]
1257
+ routing_hints.append(f"Keywords: {', '.join(keywords)}")
1258
+
1259
+ if routing.get("paths"):
1260
+ # Show first 3 paths for brevity
1261
+ paths = routing["paths"][:3]
1262
+ routing_hints.append(f"Paths: {', '.join(paths)}")
1263
+
1264
+ if routing.get("priority"):
1265
+ routing_hints.append(f"Priority: {routing['priority']}")
1266
+
1267
+ if routing_hints:
1268
+ section += f"- **Routing**: {' | '.join(routing_hints)}\n"
1269
+
1270
+ # Add when_to_use if present
1271
+ if routing.get("when_to_use"):
1272
+ section += f"- **When to use**: {routing['when_to_use']}\n"
1273
+
1531
1274
  # Add any additional metadata if present
1532
1275
  if agent.get("authority"):
1533
1276
  section += f"- **Authority**: {agent['authority']}\n"
@@ -1556,10 +1299,11 @@ Extract tickets from these patterns:
1556
1299
  section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
1557
1300
 
1558
1301
  # Cache the generated capabilities
1559
- self._agent_capabilities_cache = section
1560
- self._agent_capabilities_cache_time = current_time
1561
- self.logger.debug(f"Cached agent capabilities section ({len(section)} chars)")
1562
-
1302
+ self._cache_manager.set_capabilities(section)
1303
+ self.logger.debug(
1304
+ f"Cached agent capabilities section ({len(section)} chars)"
1305
+ )
1306
+
1563
1307
  return section
1564
1308
 
1565
1309
  except Exception as e:
@@ -1582,22 +1326,24 @@ Extract tickets from these patterns:
1582
1326
  cache_key = str(agent_file)
1583
1327
  file_mtime = agent_file.stat().st_mtime
1584
1328
  current_time = time.time()
1585
-
1586
- # Check if we have cached data for this file
1587
- if cache_key in self._agent_metadata_cache:
1588
- cached_data, cached_mtime = self._agent_metadata_cache[cache_key]
1329
+
1330
+ # Try to get from cache first
1331
+ cached_result = self._cache_manager.get_agent_metadata(cache_key)
1332
+ if cached_result is not None:
1333
+ cached_data, cached_mtime = cached_result
1589
1334
  # Use cache if file hasn't been modified and cache isn't too old
1590
- if (cached_mtime == file_mtime and
1591
- current_time - cached_mtime < self.METADATA_CACHE_TTL):
1335
+ if cached_mtime == file_mtime:
1592
1336
  self.logger.debug(f"Using cached metadata for {agent_file.name}")
1593
1337
  return cached_data
1594
-
1338
+
1595
1339
  # Cache miss or expired - parse the file
1596
- self.logger.debug(f"Parsing metadata for {agent_file.name} (cache miss or expired)")
1597
-
1340
+ self.logger.debug(
1341
+ f"Parsing metadata for {agent_file.name} (cache miss or expired)"
1342
+ )
1343
+
1598
1344
  import yaml
1599
1345
 
1600
- with open(agent_file, "r") as f:
1346
+ with open(agent_file) as f:
1601
1347
  content = f.read()
1602
1348
 
1603
1349
  # Default values
@@ -1632,15 +1378,89 @@ Extract tickets from these patterns:
1632
1378
  # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1633
1379
  # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1634
1380
 
1381
+ # Try to load routing metadata from JSON template if not in YAML frontmatter
1382
+ if "routing" not in agent_data:
1383
+ routing_data = self._load_routing_from_template(agent_file.stem)
1384
+ if routing_data:
1385
+ agent_data["routing"] = routing_data
1386
+
1635
1387
  # Cache the parsed metadata
1636
- self._agent_metadata_cache[cache_key] = (agent_data, file_mtime)
1637
-
1388
+ self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
1389
+
1638
1390
  return agent_data
1639
1391
 
1640
1392
  except Exception as e:
1641
1393
  self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
1642
1394
  return None
1643
1395
 
1396
+ def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
1397
+ """Load routing metadata from agent JSON template.
1398
+
1399
+ Args:
1400
+ agent_name: Name of the agent (stem of the file)
1401
+
1402
+ Returns:
1403
+ Dictionary with routing metadata or None if not found
1404
+ """
1405
+ try:
1406
+ import json
1407
+
1408
+ # Check if we have a framework path
1409
+ if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
1410
+ # For packaged installations, try to load from package resources
1411
+ if files:
1412
+ try:
1413
+ templates_package = files("claude_mpm.agents.templates")
1414
+ template_file = templates_package / f"{agent_name}.json"
1415
+
1416
+ if template_file.is_file():
1417
+ template_content = template_file.read_text()
1418
+ template_data = json.loads(template_content)
1419
+ return template_data.get("routing")
1420
+ except Exception as e:
1421
+ self.logger.debug(
1422
+ f"Could not load routing from packaged template for {agent_name}: {e}"
1423
+ )
1424
+ return None
1425
+
1426
+ # For development mode, load from filesystem
1427
+ templates_dir = (
1428
+ self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
1429
+ )
1430
+ template_file = templates_dir / f"{agent_name}.json"
1431
+
1432
+ if template_file.exists():
1433
+ with open(template_file) as f:
1434
+ template_data = json.load(f)
1435
+ return template_data.get("routing")
1436
+
1437
+ # Also check for variations in naming (underscore vs dash)
1438
+ # Handle common naming variations between deployed .md files and .json templates
1439
+ # Remove duplicates by using a set
1440
+ alternative_names = list(
1441
+ {
1442
+ agent_name.replace("-", "_"), # api-qa -> api_qa
1443
+ agent_name.replace("_", "-"), # api_qa -> api-qa
1444
+ agent_name.replace("-", ""), # api-qa -> apiqa
1445
+ agent_name.replace("_", ""), # api_qa -> apiqa
1446
+ }
1447
+ )
1448
+
1449
+ for alt_name in alternative_names:
1450
+ if alt_name != agent_name:
1451
+ alt_file = templates_dir / f"{alt_name}.json"
1452
+ if alt_file.exists():
1453
+ with open(alt_file) as f:
1454
+ template_data = json.load(f)
1455
+ return template_data.get("routing")
1456
+
1457
+ self.logger.debug(f"No JSON template found for agent: {agent_name}")
1458
+ return None
1459
+
1460
+ except Exception as e:
1461
+ self.logger.debug(f"Could not load routing metadata for {agent_name}: {e}")
1462
+ return None
1463
+
1644
1464
  def _generate_agent_selection_guide(self, deployed_agents: list) -> str:
1645
1465
  """Generate Context-Aware Agent Selection guide from deployed agents.
1646
1466
 
@@ -1660,55 +1480,55 @@ Extract tickets from these patterns:
1660
1480
  if "implementation" in desc_lower or (
1661
1481
  "engineer" in agent_id and "data" not in agent_id
1662
1482
  ):
1663
- selection_map[
1664
- "Implementation tasks"
1665
- ] = f"{agent['display_name']} (`{agent_id}`)"
1483
+ selection_map["Implementation tasks"] = (
1484
+ f"{agent['display_name']} (`{agent_id}`)"
1485
+ )
1666
1486
  if "codebase analysis" in desc_lower or "research" in agent_id:
1667
- selection_map[
1668
- "Codebase analysis"
1669
- ] = f"{agent['display_name']} (`{agent_id}`)"
1487
+ selection_map["Codebase analysis"] = (
1488
+ f"{agent['display_name']} (`{agent_id}`)"
1489
+ )
1670
1490
  if "testing" in desc_lower or "qa" in agent_id:
1671
- selection_map[
1672
- "Testing/quality"
1673
- ] = f"{agent['display_name']} (`{agent_id}`)"
1491
+ selection_map["Testing/quality"] = (
1492
+ f"{agent['display_name']} (`{agent_id}`)"
1493
+ )
1674
1494
  if "documentation" in desc_lower:
1675
- selection_map[
1676
- "Documentation"
1677
- ] = f"{agent['display_name']} (`{agent_id}`)"
1495
+ selection_map["Documentation"] = (
1496
+ f"{agent['display_name']} (`{agent_id}`)"
1497
+ )
1678
1498
  if "security" in desc_lower or "sast" in desc_lower:
1679
- selection_map[
1680
- "Security operations"
1681
- ] = f"{agent['display_name']} (`{agent_id}`)"
1499
+ selection_map["Security operations"] = (
1500
+ f"{agent['display_name']} (`{agent_id}`)"
1501
+ )
1682
1502
  if (
1683
1503
  "deployment" in desc_lower
1684
1504
  or "infrastructure" in desc_lower
1685
1505
  or "ops" in agent_id
1686
1506
  ):
1687
- selection_map[
1688
- "Deployment/infrastructure"
1689
- ] = f"{agent['display_name']} (`{agent_id}`)"
1507
+ selection_map["Deployment/infrastructure"] = (
1508
+ f"{agent['display_name']} (`{agent_id}`)"
1509
+ )
1690
1510
  if "data" in desc_lower and (
1691
1511
  "pipeline" in desc_lower or "etl" in desc_lower
1692
1512
  ):
1693
- selection_map[
1694
- "Data pipeline/ETL"
1695
- ] = f"{agent['display_name']} (`{agent_id}`)"
1513
+ selection_map["Data pipeline/ETL"] = (
1514
+ f"{agent['display_name']} (`{agent_id}`)"
1515
+ )
1696
1516
  if "git" in desc_lower or "version control" in desc_lower:
1697
- selection_map[
1698
- "Version control"
1699
- ] = f"{agent['display_name']} (`{agent_id}`)"
1517
+ selection_map["Version control"] = (
1518
+ f"{agent['display_name']} (`{agent_id}`)"
1519
+ )
1700
1520
  if "ticket" in desc_lower or "epic" in desc_lower:
1701
- selection_map[
1702
- "Ticket/issue management"
1703
- ] = f"{agent['display_name']} (`{agent_id}`)"
1521
+ selection_map["Ticket/issue management"] = (
1522
+ f"{agent['display_name']} (`{agent_id}`)"
1523
+ )
1704
1524
  if "browser" in desc_lower or "e2e" in desc_lower:
1705
- selection_map[
1706
- "Browser/E2E testing"
1707
- ] = f"{agent['display_name']} (`{agent_id}`)"
1525
+ selection_map["Browser/E2E testing"] = (
1526
+ f"{agent['display_name']} (`{agent_id}`)"
1527
+ )
1708
1528
  if "frontend" in desc_lower or "ui" in desc_lower or "html" in desc_lower:
1709
- selection_map[
1710
- "Frontend/UI development"
1711
- ] = f"{agent['display_name']} (`{agent_id}`)"
1529
+ selection_map["Frontend/UI development"] = (
1530
+ f"{agent['display_name']} (`{agent_id}`)"
1531
+ )
1712
1532
 
1713
1533
  # Always include PM questions
1714
1534
  selection_map["PM questions"] = "Answer directly (only exception)"