claude-mpm 4.1.1__py3-none-any.whl → 4.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) 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/imagemagick.json +256 -0
  18. claude_mpm/agents/templates/qa.json +41 -2
  19. claude_mpm/agents/templates/ticketing.json +5 -5
  20. claude_mpm/agents/templates/web_qa.json +50 -2
  21. claude_mpm/cli/__init__.py +51 -46
  22. claude_mpm/cli/__main__.py +1 -1
  23. claude_mpm/cli/commands/__init__.py +10 -12
  24. claude_mpm/cli/commands/agent_manager.py +186 -181
  25. claude_mpm/cli/commands/agents.py +271 -268
  26. claude_mpm/cli/commands/aggregate.py +30 -29
  27. claude_mpm/cli/commands/cleanup.py +50 -44
  28. claude_mpm/cli/commands/cleanup_orphaned_agents.py +25 -25
  29. claude_mpm/cli/commands/config.py +162 -127
  30. claude_mpm/cli/commands/doctor.py +52 -62
  31. claude_mpm/cli/commands/info.py +37 -25
  32. claude_mpm/cli/commands/mcp.py +3 -7
  33. claude_mpm/cli/commands/mcp_command_router.py +14 -18
  34. claude_mpm/cli/commands/mcp_install_commands.py +28 -23
  35. claude_mpm/cli/commands/mcp_pipx_config.py +58 -49
  36. claude_mpm/cli/commands/mcp_server_commands.py +23 -17
  37. claude_mpm/cli/commands/memory.py +192 -141
  38. claude_mpm/cli/commands/monitor.py +117 -88
  39. claude_mpm/cli/commands/run.py +120 -84
  40. claude_mpm/cli/commands/run_config_checker.py +4 -5
  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 +204 -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 +20 -23
  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 +581 -280
  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 -15
  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 +93 -38
  114. claude_mpm/hooks/claude_hooks/hook_handler.py +130 -76
  115. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +104 -77
  116. claude_mpm/hooks/claude_hooks/memory_integration.py +2 -4
  117. claude_mpm/hooks/claude_hooks/response_tracking.py +15 -11
  118. claude_mpm/hooks/claude_hooks/tool_analysis.py +12 -18
  119. claude_mpm/hooks/memory_integration_hook.py +5 -5
  120. claude_mpm/hooks/tool_call_interceptor.py +1 -1
  121. claude_mpm/hooks/validation_hooks.py +4 -4
  122. claude_mpm/init.py +4 -9
  123. claude_mpm/models/__init__.py +2 -2
  124. claude_mpm/models/agent_session.py +11 -14
  125. claude_mpm/scripts/mcp_server.py +20 -11
  126. claude_mpm/scripts/mcp_wrapper.py +5 -5
  127. claude_mpm/scripts/mpm_doctor.py +321 -0
  128. claude_mpm/scripts/socketio_daemon.py +28 -25
  129. claude_mpm/scripts/socketio_daemon_hardened.py +298 -258
  130. claude_mpm/scripts/socketio_server_manager.py +116 -95
  131. claude_mpm/services/__init__.py +49 -49
  132. claude_mpm/services/agent_capabilities_service.py +12 -18
  133. claude_mpm/services/agents/__init__.py +22 -22
  134. claude_mpm/services/agents/agent_builder.py +140 -119
  135. claude_mpm/services/agents/deployment/__init__.py +3 -3
  136. claude_mpm/services/agents/deployment/agent_config_provider.py +9 -9
  137. claude_mpm/services/agents/deployment/agent_configuration_manager.py +19 -20
  138. claude_mpm/services/agents/deployment/agent_definition_factory.py +1 -5
  139. claude_mpm/services/agents/deployment/agent_deployment.py +136 -106
  140. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -8
  141. claude_mpm/services/agents/deployment/agent_environment_manager.py +2 -7
  142. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +6 -10
  143. claude_mpm/services/agents/deployment/agent_format_converter.py +11 -15
  144. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +2 -3
  145. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +5 -5
  146. claude_mpm/services/agents/deployment/agent_metrics_collector.py +13 -19
  147. claude_mpm/services/agents/deployment/agent_restore_handler.py +0 -1
  148. claude_mpm/services/agents/deployment/agent_template_builder.py +26 -35
  149. claude_mpm/services/agents/deployment/agent_validator.py +0 -1
  150. claude_mpm/services/agents/deployment/agent_version_manager.py +7 -9
  151. claude_mpm/services/agents/deployment/agent_versioning.py +3 -3
  152. claude_mpm/services/agents/deployment/agents_directory_resolver.py +6 -7
  153. claude_mpm/services/agents/deployment/async_agent_deployment.py +51 -38
  154. claude_mpm/services/agents/deployment/config/__init__.py +1 -1
  155. claude_mpm/services/agents/deployment/config/deployment_config.py +7 -8
  156. claude_mpm/services/agents/deployment/deployment_type_detector.py +1 -1
  157. claude_mpm/services/agents/deployment/deployment_wrapper.py +18 -18
  158. claude_mpm/services/agents/deployment/facade/__init__.py +1 -1
  159. claude_mpm/services/agents/deployment/facade/deployment_executor.py +0 -3
  160. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -4
  161. claude_mpm/services/agents/deployment/interface_adapter.py +5 -7
  162. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +345 -276
  163. claude_mpm/services/agents/deployment/pipeline/__init__.py +2 -2
  164. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +1 -1
  165. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +6 -4
  166. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +3 -3
  167. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +2 -2
  168. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +14 -13
  169. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +0 -1
  170. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +1 -1
  171. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +8 -9
  172. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +1 -1
  173. claude_mpm/services/agents/deployment/processors/__init__.py +1 -1
  174. claude_mpm/services/agents/deployment/processors/agent_processor.py +20 -16
  175. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +5 -12
  176. claude_mpm/services/agents/deployment/results/__init__.py +1 -1
  177. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +1 -1
  178. claude_mpm/services/agents/deployment/strategies/__init__.py +2 -2
  179. claude_mpm/services/agents/deployment/strategies/base_strategy.py +1 -7
  180. claude_mpm/services/agents/deployment/strategies/project_strategy.py +1 -4
  181. claude_mpm/services/agents/deployment/strategies/system_strategy.py +2 -3
  182. claude_mpm/services/agents/deployment/strategies/user_strategy.py +3 -7
  183. claude_mpm/services/agents/deployment/validation/__init__.py +1 -1
  184. claude_mpm/services/agents/deployment/validation/agent_validator.py +1 -1
  185. claude_mpm/services/agents/deployment/validation/template_validator.py +2 -2
  186. claude_mpm/services/agents/deployment/validation/validation_result.py +2 -6
  187. claude_mpm/services/agents/loading/__init__.py +1 -1
  188. claude_mpm/services/agents/loading/agent_profile_loader.py +6 -12
  189. claude_mpm/services/agents/loading/base_agent_manager.py +5 -5
  190. claude_mpm/services/agents/loading/framework_agent_loader.py +2 -4
  191. claude_mpm/services/agents/management/__init__.py +1 -1
  192. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -3
  193. claude_mpm/services/agents/management/agent_management_service.py +5 -9
  194. claude_mpm/services/agents/memory/__init__.py +4 -4
  195. claude_mpm/services/agents/memory/agent_memory_manager.py +280 -160
  196. claude_mpm/services/agents/memory/agent_persistence_service.py +0 -2
  197. claude_mpm/services/agents/memory/content_manager.py +44 -38
  198. claude_mpm/services/agents/memory/template_generator.py +4 -6
  199. claude_mpm/services/agents/registry/__init__.py +10 -6
  200. claude_mpm/services/agents/registry/deployed_agent_discovery.py +30 -27
  201. claude_mpm/services/agents/registry/modification_tracker.py +3 -6
  202. claude_mpm/services/async_session_logger.py +1 -2
  203. claude_mpm/services/claude_session_logger.py +1 -2
  204. claude_mpm/services/command_deployment_service.py +173 -0
  205. claude_mpm/services/command_handler_service.py +20 -22
  206. claude_mpm/services/core/__init__.py +25 -25
  207. claude_mpm/services/core/base.py +0 -5
  208. claude_mpm/services/core/interfaces/__init__.py +32 -32
  209. claude_mpm/services/core/interfaces/agent.py +0 -21
  210. claude_mpm/services/core/interfaces/communication.py +0 -27
  211. claude_mpm/services/core/interfaces/infrastructure.py +0 -56
  212. claude_mpm/services/core/interfaces/service.py +0 -29
  213. claude_mpm/services/diagnostics/__init__.py +1 -1
  214. claude_mpm/services/diagnostics/checks/__init__.py +6 -6
  215. claude_mpm/services/diagnostics/checks/agent_check.py +89 -80
  216. claude_mpm/services/diagnostics/checks/base_check.py +12 -16
  217. claude_mpm/services/diagnostics/checks/claude_desktop_check.py +84 -81
  218. claude_mpm/services/diagnostics/checks/common_issues_check.py +99 -91
  219. claude_mpm/services/diagnostics/checks/configuration_check.py +82 -77
  220. claude_mpm/services/diagnostics/checks/filesystem_check.py +67 -68
  221. claude_mpm/services/diagnostics/checks/installation_check.py +254 -94
  222. claude_mpm/services/diagnostics/checks/mcp_check.py +90 -88
  223. claude_mpm/services/diagnostics/checks/monitor_check.py +75 -76
  224. claude_mpm/services/diagnostics/checks/startup_log_check.py +67 -73
  225. claude_mpm/services/diagnostics/diagnostic_runner.py +67 -59
  226. claude_mpm/services/diagnostics/doctor_reporter.py +107 -70
  227. claude_mpm/services/diagnostics/models.py +21 -19
  228. claude_mpm/services/event_aggregator.py +10 -17
  229. claude_mpm/services/event_bus/__init__.py +1 -1
  230. claude_mpm/services/event_bus/config.py +54 -35
  231. claude_mpm/services/event_bus/event_bus.py +76 -71
  232. claude_mpm/services/event_bus/relay.py +74 -64
  233. claude_mpm/services/events/__init__.py +11 -11
  234. claude_mpm/services/events/consumers/__init__.py +3 -3
  235. claude_mpm/services/events/consumers/dead_letter.py +71 -63
  236. claude_mpm/services/events/consumers/logging.py +39 -37
  237. claude_mpm/services/events/consumers/metrics.py +56 -57
  238. claude_mpm/services/events/consumers/socketio.py +82 -81
  239. claude_mpm/services/events/core.py +110 -99
  240. claude_mpm/services/events/interfaces.py +56 -72
  241. claude_mpm/services/events/producers/__init__.py +1 -1
  242. claude_mpm/services/events/producers/hook.py +38 -38
  243. claude_mpm/services/events/producers/system.py +46 -44
  244. claude_mpm/services/exceptions.py +81 -80
  245. claude_mpm/services/framework_claude_md_generator/__init__.py +2 -4
  246. claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -5
  247. claude_mpm/services/framework_claude_md_generator/content_validator.py +1 -1
  248. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +4 -4
  249. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +0 -1
  250. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +0 -2
  251. claude_mpm/services/framework_claude_md_generator/version_manager.py +4 -5
  252. claude_mpm/services/hook_service.py +6 -9
  253. claude_mpm/services/infrastructure/__init__.py +1 -1
  254. claude_mpm/services/infrastructure/context_preservation.py +8 -12
  255. claude_mpm/services/infrastructure/monitoring.py +21 -23
  256. claude_mpm/services/mcp_gateway/__init__.py +37 -37
  257. claude_mpm/services/mcp_gateway/auto_configure.py +95 -103
  258. claude_mpm/services/mcp_gateway/config/__init__.py +1 -1
  259. claude_mpm/services/mcp_gateway/config/config_loader.py +23 -25
  260. claude_mpm/services/mcp_gateway/config/config_schema.py +5 -5
  261. claude_mpm/services/mcp_gateway/config/configuration.py +9 -6
  262. claude_mpm/services/mcp_gateway/core/__init__.py +10 -10
  263. claude_mpm/services/mcp_gateway/core/base.py +0 -3
  264. claude_mpm/services/mcp_gateway/core/interfaces.py +1 -38
  265. claude_mpm/services/mcp_gateway/core/process_pool.py +99 -93
  266. claude_mpm/services/mcp_gateway/core/singleton_manager.py +65 -62
  267. claude_mpm/services/mcp_gateway/core/startup_verification.py +75 -74
  268. claude_mpm/services/mcp_gateway/main.py +2 -1
  269. claude_mpm/services/mcp_gateway/registry/service_registry.py +5 -8
  270. claude_mpm/services/mcp_gateway/registry/tool_registry.py +1 -1
  271. claude_mpm/services/mcp_gateway/server/__init__.py +1 -1
  272. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +12 -19
  273. claude_mpm/services/mcp_gateway/server/stdio_handler.py +4 -3
  274. claude_mpm/services/mcp_gateway/server/stdio_server.py +79 -71
  275. claude_mpm/services/mcp_gateway/tools/__init__.py +2 -2
  276. claude_mpm/services/mcp_gateway/tools/base_adapter.py +5 -6
  277. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +13 -22
  278. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +79 -78
  279. claude_mpm/services/mcp_gateway/tools/hello_world.py +12 -14
  280. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +42 -49
  281. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +51 -55
  282. claude_mpm/services/memory/__init__.py +3 -3
  283. claude_mpm/services/memory/builder.py +3 -6
  284. claude_mpm/services/memory/cache/__init__.py +1 -1
  285. claude_mpm/services/memory/cache/shared_prompt_cache.py +3 -5
  286. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  287. claude_mpm/services/memory/indexed_memory.py +5 -7
  288. claude_mpm/services/memory/optimizer.py +7 -10
  289. claude_mpm/services/memory/router.py +8 -9
  290. claude_mpm/services/memory_hook_service.py +48 -34
  291. claude_mpm/services/monitor_build_service.py +77 -73
  292. claude_mpm/services/port_manager.py +130 -108
  293. claude_mpm/services/project/analyzer.py +12 -10
  294. claude_mpm/services/project/registry.py +11 -11
  295. claude_mpm/services/recovery_manager.py +10 -19
  296. claude_mpm/services/response_tracker.py +0 -1
  297. claude_mpm/services/runner_configuration_service.py +19 -20
  298. claude_mpm/services/session_management_service.py +7 -11
  299. claude_mpm/services/shared/__init__.py +1 -1
  300. claude_mpm/services/shared/async_service_base.py +58 -50
  301. claude_mpm/services/shared/config_service_base.py +73 -67
  302. claude_mpm/services/shared/lifecycle_service_base.py +82 -78
  303. claude_mpm/services/shared/manager_base.py +94 -82
  304. claude_mpm/services/shared/service_factory.py +96 -98
  305. claude_mpm/services/socketio/__init__.py +3 -3
  306. claude_mpm/services/socketio/client_proxy.py +5 -5
  307. claude_mpm/services/socketio/event_normalizer.py +199 -181
  308. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  309. claude_mpm/services/socketio/handlers/base.py +5 -4
  310. claude_mpm/services/socketio/handlers/connection.py +163 -136
  311. claude_mpm/services/socketio/handlers/file.py +13 -14
  312. claude_mpm/services/socketio/handlers/git.py +12 -7
  313. claude_mpm/services/socketio/handlers/hook.py +49 -44
  314. claude_mpm/services/socketio/handlers/memory.py +0 -1
  315. claude_mpm/services/socketio/handlers/project.py +0 -1
  316. claude_mpm/services/socketio/handlers/registry.py +37 -19
  317. claude_mpm/services/socketio/migration_utils.py +98 -84
  318. claude_mpm/services/socketio/server/__init__.py +1 -1
  319. claude_mpm/services/socketio/server/broadcaster.py +81 -87
  320. claude_mpm/services/socketio/server/core.py +65 -54
  321. claude_mpm/services/socketio/server/eventbus_integration.py +95 -56
  322. claude_mpm/services/socketio/server/main.py +64 -38
  323. claude_mpm/services/socketio_client_manager.py +10 -12
  324. claude_mpm/services/subprocess_launcher_service.py +4 -7
  325. claude_mpm/services/system_instructions_service.py +13 -14
  326. claude_mpm/services/ticket_manager.py +2 -2
  327. claude_mpm/services/utility_service.py +5 -13
  328. claude_mpm/services/version_control/__init__.py +16 -16
  329. claude_mpm/services/version_control/branch_strategy.py +5 -8
  330. claude_mpm/services/version_control/conflict_resolution.py +9 -23
  331. claude_mpm/services/version_control/git_operations.py +5 -7
  332. claude_mpm/services/version_control/semantic_versioning.py +16 -17
  333. claude_mpm/services/version_control/version_parser.py +13 -18
  334. claude_mpm/services/version_service.py +10 -11
  335. claude_mpm/storage/__init__.py +1 -1
  336. claude_mpm/storage/state_storage.py +22 -28
  337. claude_mpm/utils/__init__.py +6 -6
  338. claude_mpm/utils/agent_dependency_loader.py +47 -33
  339. claude_mpm/utils/config_manager.py +11 -14
  340. claude_mpm/utils/dependency_cache.py +1 -1
  341. claude_mpm/utils/dependency_manager.py +13 -17
  342. claude_mpm/utils/dependency_strategies.py +8 -10
  343. claude_mpm/utils/environment_context.py +3 -9
  344. claude_mpm/utils/error_handler.py +3 -13
  345. claude_mpm/utils/file_utils.py +1 -1
  346. claude_mpm/utils/path_operations.py +8 -12
  347. claude_mpm/utils/robust_installer.py +110 -33
  348. claude_mpm/utils/subprocess_utils.py +5 -6
  349. claude_mpm/validation/agent_validator.py +3 -6
  350. claude_mpm/validation/frontmatter_validator.py +1 -1
  351. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.2.dist-info}/METADATA +1 -1
  352. claude_mpm-4.1.2.dist-info/RECORD +498 -0
  353. claude_mpm-4.1.1.dist-info/RECORD +0 -494
  354. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.2.dist-info}/WHEEL +0 -0
  355. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.2.dist-info}/entry_points.txt +0 -0
  356. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.2.dist-info}/licenses/LICENSE +0 -0
  357. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,6 @@
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
@@ -38,33 +36,33 @@ class FrameworkLoader:
38
36
  2. Loading custom instructions from .claude-mpm/ directories
39
37
  3. Preparing agent definitions
40
38
  4. Formatting for injection
41
-
39
+
42
40
  Custom Instructions Loading:
43
41
  The framework loader supports custom instructions through .claude-mpm/ directories.
44
42
  It NEVER reads from .claude/ directories to avoid conflicts with Claude Code.
45
-
43
+
46
44
  File Loading Precedence (highest to lowest):
47
-
45
+
48
46
  INSTRUCTIONS.md:
49
47
  1. Project: ./.claude-mpm/INSTRUCTIONS.md
50
48
  2. User: ~/.claude-mpm/INSTRUCTIONS.md
51
49
  3. System: (built-in framework instructions)
52
-
50
+
53
51
  WORKFLOW.md:
54
52
  1. Project: ./.claude-mpm/WORKFLOW.md
55
53
  2. User: ~/.claude-mpm/WORKFLOW.md
56
54
  3. System: src/claude_mpm/agents/WORKFLOW.md
57
-
55
+
58
56
  MEMORY.md:
59
57
  1. Project: ./.claude-mpm/MEMORY.md
60
58
  2. User: ~/.claude-mpm/MEMORY.md
61
59
  3. System: src/claude_mpm/agents/MEMORY.md
62
-
60
+
63
61
  Actual Memories:
64
62
  - User: ~/.claude-mpm/memories/PM_memories.md
65
63
  - Project: ./.claude-mpm/memories/PM_memories.md (overrides user)
66
64
  - Agent memories: *_memories.md files (only loaded if agent is deployed)
67
-
65
+
68
66
  Important Notes:
69
67
  - Project-level files always override user-level files
70
68
  - User-level files always override system defaults
@@ -87,31 +85,33 @@ class FrameworkLoader:
87
85
  self.agents_dir = agents_dir
88
86
  self.framework_version = None
89
87
  self.framework_last_modified = None
90
-
88
+
91
89
  # Performance optimization: Initialize caches
92
90
  self._agent_capabilities_cache: Optional[str] = None
93
91
  self._agent_capabilities_cache_time: float = 0
94
92
  self._deployed_agents_cache: Optional[Set[str]] = None
95
93
  self._deployed_agents_cache_time: float = 0
96
- self._agent_metadata_cache: Dict[str, Tuple[Optional[Dict[str, Any]], float]] = {}
94
+ self._agent_metadata_cache: Dict[
95
+ str, Tuple[Optional[Dict[str, Any]], float]
96
+ ] = {}
97
97
  self._memories_cache: Optional[Dict[str, Any]] = None
98
98
  self._memories_cache_time: float = 0
99
-
99
+
100
100
  # Cache TTL settings (in seconds)
101
101
  self.CAPABILITIES_CACHE_TTL = 60 # 60 seconds for capabilities
102
102
  self.DEPLOYED_AGENTS_CACHE_TTL = 30 # 30 seconds for deployed agents
103
103
  self.METADATA_CACHE_TTL = 60 # 60 seconds for agent metadata
104
104
  self.MEMORIES_CACHE_TTL = 60 # 60 seconds for memories
105
-
105
+
106
106
  self.framework_content = self._load_framework_content()
107
107
 
108
108
  # Initialize agent registry
109
109
  self.agent_registry = AgentRegistryAdapter(self.framework_path)
110
-
110
+
111
111
  # Initialize output style manager (must be after content is loaded)
112
112
  self.output_style_manager = None
113
113
  # Defer initialization until first use to ensure content is loaded
114
-
114
+
115
115
  def clear_all_caches(self) -> None:
116
116
  """Clear all caches to force reload on next access."""
117
117
  self.logger.info("Clearing all framework loader caches")
@@ -122,7 +122,7 @@ class FrameworkLoader:
122
122
  self._agent_metadata_cache.clear()
123
123
  self._memories_cache = None
124
124
  self._memories_cache_time = 0
125
-
125
+
126
126
  def clear_agent_caches(self) -> None:
127
127
  """Clear agent-related caches (capabilities, deployed agents, metadata)."""
128
128
  self.logger.info("Clearing agent-related caches")
@@ -131,7 +131,7 @@ class FrameworkLoader:
131
131
  self._deployed_agents_cache = None
132
132
  self._deployed_agents_cache_time = 0
133
133
  self._agent_metadata_cache.clear()
134
-
134
+
135
135
  def clear_memory_caches(self) -> None:
136
136
  """Clear memory-related caches."""
137
137
  self.logger.info("Clearing memory caches")
@@ -142,98 +142,132 @@ class FrameworkLoader:
142
142
  """Initialize output style management and deploy if applicable."""
143
143
  try:
144
144
  from claude_mpm.core.output_style_manager import OutputStyleManager
145
-
145
+
146
146
  self.output_style_manager = OutputStyleManager()
147
-
147
+
148
148
  # Log detailed output style status
149
149
  self._log_output_style_status()
150
-
150
+
151
151
  # 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
-
152
+ output_style_content = (
153
+ self.output_style_manager.extract_output_style_content(
154
+ framework_loader=self
155
+ )
156
+ )
157
+ self.output_style_manager.save_output_style(output_style_content)
158
+
155
159
  # Deploy to Claude Code if supported
156
- deployed = self.output_style_manager.deploy_output_style(output_style_content)
157
-
160
+ deployed = self.output_style_manager.deploy_output_style(
161
+ output_style_content
162
+ )
163
+
158
164
  if deployed:
159
165
  self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
160
166
  else:
161
- self.logger.info("📝 Output style will be injected into instructions for older Claude versions")
162
-
167
+ self.logger.info(
168
+ "📝 Output style will be injected into instructions for older Claude versions"
169
+ )
170
+
163
171
  except Exception as e:
164
172
  self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
165
173
  # Continue without output style management
166
-
174
+
167
175
  def _log_output_style_status(self) -> None:
168
176
  """Log comprehensive output style status information."""
169
177
  if not self.output_style_manager:
170
178
  return
171
-
179
+
172
180
  # Claude version detection
173
181
  claude_version = self.output_style_manager.claude_version
174
182
  if claude_version:
175
183
  self.logger.info(f"Claude Code version detected: {claude_version}")
176
-
184
+
177
185
  # Check if version supports output styles
178
186
  if self.output_style_manager.supports_output_styles():
179
187
  self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
180
-
188
+
181
189
  # Check deployment status
182
190
  output_style_path = self.output_style_manager.output_style_path
183
191
  if output_style_path.exists():
184
- self.logger.info(f"📁 Output style file exists: {output_style_path}")
192
+ self.logger.info(
193
+ f"📁 Output style file exists: {output_style_path}"
194
+ )
185
195
  else:
186
- self.logger.info(f"📝 Output style will be created at: {output_style_path}")
187
-
196
+ self.logger.info(
197
+ f"📝 Output style will be created at: {output_style_path}"
198
+ )
199
+
188
200
  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")
201
+ self.logger.info(
202
+ f"⚠️ Claude Code {claude_version} does not support output styles (< 1.0.83)"
203
+ )
204
+ self.logger.info(
205
+ "📝 Output style content will be injected into framework instructions"
206
+ )
191
207
  else:
192
208
  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")
209
+ self.logger.info(
210
+ "📝 Output style content will be injected into framework instructions as fallback"
211
+ )
194
212
 
195
213
  def _detect_framework_path(self) -> Optional[Path]:
196
214
  """Auto-detect claude-mpm framework using unified path management."""
197
215
  try:
198
216
  # Use the unified path manager for consistent detection
199
- from ..core.unified_paths import get_path_manager, DeploymentContext
217
+ from ..core.unified_paths import DeploymentContext, get_path_manager
200
218
 
201
219
  path_manager = get_path_manager()
202
220
  deployment_context = path_manager._deployment_context
203
221
 
204
222
  # 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})")
223
+ if deployment_context in [
224
+ DeploymentContext.PIP_INSTALL,
225
+ DeploymentContext.PIPX_INSTALL,
226
+ DeploymentContext.SYSTEM_PACKAGE,
227
+ ]:
228
+ self.logger.info(
229
+ f"Running from packaged installation (context: {deployment_context})"
230
+ )
207
231
  # Return a marker path to indicate packaged installation
208
232
  return Path("__PACKAGED__")
209
- elif deployment_context == DeploymentContext.DEVELOPMENT:
233
+ if deployment_context == DeploymentContext.DEVELOPMENT:
210
234
  # Development mode - use framework root
211
235
  framework_root = path_manager.framework_root
212
236
  if (framework_root / "src" / "claude_mpm" / "agents").exists():
213
- self.logger.info(f"Using claude-mpm development installation at: {framework_root}")
237
+ self.logger.info(
238
+ f"Using claude-mpm development installation at: {framework_root}"
239
+ )
214
240
  return framework_root
215
241
  elif deployment_context == DeploymentContext.EDITABLE_INSTALL:
216
242
  # Editable install - similar to development
217
243
  framework_root = path_manager.framework_root
218
244
  if (framework_root / "src" / "claude_mpm" / "agents").exists():
219
- self.logger.info(f"Using claude-mpm editable installation at: {framework_root}")
245
+ self.logger.info(
246
+ f"Using claude-mpm editable installation at: {framework_root}"
247
+ )
220
248
  return framework_root
221
249
 
222
250
  except Exception as e:
223
- self.logger.warning(f"Failed to use unified path manager for framework detection: {e}")
251
+ self.logger.warning(
252
+ f"Failed to use unified path manager for framework detection: {e}"
253
+ )
224
254
  # Fall back to original detection logic
225
- pass
226
255
 
227
256
  # Fallback: Original detection logic for compatibility
228
257
  try:
229
258
  # Check if the package is installed
230
259
  import claude_mpm
260
+
231
261
  package_file = Path(claude_mpm.__file__)
232
262
 
233
263
  # For packaged installations, we don't need a framework path
234
264
  # 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}")
265
+ if "site-packages" in str(package_file) or "dist-packages" in str(
266
+ package_file
267
+ ):
268
+ self.logger.info(
269
+ f"Running from packaged installation at: {package_file.parent}"
270
+ )
237
271
  # Return a marker path to indicate packaged installation
238
272
  return Path("__PACKAGED__")
239
273
  except ImportError:
@@ -276,7 +310,11 @@ class FrameworkLoader:
276
310
  import subprocess
277
311
 
278
312
  result = subprocess.run(
279
- ["npm", "root", "-g"], capture_output=True, text=True, timeout=5
313
+ ["npm", "root", "-g"],
314
+ capture_output=True,
315
+ text=True,
316
+ timeout=5,
317
+ check=False,
280
318
  )
281
319
  if result.returncode == 0:
282
320
  npm_root = Path(result.stdout.strip())
@@ -364,10 +402,10 @@ class FrameworkLoader:
364
402
  def _migrate_memory_file(self, old_path: Path, new_path: Path) -> None:
365
403
  """
366
404
  Migrate memory file from old naming convention to new.
367
-
405
+
368
406
  WHY: Supports backward compatibility by automatically migrating from
369
407
  the old {agent_id}_agent.md and {agent_id}.md formats to the new {agent_id}_memories.md format.
370
-
408
+
371
409
  Args:
372
410
  old_path: Path to the old file
373
411
  new_path: Path to the new file
@@ -380,7 +418,9 @@ class FrameworkLoader:
380
418
  new_path.write_text(content, encoding="utf-8")
381
419
  # Remove old file
382
420
  old_path.unlink()
383
- self.logger.info(f"Migrated memory file from {old_path.name} to {new_path.name}")
421
+ self.logger.info(
422
+ f"Migrated memory file from {old_path.name} to {new_path.name}"
423
+ )
384
424
  except Exception as e:
385
425
  self.logger.error(f"Failed to migrate memory file {old_path.name}: {e}")
386
426
 
@@ -391,7 +431,7 @@ class FrameworkLoader:
391
431
  Precedence (highest to lowest):
392
432
  1. Project-specific: ./.claude-mpm/INSTRUCTIONS.md
393
433
  2. User-specific: ~/.claude-mpm/INSTRUCTIONS.md
394
-
434
+
395
435
  NOTE: We do NOT load CLAUDE.md files since Claude Code already picks them up automatically.
396
436
  This prevents duplication of instructions.
397
437
 
@@ -407,9 +447,11 @@ class FrameworkLoader:
407
447
  if loaded_content:
408
448
  content["custom_instructions"] = loaded_content
409
449
  content["custom_instructions_level"] = "project"
410
- self.logger.info("Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md")
450
+ self.logger.info(
451
+ "Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md"
452
+ )
411
453
  return
412
-
454
+
413
455
  # Check for user-specific INSTRUCTIONS.md
414
456
  user_instructions_path = Path.home() / ".claude-mpm" / "INSTRUCTIONS.md"
415
457
  if user_instructions_path.exists():
@@ -419,7 +461,9 @@ class FrameworkLoader:
419
461
  if loaded_content:
420
462
  content["custom_instructions"] = loaded_content
421
463
  content["custom_instructions_level"] = "user"
422
- self.logger.info("Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md")
464
+ self.logger.info(
465
+ "Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md"
466
+ )
423
467
  return
424
468
 
425
469
  def _load_workflow_instructions(self, content: Dict[str, Any]) -> None:
@@ -430,7 +474,7 @@ class FrameworkLoader:
430
474
  1. Project-specific: ./.claude-mpm/WORKFLOW.md
431
475
  2. User-specific: ~/.claude-mpm/WORKFLOW.md
432
476
  3. System default: src/claude_mpm/agents/WORKFLOW.md or packaged
433
-
477
+
434
478
  NOTE: We do NOT load from .claude/ directories to avoid conflicts.
435
479
 
436
480
  Args:
@@ -445,9 +489,11 @@ class FrameworkLoader:
445
489
  if loaded_content:
446
490
  content["workflow_instructions"] = loaded_content
447
491
  content["workflow_instructions_level"] = "project"
448
- self.logger.info("Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md")
492
+ self.logger.info(
493
+ "Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md"
494
+ )
449
495
  return
450
-
496
+
451
497
  # Check for user-specific WORKFLOW.md (medium priority)
452
498
  user_workflow_path = Path.home() / ".claude-mpm" / "WORKFLOW.md"
453
499
  if user_workflow_path.exists():
@@ -457,7 +503,9 @@ class FrameworkLoader:
457
503
  if loaded_content:
458
504
  content["workflow_instructions"] = loaded_content
459
505
  content["workflow_instructions_level"] = "user"
460
- self.logger.info("Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md")
506
+ self.logger.info(
507
+ "Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md"
508
+ )
461
509
  return
462
510
 
463
511
  # Fall back to system workflow (lowest priority)
@@ -482,7 +530,7 @@ class FrameworkLoader:
482
530
  1. Project-specific: ./.claude-mpm/MEMORY.md
483
531
  2. User-specific: ~/.claude-mpm/MEMORY.md
484
532
  3. System default: src/claude_mpm/agents/MEMORY.md or packaged
485
-
533
+
486
534
  NOTE: We do NOT load from .claude/ directories to avoid conflicts.
487
535
 
488
536
  Args:
@@ -497,9 +545,11 @@ class FrameworkLoader:
497
545
  if loaded_content:
498
546
  content["memory_instructions"] = loaded_content
499
547
  content["memory_instructions_level"] = "project"
500
- self.logger.info("Using project-specific memory instructions from .claude-mpm/MEMORY.md")
548
+ self.logger.info(
549
+ "Using project-specific memory instructions from .claude-mpm/MEMORY.md"
550
+ )
501
551
  return
502
-
552
+
503
553
  # Check for user-specific MEMORY.md (medium priority)
504
554
  user_memory_path = Path.home() / ".claude-mpm" / "MEMORY.md"
505
555
  if user_memory_path.exists():
@@ -509,7 +559,9 @@ class FrameworkLoader:
509
559
  if loaded_content:
510
560
  content["memory_instructions"] = loaded_content
511
561
  content["memory_instructions_level"] = "user"
512
- self.logger.info("Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md")
562
+ self.logger.info(
563
+ "Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md"
564
+ )
513
565
  return
514
566
 
515
567
  # Fall back to system memory instructions (lowest priority)
@@ -525,132 +577,161 @@ class FrameworkLoader:
525
577
  content["memory_instructions"] = loaded_content
526
578
  content["memory_instructions_level"] = "system"
527
579
  self.logger.info("Using system memory instructions")
528
-
580
+
529
581
  def _get_deployed_agents(self) -> set:
530
582
  """
531
583
  Get a set of deployed agent names from .claude/agents/ directories.
532
584
  Uses caching to avoid repeated filesystem scans.
533
-
585
+
534
586
  Returns:
535
587
  Set of agent names (file stems) that are deployed
536
588
  """
537
589
  # Check if cache is valid
538
590
  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)")
591
+ if (
592
+ self._deployed_agents_cache is not None
593
+ and current_time - self._deployed_agents_cache_time
594
+ < self.DEPLOYED_AGENTS_CACHE_TTL
595
+ ):
596
+ self.logger.debug(
597
+ f"Using cached deployed agents (age: {current_time - self._deployed_agents_cache_time:.1f}s)"
598
+ )
542
599
  return self._deployed_agents_cache
543
-
600
+
544
601
  # Cache miss or expired - perform actual scan
545
602
  self.logger.debug("Scanning for deployed agents (cache miss or expired)")
546
603
  deployed = set()
547
-
604
+
548
605
  # Check multiple locations for deployed agents
549
606
  agents_dirs = [
550
607
  Path.cwd() / ".claude" / "agents", # Project-specific agents
551
608
  Path.home() / ".claude" / "agents", # User's system agents
552
609
  ]
553
-
610
+
554
611
  for agents_dir in agents_dirs:
555
612
  if agents_dir.exists():
556
613
  for agent_file in agents_dir.glob("*.md"):
557
614
  if not agent_file.name.startswith("."):
558
615
  # Use stem to get agent name without extension
559
616
  deployed.add(agent_file.stem)
560
- self.logger.debug(f"Found deployed agent: {agent_file.stem} in {agents_dir}")
561
-
617
+ self.logger.debug(
618
+ f"Found deployed agent: {agent_file.stem} in {agents_dir}"
619
+ )
620
+
562
621
  self.logger.debug(f"Total deployed agents found: {len(deployed)}")
563
-
622
+
564
623
  # Update cache
565
624
  self._deployed_agents_cache = deployed
566
625
  self._deployed_agents_cache_time = current_time
567
-
626
+
568
627
  return deployed
569
-
628
+
570
629
  def _load_actual_memories(self, content: Dict[str, Any]) -> None:
571
630
  """
572
631
  Load actual memories from both user and project directories.
573
632
  Uses caching to avoid repeated file I/O operations.
574
-
633
+
575
634
  Loading order:
576
635
  1. User-level memories from ~/.claude-mpm/memories/ (global defaults)
577
636
  2. Project-level memories from ./.claude-mpm/memories/ (overrides user)
578
-
637
+
579
638
  This loads:
580
639
  1. PM memories from PM_memories.md (always loaded)
581
640
  2. Agent memories from <agent>_memories.md (only if agent is deployed)
582
-
641
+
583
642
  Args:
584
643
  content: Dictionary to update with actual memories
585
644
  """
586
645
  # Check if cache is valid
587
646
  current_time = time.time()
588
- if (self._memories_cache is not None and
589
- current_time - self._memories_cache_time < self.MEMORIES_CACHE_TTL):
647
+ if (
648
+ self._memories_cache is not None
649
+ and current_time - self._memories_cache_time < self.MEMORIES_CACHE_TTL
650
+ ):
590
651
  cache_age = current_time - self._memories_cache_time
591
652
  self.logger.debug(f"Using cached memories (age: {cache_age:.1f}s)")
592
-
653
+
593
654
  # Apply cached memories to content
594
655
  if "actual_memories" in self._memories_cache:
595
656
  content["actual_memories"] = self._memories_cache["actual_memories"]
596
657
  if "agent_memories" in self._memories_cache:
597
658
  content["agent_memories"] = self._memories_cache["agent_memories"]
598
659
  return
599
-
660
+
600
661
  # Cache miss or expired - perform actual loading
601
662
  self.logger.debug("Loading memories from disk (cache miss or expired)")
602
-
663
+
603
664
  # Define memory directories in priority order (user first, then project)
604
665
  user_memories_dir = Path.home() / ".claude-mpm" / "memories"
605
666
  project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
606
-
667
+
607
668
  # Check for deployed agents
608
669
  deployed_agents = self._get_deployed_agents()
609
-
670
+
610
671
  # Track loading statistics
611
672
  loaded_count = 0
612
673
  skipped_count = 0
613
-
674
+
614
675
  # Dictionary to store aggregated memories
615
676
  pm_memories = []
616
677
  agent_memories_dict = {}
617
-
678
+
618
679
  # Load memories from user directory first
619
680
  if user_memories_dir.exists():
620
- self.logger.info(f"Loading user-level memory files from: {user_memories_dir}")
681
+ self.logger.info(
682
+ f"Loading user-level memory files from: {user_memories_dir}"
683
+ )
621
684
  loaded, skipped = self._load_memories_from_directory(
622
- user_memories_dir, deployed_agents, pm_memories, agent_memories_dict, "user"
685
+ user_memories_dir,
686
+ deployed_agents,
687
+ pm_memories,
688
+ agent_memories_dict,
689
+ "user",
623
690
  )
624
691
  loaded_count += loaded
625
692
  skipped_count += skipped
626
693
  else:
627
- self.logger.debug(f"No user memories directory found at: {user_memories_dir}")
628
-
694
+ self.logger.debug(
695
+ f"No user memories directory found at: {user_memories_dir}"
696
+ )
697
+
629
698
  # Load memories from project directory (overrides user memories)
630
699
  if project_memories_dir.exists():
631
- self.logger.info(f"Loading project-level memory files from: {project_memories_dir}")
700
+ self.logger.info(
701
+ f"Loading project-level memory files from: {project_memories_dir}"
702
+ )
632
703
  loaded, skipped = self._load_memories_from_directory(
633
- project_memories_dir, deployed_agents, pm_memories, agent_memories_dict, "project"
704
+ project_memories_dir,
705
+ deployed_agents,
706
+ pm_memories,
707
+ agent_memories_dict,
708
+ "project",
634
709
  )
635
710
  loaded_count += loaded
636
711
  skipped_count += skipped
637
712
  else:
638
- self.logger.debug(f"No project memories directory found at: {project_memories_dir}")
639
-
713
+ self.logger.debug(
714
+ f"No project memories directory found at: {project_memories_dir}"
715
+ )
716
+
640
717
  # Aggregate PM memories
641
718
  if pm_memories:
642
719
  aggregated_pm = self._aggregate_memories(pm_memories)
643
720
  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
-
721
+ memory_size = len(aggregated_pm.encode("utf-8"))
722
+ self.logger.info(
723
+ f"Aggregated PM memory ({memory_size:,} bytes) from {len(pm_memories)} source(s)"
724
+ )
725
+
647
726
  # Store agent memories (already aggregated per agent)
648
727
  if agent_memories_dict:
649
728
  content["agent_memories"] = agent_memories_dict
650
729
  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
-
730
+ memory_size = len(memory_content.encode("utf-8"))
731
+ self.logger.debug(
732
+ f"Aggregated {agent_name} memory: {memory_size:,} bytes"
733
+ )
734
+
654
735
  # Update cache with loaded memories
655
736
  self._memories_cache = {}
656
737
  if "actual_memories" in content:
@@ -658,61 +739,65 @@ class FrameworkLoader:
658
739
  if "agent_memories" in content:
659
740
  self._memories_cache["agent_memories"] = content["agent_memories"]
660
741
  self._memories_cache_time = current_time
661
-
742
+
662
743
  # Log detailed summary
663
744
  if loaded_count > 0 or skipped_count > 0:
664
745
  # Count unique agents with memories
665
746
  agent_count = len(agent_memories_dict) if agent_memories_dict else 0
666
747
  pm_loaded = bool(content.get("actual_memories"))
667
-
748
+
668
749
  summary_parts = []
669
750
  if pm_loaded:
670
751
  summary_parts.append("PM memory loaded")
671
752
  if agent_count > 0:
672
753
  summary_parts.append(f"{agent_count} agent memories loaded")
673
754
  if skipped_count > 0:
674
- summary_parts.append(f"{skipped_count} non-deployed agent memories skipped")
675
-
755
+ summary_parts.append(
756
+ f"{skipped_count} non-deployed agent memories skipped"
757
+ )
758
+
676
759
  self.logger.info(f"Memory loading complete: {' | '.join(summary_parts)}")
677
-
760
+
678
761
  # Log deployed agents for reference
679
762
  if len(deployed_agents) > 0:
680
- self.logger.debug(f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}")
681
-
763
+ self.logger.debug(
764
+ f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}"
765
+ )
766
+
682
767
  def _load_memories_from_directory(
683
768
  self,
684
769
  memories_dir: Path,
685
770
  deployed_agents: set,
686
771
  pm_memories: list,
687
772
  agent_memories_dict: dict,
688
- source: str
773
+ source: str,
689
774
  ) -> tuple[int, int]:
690
775
  """
691
776
  Load memories from a specific directory.
692
-
777
+
693
778
  Args:
694
779
  memories_dir: Directory to load memories from
695
780
  deployed_agents: Set of deployed agent names
696
781
  pm_memories: List to append PM memories to
697
782
  agent_memories_dict: Dict to store agent memories
698
783
  source: Source label ("user" or "project")
699
-
784
+
700
785
  Returns:
701
786
  Tuple of (loaded_count, skipped_count)
702
787
  """
703
788
  loaded_count = 0
704
789
  skipped_count = 0
705
-
790
+
706
791
  # Load PM memories (always loaded)
707
792
  # Support migration from both old formats
708
793
  pm_memory_path = memories_dir / "PM_memories.md"
709
794
  old_pm_path = memories_dir / "PM.md"
710
-
795
+
711
796
  # Migrate from old PM.md if needed
712
797
  if not pm_memory_path.exists() and old_pm_path.exists():
713
798
  try:
714
799
  old_pm_path.rename(pm_memory_path)
715
- self.logger.info(f"Migrated PM.md to PM_memories.md")
800
+ self.logger.info("Migrated PM.md to PM_memories.md")
716
801
  except Exception as e:
717
802
  self.logger.error(f"Failed to migrate PM.md: {e}")
718
803
  pm_memory_path = old_pm_path # Fall back to old path
@@ -721,22 +806,29 @@ class FrameworkLoader:
721
806
  pm_memory_path, f"PM memory ({source})"
722
807
  )
723
808
  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)")
809
+ pm_memories.append(
810
+ {
811
+ "source": source,
812
+ "content": loaded_content,
813
+ "path": pm_memory_path,
814
+ }
815
+ )
816
+ memory_size = len(loaded_content.encode("utf-8"))
817
+ self.logger.info(
818
+ f"Loaded {source} PM memory: {pm_memory_path} ({memory_size:,} bytes)"
819
+ )
731
820
  loaded_count += 1
732
-
821
+
733
822
  # First, migrate any old format memory files to new format
734
823
  # This handles backward compatibility for existing installations
735
824
  for old_file in memories_dir.glob("*.md"):
736
825
  # 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"]:
826
+ if old_file.name.endswith("_memories.md") or old_file.name in [
827
+ "PM.md",
828
+ "README.md",
829
+ ]:
738
830
  continue
739
-
831
+
740
832
  # Determine new name based on old format
741
833
  if old_file.stem.endswith("_agent"):
742
834
  # Old format: {agent_name}_agent.md -> {agent_name}_memories.md
@@ -750,17 +842,17 @@ class FrameworkLoader:
750
842
  new_path = memories_dir / f"{agent_name}_memories.md"
751
843
  if not new_path.exists():
752
844
  self._migrate_memory_file(old_file, new_path)
753
-
845
+
754
846
  # Load agent memories (only for deployed agents)
755
847
  # Only process *_memories.md files to avoid README.md and other docs
756
848
  for memory_file in memories_dir.glob("*_memories.md"):
757
849
  # Skip PM_memories.md as we already handled it
758
850
  if memory_file.name == "PM_memories.md":
759
851
  continue
760
-
852
+
761
853
  # Extract agent name from file (remove "_memories" suffix)
762
854
  agent_name = memory_file.stem[:-9] # Remove "_memories" suffix
763
-
855
+
764
856
  # Check if agent is deployed
765
857
  if agent_name in deployed_agents:
766
858
  loaded_content = self._try_load_file(
@@ -770,112 +862,127 @@ class FrameworkLoader:
770
862
  # Store or merge agent memories
771
863
  if agent_name not in agent_memories_dict:
772
864
  agent_memories_dict[agent_name] = []
773
-
865
+
774
866
  # If it's a list, append the new memory entry
775
867
  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)")
868
+ agent_memories_dict[agent_name].append(
869
+ {
870
+ "source": source,
871
+ "content": loaded_content,
872
+ "path": memory_file,
873
+ }
874
+ )
875
+
876
+ memory_size = len(loaded_content.encode("utf-8"))
877
+ self.logger.info(
878
+ f"Loaded {source} memory for {agent_name}: {memory_file.name} ({memory_size:,} bytes)"
879
+ )
784
880
  loaded_count += 1
785
881
  else:
786
882
  # 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)")
883
+ self.logger.info(
884
+ f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed)"
885
+ )
788
886
  # 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:
887
+ if (
888
+ agent_name.replace("_", "-") in deployed_agents
889
+ or agent_name.replace("-", "_") in deployed_agents
890
+ ):
790
891
  # Detect naming mismatches
791
- alt_name = agent_name.replace('_', '-') if '_' in agent_name else agent_name.replace('-', '_')
892
+ alt_name = (
893
+ agent_name.replace("_", "-")
894
+ if "_" in agent_name
895
+ else agent_name.replace("-", "_")
896
+ )
792
897
  if alt_name in deployed_agents:
793
898
  self.logger.warning(
794
899
  f"Naming mismatch detected: Memory file uses '{agent_name}' but deployed agent is '{alt_name}'. "
795
900
  f"Consider renaming {memory_file.name} to {alt_name}_memories.md"
796
901
  )
797
902
  skipped_count += 1
798
-
903
+
799
904
  # After loading all memories for this directory, aggregate agent memories
800
905
  for agent_name in list(agent_memories_dict.keys()):
801
- if isinstance(agent_memories_dict[agent_name], list) and agent_memories_dict[agent_name]:
906
+ if (
907
+ isinstance(agent_memories_dict[agent_name], list)
908
+ and agent_memories_dict[agent_name]
909
+ ):
802
910
  # Aggregate memories for this agent
803
911
  aggregated = self._aggregate_memories(agent_memories_dict[agent_name])
804
912
  agent_memories_dict[agent_name] = aggregated
805
-
913
+
806
914
  return loaded_count, skipped_count
807
-
915
+
808
916
  def _aggregate_memories(self, memory_entries: list) -> str:
809
917
  """
810
918
  Aggregate multiple memory entries into a single memory string.
811
-
919
+
812
920
  Strategy:
813
921
  - Simplified to support list-based memories only
814
922
  - Preserve all unique bullet-point items (lines starting with -)
815
923
  - Remove exact duplicates
816
924
  - Project-level memories take precedence over user-level
817
-
925
+
818
926
  Args:
819
927
  memory_entries: List of memory entries with source, content, and path
820
-
928
+
821
929
  Returns:
822
930
  Aggregated memory content as a string
823
931
  """
824
932
  if not memory_entries:
825
933
  return ""
826
-
934
+
827
935
  # If only one entry, return it as-is
828
936
  if len(memory_entries) == 1:
829
937
  return memory_entries[0]["content"]
830
-
938
+
831
939
  # Parse all memories into a simple list
832
940
  all_items = {} # Dict to track items and their source
833
941
  metadata_lines = []
834
942
  agent_id = None
835
-
943
+
836
944
  for entry in memory_entries:
837
945
  content = entry["content"]
838
946
  source = entry["source"]
839
-
840
- for line in content.split('\n'):
947
+
948
+ for line in content.split("\n"):
841
949
  # Check for header to extract agent_id
842
- if line.startswith('# Agent Memory:'):
843
- agent_id = line.replace('# Agent Memory:', '').strip()
950
+ if line.startswith("# Agent Memory:"):
951
+ agent_id = line.replace("# Agent Memory:", "").strip()
844
952
  # Check for metadata lines
845
- elif line.startswith('<!-- ') and line.endswith(' -->'):
953
+ elif line.startswith("<!-- ") and line.endswith(" -->"):
846
954
  # Only keep metadata from project source or if not already present
847
955
  if source == "project" or line not in metadata_lines:
848
956
  metadata_lines.append(line)
849
957
  # Check for list items
850
- elif line.strip().startswith('-'):
958
+ elif line.strip().startswith("-"):
851
959
  # Normalize the item for comparison
852
960
  item_text = line.strip()
853
- normalized = item_text.lstrip('- ').strip().lower()
854
-
961
+ normalized = item_text.lstrip("- ").strip().lower()
962
+
855
963
  # Add item if new or if project source overrides user source
856
964
  if normalized not in all_items or source == "project":
857
965
  all_items[normalized] = (item_text, source)
858
-
966
+
859
967
  # Build aggregated content as simple list
860
968
  lines = []
861
-
969
+
862
970
  # Add header
863
971
  if agent_id:
864
972
  lines.append(f"# Agent Memory: {agent_id}")
865
973
  else:
866
974
  lines.append("# Agent Memory")
867
-
975
+
868
976
  # Add latest timestamp from metadata
869
- from datetime import datetime
870
977
  lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
871
978
  lines.append("")
872
-
979
+
873
980
  # Add all unique items (sorted for consistency)
874
981
  for normalized_key in sorted(all_items.keys()):
875
982
  item_text, source = all_items[normalized_key]
876
983
  lines.append(item_text)
877
-
878
- return '\n'.join(lines)
984
+
985
+ return "\n".join(lines)
879
986
 
880
987
  def _load_single_agent(
881
988
  self, agent_file: Path
@@ -972,7 +1079,7 @@ class FrameworkLoader:
972
1079
 
973
1080
  if not self.framework_path:
974
1081
  return content
975
-
1082
+
976
1083
  # Check if this is a packaged installation
977
1084
  if self.framework_path == Path("__PACKAGED__"):
978
1085
  # Load files using importlib.resources for packaged installations
@@ -981,7 +1088,11 @@ class FrameworkLoader:
981
1088
  # Load from filesystem for development mode
982
1089
  # Load framework's INSTRUCTIONS.md
983
1090
  framework_instructions_path = (
984
- self.framework_path / "src" / "claude_mpm" / "agents" / "INSTRUCTIONS.md"
1091
+ self.framework_path
1092
+ / "src"
1093
+ / "claude_mpm"
1094
+ / "agents"
1095
+ / "INSTRUCTIONS.md"
985
1096
  )
986
1097
  if framework_instructions_path.exists():
987
1098
  loaded_content = self._try_load_file(
@@ -993,12 +1104,14 @@ class FrameworkLoader:
993
1104
  # Add framework version to content
994
1105
  if self.framework_version:
995
1106
  content["instructions_version"] = self.framework_version
996
- content[
997
- "version"
998
- ] = self.framework_version # Update main version key
1107
+ content["version"] = (
1108
+ self.framework_version
1109
+ ) # Update main version key
999
1110
  # Add modification timestamp to content
1000
1111
  if self.framework_last_modified:
1001
- content["instructions_last_modified"] = self.framework_last_modified
1112
+ content["instructions_last_modified"] = (
1113
+ self.framework_last_modified
1114
+ )
1002
1115
 
1003
1116
  # Load BASE_PM.md for core framework requirements
1004
1117
  base_pm_path = (
@@ -1016,7 +1129,7 @@ class FrameworkLoader:
1016
1129
 
1017
1130
  # Load MEMORY.md - check for project-specific first, then system
1018
1131
  self._load_memory_instructions(content)
1019
-
1132
+
1020
1133
  # Load actual memories from .claude-mpm/memories/PM_memories.md
1021
1134
  self._load_actual_memories(content)
1022
1135
 
@@ -1027,22 +1140,27 @@ class FrameworkLoader:
1027
1140
  self._load_agents_directory(content, agents_dir, templates_dir, main_dir)
1028
1141
 
1029
1142
  return content
1030
-
1143
+
1031
1144
  def _load_packaged_framework_content(self, content: Dict[str, Any]) -> None:
1032
1145
  """Load framework content from packaged installation using importlib.resources."""
1033
1146
  if not files:
1034
- self.logger.warning("importlib.resources not available, cannot load packaged framework")
1147
+ self.logger.warning(
1148
+ "importlib.resources not available, cannot load packaged framework"
1149
+ )
1035
1150
  self.logger.debug(f"files variable is: {files}")
1036
1151
  # Try alternative import methods
1037
1152
  try:
1038
1153
  from importlib import resources
1154
+
1039
1155
  self.logger.info("Using importlib.resources as fallback")
1040
1156
  self._load_packaged_framework_content_fallback(content, resources)
1041
1157
  return
1042
1158
  except ImportError:
1043
- self.logger.error("No importlib.resources available, using minimal framework")
1159
+ self.logger.error(
1160
+ "No importlib.resources available, using minimal framework"
1161
+ )
1044
1162
  return
1045
-
1163
+
1046
1164
  try:
1047
1165
  # Load INSTRUCTIONS.md
1048
1166
  instructions_content = self._load_packaged_file("INSTRUCTIONS.md")
@@ -1050,43 +1168,51 @@ class FrameworkLoader:
1050
1168
  content["framework_instructions"] = instructions_content
1051
1169
  content["loaded"] = True
1052
1170
  # Extract and store version/timestamp metadata
1053
- self._extract_metadata_from_content(instructions_content, "INSTRUCTIONS.md")
1171
+ self._extract_metadata_from_content(
1172
+ instructions_content, "INSTRUCTIONS.md"
1173
+ )
1054
1174
  if self.framework_version:
1055
1175
  content["instructions_version"] = self.framework_version
1056
1176
  content["version"] = self.framework_version
1057
1177
  if self.framework_last_modified:
1058
1178
  content["instructions_last_modified"] = self.framework_last_modified
1059
-
1179
+
1060
1180
  # Load BASE_PM.md
1061
1181
  base_pm_content = self._load_packaged_file("BASE_PM.md")
1062
1182
  if base_pm_content:
1063
1183
  content["base_pm_instructions"] = base_pm_content
1064
-
1184
+
1065
1185
  # Load WORKFLOW.md
1066
1186
  workflow_content = self._load_packaged_file("WORKFLOW.md")
1067
1187
  if workflow_content:
1068
1188
  content["workflow_instructions"] = workflow_content
1069
1189
  content["project_workflow"] = "system"
1070
-
1190
+
1071
1191
  # Load MEMORY.md
1072
1192
  memory_content = self._load_packaged_file("MEMORY.md")
1073
1193
  if memory_content:
1074
1194
  content["memory_instructions"] = memory_content
1075
1195
  content["project_memory"] = "system"
1076
-
1196
+
1077
1197
  except Exception as e:
1078
1198
  self.logger.error(f"Failed to load packaged framework content: {e}")
1079
1199
 
1080
- def _load_packaged_framework_content_fallback(self, content: Dict[str, Any], resources) -> None:
1200
+ def _load_packaged_framework_content_fallback(
1201
+ self, content: Dict[str, Any], resources
1202
+ ) -> None:
1081
1203
  """Load framework content using importlib.resources fallback."""
1082
1204
  try:
1083
1205
  # Load INSTRUCTIONS.md
1084
- instructions_content = self._load_packaged_file_fallback("INSTRUCTIONS.md", resources)
1206
+ instructions_content = self._load_packaged_file_fallback(
1207
+ "INSTRUCTIONS.md", resources
1208
+ )
1085
1209
  if instructions_content:
1086
1210
  content["framework_instructions"] = instructions_content
1087
1211
  content["loaded"] = True
1088
1212
  # Extract and store version/timestamp metadata
1089
- self._extract_metadata_from_content(instructions_content, "INSTRUCTIONS.md")
1213
+ self._extract_metadata_from_content(
1214
+ instructions_content, "INSTRUCTIONS.md"
1215
+ )
1090
1216
  if self.framework_version:
1091
1217
  content["instructions_version"] = self.framework_version
1092
1218
  content["version"] = self.framework_version
@@ -1099,7 +1225,9 @@ class FrameworkLoader:
1099
1225
  content["base_pm_instructions"] = base_pm_content
1100
1226
 
1101
1227
  # Load WORKFLOW.md
1102
- workflow_content = self._load_packaged_file_fallback("WORKFLOW.md", resources)
1228
+ workflow_content = self._load_packaged_file_fallback(
1229
+ "WORKFLOW.md", resources
1230
+ )
1103
1231
  if workflow_content:
1104
1232
  content["workflow_instructions"] = workflow_content
1105
1233
  content["project_workflow"] = "system"
@@ -1111,7 +1239,9 @@ class FrameworkLoader:
1111
1239
  content["project_memory"] = "system"
1112
1240
 
1113
1241
  except Exception as e:
1114
- self.logger.error(f"Failed to load packaged framework content with fallback: {e}")
1242
+ self.logger.error(
1243
+ f"Failed to load packaged framework content with fallback: {e}"
1244
+ )
1115
1245
 
1116
1246
  def _load_packaged_file_fallback(self, filename: str, resources) -> Optional[str]:
1117
1247
  """Load a file from the packaged installation using importlib.resources fallback."""
@@ -1119,52 +1249,52 @@ class FrameworkLoader:
1119
1249
  # Try different resource loading methods
1120
1250
  try:
1121
1251
  # Method 1: resources.read_text (Python 3.9+)
1122
- content = resources.read_text('claude_mpm.agents', filename)
1252
+ content = resources.read_text("claude_mpm.agents", filename)
1123
1253
  self.logger.info(f"Loaded {filename} from package using read_text")
1124
1254
  return content
1125
1255
  except AttributeError:
1126
1256
  # Method 2: resources.files (Python 3.9+)
1127
- agents_files = resources.files('claude_mpm.agents')
1257
+ agents_files = resources.files("claude_mpm.agents")
1128
1258
  file_path = agents_files / filename
1129
1259
  if file_path.is_file():
1130
1260
  content = file_path.read_text()
1131
1261
  self.logger.info(f"Loaded {filename} from package using files")
1132
1262
  return content
1133
- else:
1134
- self.logger.warning(f"File {filename} not found in package")
1135
- return None
1263
+ self.logger.warning(f"File {filename} not found in package")
1264
+ return None
1136
1265
  except Exception as e:
1137
- self.logger.error(f"Failed to load {filename} from package with fallback: {e}")
1266
+ self.logger.error(
1267
+ f"Failed to load {filename} from package with fallback: {e}"
1268
+ )
1138
1269
  return None
1139
1270
 
1140
1271
  def _load_packaged_file(self, filename: str) -> Optional[str]:
1141
1272
  """Load a file from the packaged installation."""
1142
1273
  try:
1143
1274
  # Use importlib.resources to load file from package
1144
- agents_package = files('claude_mpm.agents')
1275
+ agents_package = files("claude_mpm.agents")
1145
1276
  file_path = agents_package / filename
1146
-
1277
+
1147
1278
  if file_path.is_file():
1148
1279
  content = file_path.read_text()
1149
1280
  self.logger.info(f"Loaded {filename} from package")
1150
1281
  return content
1151
- else:
1152
- self.logger.warning(f"File {filename} not found in package")
1153
- return None
1282
+ self.logger.warning(f"File {filename} not found in package")
1283
+ return None
1154
1284
  except Exception as e:
1155
1285
  self.logger.error(f"Failed to load {filename} from package: {e}")
1156
1286
  return None
1157
-
1287
+
1158
1288
  def _extract_metadata_from_content(self, content: str, filename: str) -> None:
1159
1289
  """Extract metadata from content string."""
1160
1290
  import re
1161
-
1291
+
1162
1292
  # Extract version
1163
1293
  version_match = re.search(r"<!-- FRAMEWORK_VERSION: (\d+) -->", content)
1164
1294
  if version_match and "INSTRUCTIONS.md" in filename:
1165
1295
  self.framework_version = version_match.group(1)
1166
1296
  self.logger.info(f"Framework version: {self.framework_version}")
1167
-
1297
+
1168
1298
  # Extract timestamp
1169
1299
  timestamp_match = re.search(r"<!-- LAST_MODIFIED: ([^>]+) -->", content)
1170
1300
  if timestamp_match and "INSTRUCTIONS.md" in filename:
@@ -1178,12 +1308,58 @@ class FrameworkLoader:
1178
1308
  Returns:
1179
1309
  Complete framework instructions ready for injection
1180
1310
  """
1311
+ # Import LogManager for prompt logging
1312
+ try:
1313
+ from .log_manager import get_log_manager
1314
+
1315
+ log_manager = get_log_manager()
1316
+ except ImportError:
1317
+ log_manager = None
1318
+
1319
+ # Generate the instructions
1181
1320
  if self.framework_content["loaded"]:
1182
1321
  # Build framework from components
1183
- return self._format_full_framework()
1322
+ instructions = self._format_full_framework()
1184
1323
  else:
1185
1324
  # Use minimal fallback
1186
- return self._format_minimal_framework()
1325
+ instructions = self._format_minimal_framework()
1326
+
1327
+ # Log the system prompt if LogManager is available
1328
+ if log_manager:
1329
+ try:
1330
+ import asyncio
1331
+ import os
1332
+
1333
+ # Get or create event loop
1334
+ try:
1335
+ loop = asyncio.get_running_loop()
1336
+ except RuntimeError:
1337
+ loop = asyncio.new_event_loop()
1338
+ asyncio.set_event_loop(loop)
1339
+
1340
+ # Prepare metadata
1341
+ metadata = {
1342
+ "framework_version": self.framework_version,
1343
+ "framework_loaded": self.framework_content.get("loaded", False),
1344
+ "session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
1345
+ "instructions_length": len(instructions),
1346
+ }
1347
+
1348
+ # Log the prompt asynchronously
1349
+ if loop.is_running():
1350
+ asyncio.create_task(
1351
+ log_manager.log_prompt("system_prompt", instructions, metadata)
1352
+ )
1353
+ else:
1354
+ loop.run_until_complete(
1355
+ log_manager.log_prompt("system_prompt", instructions, metadata)
1356
+ )
1357
+
1358
+ self.logger.debug("System prompt logged to prompts directory")
1359
+ except Exception as e:
1360
+ self.logger.debug(f"Could not log system prompt: {e}")
1361
+
1362
+ return instructions
1187
1363
 
1188
1364
  def _strip_metadata_comments(self, content: str) -> str:
1189
1365
  """Strip metadata HTML comments from content.
@@ -1201,12 +1377,10 @@ class FrameworkLoader:
1201
1377
  content,
1202
1378
  )
1203
1379
  # Also remove any leading blank lines that might result
1204
- cleaned = cleaned.lstrip("\n")
1205
- return cleaned
1380
+ return cleaned.lstrip("\n")
1206
1381
 
1207
1382
  def _format_full_framework(self) -> str:
1208
1383
  """Format full framework instructions."""
1209
- from datetime import datetime
1210
1384
 
1211
1385
  # Initialize output style manager on first use (ensures content is loaded)
1212
1386
  if self.output_style_manager is None:
@@ -1217,7 +1391,9 @@ class FrameworkLoader:
1217
1391
  if self.output_style_manager:
1218
1392
  inject_output_style = self.output_style_manager.should_inject_content()
1219
1393
  if inject_output_style:
1220
- self.logger.info("Injecting output style content into instructions for Claude < 1.0.83")
1394
+ self.logger.info(
1395
+ "Injecting output style content into instructions for Claude < 1.0.83"
1396
+ )
1221
1397
 
1222
1398
  # If we have the full framework INSTRUCTIONS.md, use it
1223
1399
  if self.framework_content.get("framework_instructions"):
@@ -1227,10 +1403,12 @@ class FrameworkLoader:
1227
1403
 
1228
1404
  # Note: We don't add working directory CLAUDE.md here since Claude Code
1229
1405
  # already picks it up automatically. This prevents duplication.
1230
-
1406
+
1231
1407
  # Add custom INSTRUCTIONS.md if present (overrides or extends framework instructions)
1232
1408
  if self.framework_content.get("custom_instructions"):
1233
- level = self.framework_content.get("custom_instructions_level", "unknown")
1409
+ level = self.framework_content.get(
1410
+ "custom_instructions_level", "unknown"
1411
+ )
1234
1412
  instructions += f"\n\n## Custom PM Instructions ({level} level)\n\n"
1235
1413
  instructions += "**The following custom instructions override or extend the framework defaults:**\n\n"
1236
1414
  instructions += self._strip_metadata_comments(
@@ -1243,7 +1421,9 @@ class FrameworkLoader:
1243
1421
  workflow_content = self._strip_metadata_comments(
1244
1422
  self.framework_content["workflow_instructions"]
1245
1423
  )
1246
- level = self.framework_content.get("workflow_instructions_level", "system")
1424
+ level = self.framework_content.get(
1425
+ "workflow_instructions_level", "system"
1426
+ )
1247
1427
  if level != "system":
1248
1428
  instructions += f"\n\n## Workflow Instructions ({level} level)\n\n"
1249
1429
  instructions += "**The following workflow instructions override system defaults:**\n\n"
@@ -1254,26 +1434,28 @@ class FrameworkLoader:
1254
1434
  memory_content = self._strip_metadata_comments(
1255
1435
  self.framework_content["memory_instructions"]
1256
1436
  )
1257
- level = self.framework_content.get("memory_instructions_level", "system")
1437
+ level = self.framework_content.get(
1438
+ "memory_instructions_level", "system"
1439
+ )
1258
1440
  if level != "system":
1259
1441
  instructions += f"\n\n## Memory Instructions ({level} level)\n\n"
1260
1442
  instructions += "**The following memory instructions override system defaults:**\n\n"
1261
1443
  instructions += f"{memory_content}\n"
1262
-
1444
+
1263
1445
  # Add actual PM memories after memory instructions
1264
1446
  if self.framework_content.get("actual_memories"):
1265
1447
  instructions += "\n\n## Current PM Memories\n\n"
1266
1448
  instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
1267
1449
  instructions += self.framework_content["actual_memories"]
1268
1450
  instructions += "\n"
1269
-
1451
+
1270
1452
  # Add agent memories if available
1271
1453
  if self.framework_content.get("agent_memories"):
1272
1454
  agent_memories = self.framework_content["agent_memories"]
1273
1455
  if agent_memories:
1274
1456
  instructions += "\n\n## Agent Memories\n\n"
1275
1457
  instructions += "**The following are accumulated memories from specialized agents:**\n\n"
1276
-
1458
+
1277
1459
  for agent_name in sorted(agent_memories.keys()):
1278
1460
  memory_content = agent_memories[agent_name]
1279
1461
  if memory_content:
@@ -1296,10 +1478,12 @@ class FrameworkLoader:
1296
1478
  self.framework_content["base_pm_instructions"]
1297
1479
  )
1298
1480
  instructions += f"\n\n{base_pm}"
1299
-
1481
+
1300
1482
  # Inject output style content if needed (for Claude < 1.0.83)
1301
1483
  if inject_output_style and self.output_style_manager:
1302
- output_style_content = self.output_style_manager.get_injectable_content(framework_loader=self)
1484
+ output_style_content = self.output_style_manager.get_injectable_content(
1485
+ framework_loader=self
1486
+ )
1303
1487
  if output_style_content:
1304
1488
  instructions += "\n\n## Output Style Configuration\n"
1305
1489
  instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
@@ -1307,9 +1491,7 @@ class FrameworkLoader:
1307
1491
  instructions += "\n"
1308
1492
 
1309
1493
  # Clean up any trailing whitespace
1310
- instructions = instructions.rstrip() + "\n"
1311
-
1312
- return instructions
1494
+ return instructions.rstrip() + "\n"
1313
1495
 
1314
1496
  # Otherwise fall back to generating framework
1315
1497
  instructions = """# Claude MPM Framework Instructions
@@ -1432,23 +1614,26 @@ Extract tickets from these patterns:
1432
1614
  def _generate_agent_capabilities_section(self) -> str:
1433
1615
  """Generate dynamic agent capabilities section from deployed agents.
1434
1616
  Uses caching to avoid repeated file I/O and parsing operations."""
1435
-
1617
+
1436
1618
  # Check if cache is valid
1437
1619
  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):
1620
+ if (
1621
+ self._agent_capabilities_cache is not None
1622
+ and current_time - self._agent_capabilities_cache_time
1623
+ < self.CAPABILITIES_CACHE_TTL
1624
+ ):
1440
1625
  cache_age = current_time - self._agent_capabilities_cache_time
1441
- self.logger.debug(f"Using cached agent capabilities (age: {cache_age:.1f}s)")
1626
+ self.logger.debug(
1627
+ f"Using cached agent capabilities (age: {cache_age:.1f}s)"
1628
+ )
1442
1629
  return self._agent_capabilities_cache
1443
-
1630
+
1444
1631
  # Cache miss or expired - generate capabilities
1445
1632
  self.logger.debug("Generating agent capabilities (cache miss or expired)")
1446
-
1633
+
1447
1634
  try:
1448
1635
  from pathlib import Path
1449
1636
 
1450
- import yaml
1451
-
1452
1637
  # Read directly from deployed agents in .claude/agents/
1453
1638
  # Check multiple locations for deployed agents
1454
1639
  # Priority order: project > user home > fallback
@@ -1456,29 +1641,34 @@ Extract tickets from these patterns:
1456
1641
  Path.cwd() / ".claude" / "agents", # Project-specific agents
1457
1642
  Path.home() / ".claude" / "agents", # User's system agents
1458
1643
  ]
1459
-
1644
+
1460
1645
  # Collect agents from all directories with proper precedence
1461
1646
  # Project agents override user agents with the same name
1462
1647
  all_agents = {} # key: agent_id, value: (agent_data, priority)
1463
-
1648
+
1464
1649
  for priority, potential_dir in enumerate(agents_dirs):
1465
1650
  if potential_dir.exists() and any(potential_dir.glob("*.md")):
1466
1651
  self.logger.debug(f"Found agents directory at: {potential_dir}")
1467
-
1652
+
1468
1653
  # Collect agents from this directory
1469
1654
  for agent_file in potential_dir.glob("*.md"):
1470
1655
  if agent_file.name.startswith("."):
1471
1656
  continue
1472
-
1657
+
1473
1658
  # Parse agent metadata (with caching)
1474
1659
  agent_data = self._parse_agent_metadata(agent_file)
1475
1660
  if agent_data:
1476
1661
  agent_id = agent_data["id"]
1477
1662
  # Only add if not already present (project has priority 0, user has priority 1)
1478
1663
  # Lower priority number wins (project > user)
1479
- if agent_id not in all_agents or priority < all_agents[agent_id][1]:
1664
+ if (
1665
+ agent_id not in all_agents
1666
+ or priority < all_agents[agent_id][1]
1667
+ ):
1480
1668
  all_agents[agent_id] = (agent_data, priority)
1481
- self.logger.debug(f"Added/Updated agent {agent_id} from {potential_dir} (priority {priority})")
1669
+ self.logger.debug(
1670
+ f"Added/Updated agent {agent_id} from {potential_dir} (priority {priority})"
1671
+ )
1482
1672
 
1483
1673
  if not all_agents:
1484
1674
  self.logger.warning(f"No agents found in any location: {agents_dirs}")
@@ -1487,16 +1677,20 @@ Extract tickets from these patterns:
1487
1677
  self._agent_capabilities_cache = result
1488
1678
  self._agent_capabilities_cache_time = current_time
1489
1679
  return result
1490
-
1680
+
1491
1681
  # Log agent collection summary
1492
1682
  project_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 0]
1493
1683
  user_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 1]
1494
-
1684
+
1495
1685
  if project_agents:
1496
- self.logger.info(f"Loaded {len(project_agents)} project agents: {', '.join(sorted(project_agents))}")
1686
+ self.logger.info(
1687
+ f"Loaded {len(project_agents)} project agents: {', '.join(sorted(project_agents))}"
1688
+ )
1497
1689
  if user_agents:
1498
- self.logger.info(f"Loaded {len(user_agents)} user agents: {', '.join(sorted(user_agents))}")
1499
-
1690
+ self.logger.info(
1691
+ f"Loaded {len(user_agents)} user agents: {', '.join(sorted(user_agents))}"
1692
+ )
1693
+
1500
1694
  # Build capabilities section
1501
1695
  section = "\n\n## Available Agent Capabilities\n\n"
1502
1696
 
@@ -1528,6 +1722,33 @@ Extract tickets from these patterns:
1528
1722
  section += f"\n### {display_name} (`{agent['id']}`)\n"
1529
1723
  section += f"{agent['description']}\n"
1530
1724
 
1725
+ # Add routing information if available
1726
+ if agent.get("routing"):
1727
+ routing = agent["routing"]
1728
+
1729
+ # Format routing hints for PM usage
1730
+ routing_hints = []
1731
+
1732
+ if routing.get("keywords"):
1733
+ # Show first 5 keywords for brevity
1734
+ keywords = routing["keywords"][:5]
1735
+ routing_hints.append(f"Keywords: {', '.join(keywords)}")
1736
+
1737
+ if routing.get("paths"):
1738
+ # Show first 3 paths for brevity
1739
+ paths = routing["paths"][:3]
1740
+ routing_hints.append(f"Paths: {', '.join(paths)}")
1741
+
1742
+ if routing.get("priority"):
1743
+ routing_hints.append(f"Priority: {routing['priority']}")
1744
+
1745
+ if routing_hints:
1746
+ section += f"- **Routing**: {' | '.join(routing_hints)}\n"
1747
+
1748
+ # Add when_to_use if present
1749
+ if routing.get("when_to_use"):
1750
+ section += f"- **When to use**: {routing['when_to_use']}\n"
1751
+
1531
1752
  # Add any additional metadata if present
1532
1753
  if agent.get("authority"):
1533
1754
  section += f"- **Authority**: {agent['authority']}\n"
@@ -1558,8 +1779,10 @@ Extract tickets from these patterns:
1558
1779
  # Cache the generated capabilities
1559
1780
  self._agent_capabilities_cache = section
1560
1781
  self._agent_capabilities_cache_time = current_time
1561
- self.logger.debug(f"Cached agent capabilities section ({len(section)} chars)")
1562
-
1782
+ self.logger.debug(
1783
+ f"Cached agent capabilities section ({len(section)} chars)"
1784
+ )
1785
+
1563
1786
  return section
1564
1787
 
1565
1788
  except Exception as e:
@@ -1582,22 +1805,26 @@ Extract tickets from these patterns:
1582
1805
  cache_key = str(agent_file)
1583
1806
  file_mtime = agent_file.stat().st_mtime
1584
1807
  current_time = time.time()
1585
-
1808
+
1586
1809
  # Check if we have cached data for this file
1587
1810
  if cache_key in self._agent_metadata_cache:
1588
1811
  cached_data, cached_mtime = self._agent_metadata_cache[cache_key]
1589
1812
  # 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):
1813
+ if (
1814
+ cached_mtime == file_mtime
1815
+ and current_time - cached_mtime < self.METADATA_CACHE_TTL
1816
+ ):
1592
1817
  self.logger.debug(f"Using cached metadata for {agent_file.name}")
1593
1818
  return cached_data
1594
-
1819
+
1595
1820
  # Cache miss or expired - parse the file
1596
- self.logger.debug(f"Parsing metadata for {agent_file.name} (cache miss or expired)")
1597
-
1821
+ self.logger.debug(
1822
+ f"Parsing metadata for {agent_file.name} (cache miss or expired)"
1823
+ )
1824
+
1598
1825
  import yaml
1599
1826
 
1600
- with open(agent_file, "r") as f:
1827
+ with open(agent_file) as f:
1601
1828
  content = f.read()
1602
1829
 
1603
1830
  # Default values
@@ -1632,15 +1859,89 @@ Extract tickets from these patterns:
1632
1859
  # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1633
1860
  # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1634
1861
 
1862
+ # Try to load routing metadata from JSON template if not in YAML frontmatter
1863
+ if "routing" not in agent_data:
1864
+ routing_data = self._load_routing_from_template(agent_file.stem)
1865
+ if routing_data:
1866
+ agent_data["routing"] = routing_data
1867
+
1635
1868
  # Cache the parsed metadata
1636
1869
  self._agent_metadata_cache[cache_key] = (agent_data, file_mtime)
1637
-
1870
+
1638
1871
  return agent_data
1639
1872
 
1640
1873
  except Exception as e:
1641
1874
  self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
1642
1875
  return None
1643
1876
 
1877
+ def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
1878
+ """Load routing metadata from agent JSON template.
1879
+
1880
+ Args:
1881
+ agent_name: Name of the agent (stem of the file)
1882
+
1883
+ Returns:
1884
+ Dictionary with routing metadata or None if not found
1885
+ """
1886
+ try:
1887
+ import json
1888
+
1889
+ # Check if we have a framework path
1890
+ if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
1891
+ # For packaged installations, try to load from package resources
1892
+ if files:
1893
+ try:
1894
+ templates_package = files("claude_mpm.agents.templates")
1895
+ template_file = templates_package / f"{agent_name}.json"
1896
+
1897
+ if template_file.is_file():
1898
+ template_content = template_file.read_text()
1899
+ template_data = json.loads(template_content)
1900
+ return template_data.get("routing")
1901
+ except Exception as e:
1902
+ self.logger.debug(
1903
+ f"Could not load routing from packaged template for {agent_name}: {e}"
1904
+ )
1905
+ return None
1906
+
1907
+ # For development mode, load from filesystem
1908
+ templates_dir = (
1909
+ self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
1910
+ )
1911
+ template_file = templates_dir / f"{agent_name}.json"
1912
+
1913
+ if template_file.exists():
1914
+ with open(template_file) as f:
1915
+ template_data = json.load(f)
1916
+ return template_data.get("routing")
1917
+
1918
+ # Also check for variations in naming (underscore vs dash)
1919
+ # Handle common naming variations between deployed .md files and .json templates
1920
+ # Remove duplicates by using a set
1921
+ alternative_names = list(
1922
+ {
1923
+ agent_name.replace("-", "_"), # api-qa -> api_qa
1924
+ agent_name.replace("_", "-"), # api_qa -> api-qa
1925
+ agent_name.replace("-", ""), # api-qa -> apiqa
1926
+ agent_name.replace("_", ""), # api_qa -> apiqa
1927
+ }
1928
+ )
1929
+
1930
+ for alt_name in alternative_names:
1931
+ if alt_name != agent_name:
1932
+ alt_file = templates_dir / f"{alt_name}.json"
1933
+ if alt_file.exists():
1934
+ with open(alt_file) as f:
1935
+ template_data = json.load(f)
1936
+ return template_data.get("routing")
1937
+
1938
+ self.logger.debug(f"No JSON template found for agent: {agent_name}")
1939
+ return None
1940
+
1941
+ except Exception as e:
1942
+ self.logger.debug(f"Could not load routing metadata for {agent_name}: {e}")
1943
+ return None
1944
+
1644
1945
  def _generate_agent_selection_guide(self, deployed_agents: list) -> str:
1645
1946
  """Generate Context-Aware Agent Selection guide from deployed agents.
1646
1947
 
@@ -1660,55 +1961,55 @@ Extract tickets from these patterns:
1660
1961
  if "implementation" in desc_lower or (
1661
1962
  "engineer" in agent_id and "data" not in agent_id
1662
1963
  ):
1663
- selection_map[
1664
- "Implementation tasks"
1665
- ] = f"{agent['display_name']} (`{agent_id}`)"
1964
+ selection_map["Implementation tasks"] = (
1965
+ f"{agent['display_name']} (`{agent_id}`)"
1966
+ )
1666
1967
  if "codebase analysis" in desc_lower or "research" in agent_id:
1667
- selection_map[
1668
- "Codebase analysis"
1669
- ] = f"{agent['display_name']} (`{agent_id}`)"
1968
+ selection_map["Codebase analysis"] = (
1969
+ f"{agent['display_name']} (`{agent_id}`)"
1970
+ )
1670
1971
  if "testing" in desc_lower or "qa" in agent_id:
1671
- selection_map[
1672
- "Testing/quality"
1673
- ] = f"{agent['display_name']} (`{agent_id}`)"
1972
+ selection_map["Testing/quality"] = (
1973
+ f"{agent['display_name']} (`{agent_id}`)"
1974
+ )
1674
1975
  if "documentation" in desc_lower:
1675
- selection_map[
1676
- "Documentation"
1677
- ] = f"{agent['display_name']} (`{agent_id}`)"
1976
+ selection_map["Documentation"] = (
1977
+ f"{agent['display_name']} (`{agent_id}`)"
1978
+ )
1678
1979
  if "security" in desc_lower or "sast" in desc_lower:
1679
- selection_map[
1680
- "Security operations"
1681
- ] = f"{agent['display_name']} (`{agent_id}`)"
1980
+ selection_map["Security operations"] = (
1981
+ f"{agent['display_name']} (`{agent_id}`)"
1982
+ )
1682
1983
  if (
1683
1984
  "deployment" in desc_lower
1684
1985
  or "infrastructure" in desc_lower
1685
1986
  or "ops" in agent_id
1686
1987
  ):
1687
- selection_map[
1688
- "Deployment/infrastructure"
1689
- ] = f"{agent['display_name']} (`{agent_id}`)"
1988
+ selection_map["Deployment/infrastructure"] = (
1989
+ f"{agent['display_name']} (`{agent_id}`)"
1990
+ )
1690
1991
  if "data" in desc_lower and (
1691
1992
  "pipeline" in desc_lower or "etl" in desc_lower
1692
1993
  ):
1693
- selection_map[
1694
- "Data pipeline/ETL"
1695
- ] = f"{agent['display_name']} (`{agent_id}`)"
1994
+ selection_map["Data pipeline/ETL"] = (
1995
+ f"{agent['display_name']} (`{agent_id}`)"
1996
+ )
1696
1997
  if "git" in desc_lower or "version control" in desc_lower:
1697
- selection_map[
1698
- "Version control"
1699
- ] = f"{agent['display_name']} (`{agent_id}`)"
1998
+ selection_map["Version control"] = (
1999
+ f"{agent['display_name']} (`{agent_id}`)"
2000
+ )
1700
2001
  if "ticket" in desc_lower or "epic" in desc_lower:
1701
- selection_map[
1702
- "Ticket/issue management"
1703
- ] = f"{agent['display_name']} (`{agent_id}`)"
2002
+ selection_map["Ticket/issue management"] = (
2003
+ f"{agent['display_name']} (`{agent_id}`)"
2004
+ )
1704
2005
  if "browser" in desc_lower or "e2e" in desc_lower:
1705
- selection_map[
1706
- "Browser/E2E testing"
1707
- ] = f"{agent['display_name']} (`{agent_id}`)"
2006
+ selection_map["Browser/E2E testing"] = (
2007
+ f"{agent['display_name']} (`{agent_id}`)"
2008
+ )
1708
2009
  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}`)"
2010
+ selection_map["Frontend/UI development"] = (
2011
+ f"{agent['display_name']} (`{agent_id}`)"
2012
+ )
1712
2013
 
1713
2014
  # Always include PM questions
1714
2015
  selection_map["PM questions"] = "Answer directly (only exception)"