claude-mpm 5.1.8__py3-none-any.whl → 5.4.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (191) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  8. claude_mpm/cli/__main__.py +4 -0
  9. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  11. claude_mpm/cli/commands/agents.py +169 -31
  12. claude_mpm/cli/commands/auto_configure.py +210 -25
  13. claude_mpm/cli/commands/config.py +88 -2
  14. claude_mpm/cli/commands/configure.py +1111 -161
  15. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  16. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  17. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  18. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  19. claude_mpm/cli/commands/skills.py +214 -189
  20. claude_mpm/cli/commands/summarize.py +413 -0
  21. claude_mpm/cli/executor.py +11 -3
  22. claude_mpm/cli/parsers/agents_parser.py +54 -9
  23. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  24. claude_mpm/cli/parsers/base_parser.py +5 -0
  25. claude_mpm/cli/parsers/config_parser.py +153 -83
  26. claude_mpm/cli/parsers/skills_parser.py +3 -2
  27. claude_mpm/cli/startup.py +550 -94
  28. claude_mpm/commands/mpm-config.md +265 -0
  29. claude_mpm/commands/mpm-help.md +14 -95
  30. claude_mpm/commands/mpm-organize.md +500 -0
  31. claude_mpm/config/agent_sources.py +27 -0
  32. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  33. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  34. claude_mpm/core/framework_loader.py +4 -2
  35. claude_mpm/core/logger.py +13 -0
  36. claude_mpm/core/output_style_manager.py +173 -43
  37. claude_mpm/core/socketio_pool.py +3 -3
  38. claude_mpm/core/unified_agent_registry.py +134 -16
  39. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  40. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  41. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  42. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  43. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  44. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  45. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  46. claude_mpm/hooks/memory_integration_hook.py +46 -1
  47. claude_mpm/init.py +0 -19
  48. claude_mpm/models/agent_definition.py +7 -0
  49. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  50. claude_mpm/scripts/launch_monitor.py +93 -13
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  53. claude_mpm/services/agents/agent_review_service.py +280 -0
  54. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  55. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  56. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +188 -12
  57. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +531 -55
  58. claude_mpm/services/agents/git_source_manager.py +34 -0
  59. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  60. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  61. claude_mpm/services/agents/toolchain_detector.py +10 -6
  62. claude_mpm/services/analysis/__init__.py +11 -1
  63. claude_mpm/services/analysis/clone_detector.py +1030 -0
  64. claude_mpm/services/command_deployment_service.py +81 -10
  65. claude_mpm/services/event_bus/config.py +3 -1
  66. claude_mpm/services/git/git_operations_service.py +93 -8
  67. claude_mpm/services/monitor/daemon.py +9 -2
  68. claude_mpm/services/monitor/daemon_manager.py +39 -3
  69. claude_mpm/services/monitor/server.py +225 -19
  70. claude_mpm/services/self_upgrade_service.py +120 -12
  71. claude_mpm/services/skills/__init__.py +3 -0
  72. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  73. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  74. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  75. claude_mpm/services/skills_deployer.py +126 -9
  76. claude_mpm/services/socketio/event_normalizer.py +15 -1
  77. claude_mpm/services/socketio/server/core.py +160 -21
  78. claude_mpm/services/version_control/git_operations.py +103 -0
  79. claude_mpm/utils/agent_filters.py +17 -44
  80. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  81. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +86 -176
  82. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  83. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  84. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  85. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  86. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  87. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  88. claude_mpm/agents/BASE_OPS.md +0 -219
  89. claude_mpm/agents/BASE_PM.md +0 -480
  90. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  91. claude_mpm/agents/BASE_QA.md +0 -167
  92. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  93. claude_mpm/agents/base_agent.json +0 -31
  94. claude_mpm/agents/base_agent_loader.py +0 -601
  95. claude_mpm/cli/commands/agents_detect.py +0 -380
  96. claude_mpm/cli/commands/agents_recommend.py +0 -309
  97. claude_mpm/cli/ticket_cli.py +0 -35
  98. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  99. claude_mpm/commands/mpm-agents-detect.md +0 -177
  100. claude_mpm/commands/mpm-agents-list.md +0 -131
  101. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  102. claude_mpm/commands/mpm-config-view.md +0 -150
  103. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  104. claude_mpm/dashboard/analysis_runner.py +0 -455
  105. claude_mpm/dashboard/index.html +0 -13
  106. claude_mpm/dashboard/open_dashboard.py +0 -66
  107. claude_mpm/dashboard/static/css/activity.css +0 -1958
  108. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  109. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  110. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  111. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  112. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  113. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  114. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  115. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  116. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  117. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  118. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  119. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  120. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  121. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  122. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  123. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  124. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  125. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  126. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  127. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  128. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  129. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  130. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  131. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  132. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  133. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  134. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  135. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  136. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  137. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  138. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  139. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  140. claude_mpm/dashboard/templates/code_simple.html +0 -153
  141. claude_mpm/dashboard/templates/index.html +0 -606
  142. claude_mpm/dashboard/test_dashboard.html +0 -372
  143. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  154. claude_mpm/scripts/mcp_server.py +0 -75
  155. claude_mpm/scripts/mcp_wrapper.py +0 -39
  156. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  157. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  158. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  159. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  160. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  161. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  162. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  163. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  164. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  165. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  166. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  167. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  168. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  169. claude_mpm/services/mcp_gateway/main.py +0 -589
  170. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  171. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  172. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  173. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  174. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  175. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  176. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  177. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  178. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  179. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  180. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  181. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  182. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  183. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  184. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  185. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  186. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  187. claude_mpm-5.1.8.dist-info/entry_points.txt +0 -10
  188. claude_mpm-5.1.8.dist-info/licenses/LICENSE +0 -21
  189. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  190. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  191. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ This module handles:
5
5
  2. Output style extraction from framework instructions
6
6
  3. One-time deployment to Claude Code >= 1.0.83 at startup
7
7
  4. Fallback injection for older versions
8
+ 5. Support for multiple output styles (professional and teaching modes)
8
9
 
9
10
  The output style is set once at startup and not monitored or enforced after that.
10
11
  Users can change it if they want, and the system will respect their choice.
@@ -14,7 +15,7 @@ import json
14
15
  import re
15
16
  import subprocess
16
17
  from pathlib import Path
17
- from typing import Dict, Optional
18
+ from typing import Any, Dict, Literal, Optional, TypedDict, cast
18
19
 
19
20
  from ..utils.imports import safe_import
20
21
 
@@ -25,22 +26,56 @@ get_logger = safe_import("claude_mpm.core.logger", "core.logger", ["get_logger"]
25
26
  _CACHED_CLAUDE_VERSION: Optional[str] = None
26
27
  _VERSION_DETECTED: bool = False
27
28
 
29
+ # Output style types
30
+ OutputStyleType = Literal["professional", "teaching"]
31
+
32
+
33
+ class StyleConfig(TypedDict):
34
+ """Configuration for an output style."""
35
+
36
+ source: Path
37
+ target: Path
38
+ name: str
39
+
28
40
 
29
41
  class OutputStyleManager:
30
- """Manages output style deployment and version-based handling."""
42
+ """Manages output style deployment and version-based handling.
43
+
44
+ Supports two output styles:
45
+ - professional: Default Claude MPM style (claude-mpm.md)
46
+ - teaching: Adaptive teaching mode (claude-mpm-teach.md)
47
+ """
31
48
 
32
- def __init__(self):
49
+ def __init__(self) -> None:
33
50
  """Initialize the output style manager."""
34
- self.logger = get_logger("output_style_manager")
51
+ self.logger = get_logger("output_style_manager") # type: ignore[misc]
35
52
  self.claude_version = self._detect_claude_version()
36
- self.output_style_dir = Path.home() / ".claude" / "output-styles"
37
- self.output_style_path = self.output_style_dir / "claude-mpm.md"
53
+
54
+ # Deploy to ~/.claude/styles/ directory (NOT output-styles/)
55
+ self.output_style_dir = Path.home() / ".claude" / "styles"
38
56
  self.settings_file = Path.home() / ".claude" / "settings.json"
39
57
 
40
- # Cache the output style content path
41
- self.mpm_output_style_path = (
42
- Path(__file__).parent.parent / "agents" / "OUTPUT_STYLE.md"
43
- )
58
+ # Style definitions
59
+ self.styles: Dict[str, StyleConfig] = {
60
+ "professional": StyleConfig(
61
+ source=Path(__file__).parent.parent
62
+ / "agents"
63
+ / "CLAUDE_MPM_OUTPUT_STYLE.md",
64
+ target=self.output_style_dir / "claude-mpm.md",
65
+ name="claude-mpm",
66
+ ),
67
+ "teaching": StyleConfig(
68
+ source=Path(__file__).parent.parent
69
+ / "agents"
70
+ / "CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md",
71
+ target=self.output_style_dir / "claude-mpm-teach.md",
72
+ name="claude-mpm-teach",
73
+ ),
74
+ }
75
+
76
+ # Default style path (for backward compatibility)
77
+ self.output_style_path = self.styles["professional"]["target"]
78
+ self.mpm_output_style_path = self.styles["professional"]["source"]
44
79
 
45
80
  def _detect_claude_version(self) -> Optional[str]:
46
81
  """
@@ -158,56 +193,77 @@ class OutputStyleManager:
158
193
  """
159
194
  return not self.supports_output_styles()
160
195
 
161
- def extract_output_style_content(self, framework_loader=None) -> str:
196
+ def extract_output_style_content(
197
+ self, framework_loader: Any = None, style: OutputStyleType = "professional"
198
+ ) -> str:
162
199
  """
163
- Read output style content from OUTPUT_STYLE.md.
200
+ Read output style content from style source file.
164
201
 
165
202
  Args:
166
203
  framework_loader: Optional framework loader (kept for compatibility, not used)
204
+ style: Style type to extract ("professional" or "teaching")
167
205
 
168
206
  Returns:
169
207
  Complete output style content from file
170
208
  """
171
- # Always read from the complete OUTPUT_STYLE.md file
172
- if self.mpm_output_style_path.exists():
173
- content = self.mpm_output_style_path.read_text()
174
- self.logger.info(f"Read OUTPUT_STYLE.md directly ({len(content)} chars)")
209
+ style_config = self.styles[style]
210
+ source_path = style_config["source"]
211
+
212
+ if source_path.exists():
213
+ content = source_path.read_text()
214
+ self.logger.info(
215
+ f"Read {style} style from {source_path.name} ({len(content)} chars)"
216
+ )
175
217
  return content
218
+
176
219
  # Fallback error
177
- error_msg = f"OUTPUT_STYLE.md not found at {self.mpm_output_style_path}"
220
+ error_msg = f"{style} style not found at {source_path}"
178
221
  self.logger.error(error_msg)
179
222
  raise FileNotFoundError(error_msg)
180
223
 
181
- def save_output_style(self, content: str) -> Path:
224
+ def save_output_style(
225
+ self, content: str, style: OutputStyleType = "professional"
226
+ ) -> Path:
182
227
  """
183
- Save output style content to OUTPUT_STYLE.md.
228
+ Save output style content to source file.
184
229
 
185
230
  Args:
186
231
  content: The formatted output style content
232
+ style: Style type to save ("professional" or "teaching")
187
233
 
188
234
  Returns:
189
235
  Path to the saved file
190
236
  """
191
237
  try:
238
+ style_config = self.styles[style]
239
+ source_path = style_config["source"]
240
+
192
241
  # Ensure the parent directory exists
193
- self.mpm_output_style_path.parent.mkdir(parents=True, exist_ok=True)
242
+ source_path.parent.mkdir(parents=True, exist_ok=True)
194
243
 
195
244
  # Write the content
196
- self.mpm_output_style_path.write_text(content, encoding="utf-8")
197
- self.logger.info(f"Saved output style to {self.mpm_output_style_path}")
245
+ source_path.write_text(content, encoding="utf-8")
246
+ self.logger.info(f"Saved {style} style to {source_path}")
198
247
 
199
- return self.mpm_output_style_path
248
+ return source_path
200
249
  except Exception as e:
201
- self.logger.error(f"Failed to save output style: {e}")
250
+ self.logger.error(f"Failed to save {style} style: {e}")
202
251
  raise
203
252
 
204
- def deploy_output_style(self, content: str) -> bool:
253
+ def deploy_output_style(
254
+ self,
255
+ content: Optional[str] = None,
256
+ style: OutputStyleType = "professional",
257
+ activate: bool = True,
258
+ ) -> bool:
205
259
  """
206
260
  Deploy output style to Claude Code if version >= 1.0.83.
207
- Deploys the style file and activates it once.
261
+ Deploys the style file and optionally activates it.
208
262
 
209
263
  Args:
210
- content: The output style content to deploy
264
+ content: The output style content to deploy (if None, reads from source)
265
+ style: Style type to deploy ("professional" or "teaching")
266
+ activate: Whether to activate the style after deployment
211
267
 
212
268
  Returns:
213
269
  True if deployed successfully, False otherwise
@@ -219,26 +275,37 @@ class OutputStyleManager:
219
275
  return False
220
276
 
221
277
  try:
222
- # Ensure output-styles directory exists
278
+ style_config = self.styles[style]
279
+ target_path = style_config["target"]
280
+ style_name = style_config["name"]
281
+
282
+ # If content not provided, read from source
283
+ if content is None:
284
+ content = self.extract_output_style_content(style=style)
285
+
286
+ # Ensure styles directory exists
223
287
  self.output_style_dir.mkdir(parents=True, exist_ok=True)
224
288
 
225
289
  # Write the output style file
226
- self.output_style_path.write_text(content, encoding="utf-8")
227
- self.logger.info(f"Deployed output style to {self.output_style_path}")
290
+ target_path.write_text(content, encoding="utf-8")
291
+ self.logger.info(f"Deployed {style} style to {target_path}")
228
292
 
229
- # Activate the claude-mpm style
230
- self._activate_output_style()
293
+ # Activate the style if requested
294
+ if activate:
295
+ self._activate_output_style(style_name)
231
296
 
232
297
  return True
233
298
 
234
299
  except Exception as e:
235
- self.logger.error(f"Failed to deploy output style: {e}")
300
+ self.logger.error(f"Failed to deploy {style} style: {e}")
236
301
  return False
237
302
 
238
- def _activate_output_style(self) -> bool:
303
+ def _activate_output_style(self, style_name: str = "claude-mpm") -> bool:
239
304
  """
240
- Update Claude Code settings to activate the claude-mpm output style.
241
- Sets activeOutputStyle to "claude-mpm" once at startup.
305
+ Update Claude Code settings to activate a specific output style.
306
+
307
+ Args:
308
+ style_name: Name of the style to activate (e.g., "claude-mpm", "claude-mpm-teach")
242
309
 
243
310
  Returns:
244
311
  True if activated successfully, False otherwise
@@ -257,9 +324,9 @@ class OutputStyleManager:
257
324
  # Check current active style
258
325
  current_style = settings.get("activeOutputStyle")
259
326
 
260
- # Update active output style to claude-mpm if not already set
261
- if current_style != "claude-mpm":
262
- settings["activeOutputStyle"] = "claude-mpm"
327
+ # Update active output style if different
328
+ if current_style != style_name:
329
+ settings["activeOutputStyle"] = style_name
263
330
 
264
331
  # Ensure settings directory exists
265
332
  self.settings_file.parent.mkdir(parents=True, exist_ok=True)
@@ -270,10 +337,10 @@ class OutputStyleManager:
270
337
  )
271
338
 
272
339
  self.logger.info(
273
- f"✅ Activated claude-mpm output style (was: {current_style or 'none'})"
340
+ f"✅ Activated {style_name} output style (was: {current_style or 'none'})"
274
341
  )
275
342
  else:
276
- self.logger.debug("Claude MPM output style already active")
343
+ self.logger.debug(f"{style_name} output style already active")
277
344
 
278
345
  return True
279
346
 
@@ -319,7 +386,9 @@ class OutputStyleManager:
319
386
 
320
387
  return status
321
388
 
322
- def get_injectable_content(self, framework_loader=None) -> str:
389
+ def get_injectable_content(
390
+ self, framework_loader: Any = None, style: OutputStyleType = "professional"
391
+ ) -> str:
323
392
  """
324
393
  Get output style content for injection into instructions (for Claude < 1.0.83).
325
394
 
@@ -328,12 +397,13 @@ class OutputStyleManager:
328
397
 
329
398
  Args:
330
399
  framework_loader: Optional FrameworkLoader instance to reuse loaded content
400
+ style: Style type to extract ("professional" or "teaching")
331
401
 
332
402
  Returns:
333
403
  Simplified output style content for injection
334
404
  """
335
405
  # Extract the same content but without YAML frontmatter
336
- full_content = self.extract_output_style_content(framework_loader)
406
+ full_content = self.extract_output_style_content(framework_loader, style=style)
337
407
 
338
408
  # Remove YAML frontmatter
339
409
  lines = full_content.split("\n")
@@ -351,3 +421,63 @@ class OutputStyleManager:
351
421
 
352
422
  # If no frontmatter found, return as-is
353
423
  return full_content
424
+
425
+ def deploy_all_styles(self, activate_default: bool = True) -> Dict[str, bool]:
426
+ """
427
+ Deploy all available output styles to Claude Code.
428
+
429
+ Args:
430
+ activate_default: Whether to activate the professional style after deployment
431
+
432
+ Returns:
433
+ Dictionary mapping style names to deployment success status
434
+ """
435
+ results: Dict[str, bool] = {}
436
+
437
+ for style_type_key in self.styles:
438
+ # Deploy without activation
439
+ # Cast is safe because we know self.styles keys are OutputStyleType
440
+ style_type = cast("OutputStyleType", style_type_key)
441
+ success = self.deploy_output_style(style=style_type, activate=False)
442
+ results[style_type] = success
443
+
444
+ # Activate the default style if requested
445
+ if activate_default and results.get("professional", False):
446
+ self._activate_output_style("claude-mpm")
447
+
448
+ return results
449
+
450
+ def deploy_teaching_style(self, activate: bool = False) -> bool:
451
+ """
452
+ Deploy the teaching style specifically.
453
+
454
+ Args:
455
+ activate: Whether to activate the teaching style after deployment
456
+
457
+ Returns:
458
+ True if deployed successfully, False otherwise
459
+ """
460
+ return self.deploy_output_style(style="teaching", activate=activate)
461
+
462
+ def list_available_styles(self) -> Dict[str, Dict[str, str]]:
463
+ """
464
+ List all available output styles with their metadata.
465
+
466
+ Returns:
467
+ Dictionary mapping style types to their configuration
468
+ """
469
+ available_styles = {}
470
+
471
+ for style_type, config in self.styles.items():
472
+ source_exists = config["source"].exists()
473
+ target_exists = config["target"].exists()
474
+
475
+ available_styles[style_type] = {
476
+ "name": config["name"],
477
+ "source_path": str(config["source"]),
478
+ "target_path": str(config["target"]),
479
+ "source_exists": str(source_exists),
480
+ "deployed": str(target_exists),
481
+ }
482
+
483
+ return available_styles
@@ -55,8 +55,8 @@ class CircuitState(Enum):
55
55
  class ConnectionStats:
56
56
  """Connection statistics for monitoring."""
57
57
 
58
- created_at: datetime = field(default_factory=datetime.now)
59
- last_used: datetime = field(default_factory=datetime.now)
58
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
59
+ last_used: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
60
60
  events_sent: int = 0
61
61
  errors: int = 0
62
62
  consecutive_errors: int = 0
@@ -70,7 +70,7 @@ class BatchEvent:
70
70
  namespace: str
71
71
  event: str
72
72
  data: Dict[str, Any]
73
- timestamp: datetime = field(default_factory=datetime.now)
73
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
74
74
 
75
75
 
76
76
  class CircuitBreaker:
@@ -84,6 +84,12 @@ class AgentMetadata:
84
84
  version: str = "1.0.0"
85
85
  author: str = ""
86
86
  tags: List[str] = None
87
+ # NEW: Collection-based identification fields
88
+ collection_id: Optional[str] = None # Format: owner/repo-name
89
+ source_path: Optional[str] = None # Relative path in repo
90
+ canonical_id: Optional[str] = (
91
+ None # Format: collection_id:agent_id or legacy:filename
92
+ )
87
93
 
88
94
  def __post_init__(self):
89
95
  """Initialize default values for mutable fields."""
@@ -168,26 +174,16 @@ class UnifiedAgentRegistry:
168
174
  if project_path.exists():
169
175
  self.discovery_paths.append(project_path)
170
176
 
171
- # Also check for local JSON templates in .claude-mpm/agents/
172
- local_project_path = self.path_manager.project_root / ".claude-mpm" / "agents"
173
- if (
174
- local_project_path.exists()
175
- and local_project_path not in self.discovery_paths
176
- ):
177
- self.discovery_paths.append(local_project_path)
178
- logger.debug(f"Added local project templates path: {local_project_path}")
177
+ # NOTE: .claude-mpm/agents/ is deprecated in the simplified architecture
178
+ # Source agents come from ~/.claude-mpm/cache/remote-agents/
179
+ # Deployed agents go to .claude/agents/
179
180
 
180
- # User-level agents
181
+ # User-level agents (deprecated in simplified architecture)
182
+ # Keeping for backward compatibility but not actively used
181
183
  user_path = self.path_manager.get_user_agents_dir()
182
184
  if user_path.exists():
183
185
  self.discovery_paths.append(user_path)
184
186
 
185
- # Also check for user JSON templates in ~/.claude-mpm/agents/
186
- local_user_path = Path.home() / ".claude-mpm" / "agents"
187
- if local_user_path.exists() and local_user_path not in self.discovery_paths:
188
- self.discovery_paths.append(local_user_path)
189
- logger.debug(f"Added local user templates path: {local_user_path}")
190
-
191
187
  # System-level agents (includes templates as a subdirectory)
192
188
  system_path = self.path_manager.get_system_agents_dir()
193
189
  if system_path.exists():
@@ -690,6 +686,111 @@ class UnifiedAgentRegistry:
690
686
  """Get all memory-aware agents."""
691
687
  return self.list_agents(agent_type=AgentType.MEMORY_AWARE)
692
688
 
689
+ def get_agents_by_collection(self, collection_id: str) -> List[AgentMetadata]:
690
+ """Get all agents from a specific collection.
691
+
692
+ NEW: Enables collection-based agent selection.
693
+
694
+ Args:
695
+ collection_id: Collection identifier (e.g., "bobmatnyc/claude-mpm-agents")
696
+
697
+ Returns:
698
+ List of agents from the specified collection
699
+
700
+ Example:
701
+ >>> registry = get_agent_registry()
702
+ >>> agents = registry.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
703
+ >>> len(agents)
704
+ 45
705
+ """
706
+ if not self.registry:
707
+ self.discover_agents()
708
+
709
+ collection_agents = [
710
+ agent
711
+ for agent in self.registry.values()
712
+ if agent.collection_id == collection_id
713
+ ]
714
+
715
+ return sorted(collection_agents, key=lambda a: a.name)
716
+
717
+ def list_collections(self) -> List[Dict[str, Any]]:
718
+ """List all available collections with agent counts.
719
+
720
+ NEW: Provides overview of available collections.
721
+
722
+ Returns:
723
+ List of collection info dictionaries with:
724
+ - collection_id: Collection identifier
725
+ - agent_count: Number of agents in collection
726
+ - agents: List of agent names in collection
727
+
728
+ Example:
729
+ >>> registry = get_agent_registry()
730
+ >>> collections = registry.list_collections()
731
+ >>> collections
732
+ [
733
+ {
734
+ "collection_id": "bobmatnyc/claude-mpm-agents",
735
+ "agent_count": 45,
736
+ "agents": ["pm", "engineer", "qa", ...]
737
+ }
738
+ ]
739
+ """
740
+ if not self.registry:
741
+ self.discover_agents()
742
+
743
+ # Group agents by collection_id
744
+ collections_map: Dict[str, List[str]] = {}
745
+
746
+ for agent in self.registry.values():
747
+ if not agent.collection_id:
748
+ # Skip agents without collection (legacy or local)
749
+ continue
750
+
751
+ if agent.collection_id not in collections_map:
752
+ collections_map[agent.collection_id] = []
753
+
754
+ collections_map[agent.collection_id].append(agent.name)
755
+
756
+ # Convert to list format
757
+ collections = [
758
+ {
759
+ "collection_id": coll_id,
760
+ "agent_count": len(agent_names),
761
+ "agents": sorted(agent_names),
762
+ }
763
+ for coll_id, agent_names in collections_map.items()
764
+ ]
765
+
766
+ return sorted(collections, key=lambda c: c["collection_id"])
767
+
768
+ def get_agent_by_canonical_id(self, canonical_id: str) -> Optional[AgentMetadata]:
769
+ """Get agent by canonical ID (primary matching key).
770
+
771
+ NEW: Primary matching method using canonical_id.
772
+
773
+ Args:
774
+ canonical_id: Canonical identifier (e.g., "bobmatnyc/claude-mpm-agents:pm")
775
+
776
+ Returns:
777
+ AgentMetadata if found, None otherwise
778
+
779
+ Example:
780
+ >>> registry = get_agent_registry()
781
+ >>> agent = registry.get_agent_by_canonical_id("bobmatnyc/claude-mpm-agents:pm")
782
+ >>> agent.name
783
+ 'Project Manager Agent'
784
+ """
785
+ if not self.registry:
786
+ self.discover_agents()
787
+
788
+ for agent in self.registry.values():
789
+ if agent.canonical_id == canonical_id:
790
+ return agent
791
+
792
+ return None
793
+
693
794
  def add_discovery_path(self, path: Union[str, Path]) -> None:
694
795
  """Add a new path for agent discovery."""
695
796
  path = Path(path)
@@ -809,6 +910,21 @@ def get_registry_stats() -> Dict[str, Any]:
809
910
  return get_agent_registry().get_registry_stats()
810
911
 
811
912
 
913
+ def get_agents_by_collection(collection_id: str) -> List[AgentMetadata]:
914
+ """Get all agents from a specific collection."""
915
+ return get_agent_registry().get_agents_by_collection(collection_id)
916
+
917
+
918
+ def list_collections() -> List[Dict[str, Any]]:
919
+ """List all available collections."""
920
+ return get_agent_registry().list_collections()
921
+
922
+
923
+ def get_agent_by_canonical_id(canonical_id: str) -> Optional[AgentMetadata]:
924
+ """Get agent by canonical ID."""
925
+ return get_agent_registry().get_agent_by_canonical_id(canonical_id)
926
+
927
+
812
928
  # Legacy function names for backward compatibility
813
929
  def listAgents() -> List[str]:
814
930
  """Legacy function: Get list of agent names."""
@@ -838,14 +954,16 @@ __all__ = [
838
954
  "discover_agents",
839
955
  "discover_agents_sync",
840
956
  "get_agent",
957
+ "get_agent_by_canonical_id",
841
958
  "get_agent_names",
842
959
  "get_agent_registry",
960
+ "get_agents_by_collection",
843
961
  "get_core_agents",
844
962
  "get_project_agents",
845
963
  "get_registry_stats",
846
964
  "get_specialized_agents",
847
- # Legacy compatibility
848
965
  "listAgents",
849
966
  "list_agents",
850
967
  "list_agents_all",
968
+ "list_collections",
851
969
  ]
@@ -0,0 +1,60 @@
1
+ """Cross-process correlation storage using .claude-mpm directory."""
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+
7
+
8
+ def get_correlation_dir() -> Path:
9
+ """Get correlation directory in project's .claude-mpm folder."""
10
+ # Use CWD's .claude-mpm directory (where hooks run from)
11
+ cwd = Path.cwd()
12
+ return cwd / ".claude-mpm" / "correlations"
13
+
14
+
15
+ TTL_SECONDS = 3600 # 1 hour
16
+
17
+
18
+ class CorrelationManager:
19
+ """Manages correlation IDs across separate hook processes."""
20
+
21
+ @staticmethod
22
+ def store(session_id: str, tool_call_id: str, tool_name: str) -> None:
23
+ """Store correlation data for later retrieval by post_tool."""
24
+ correlation_dir = get_correlation_dir()
25
+ correlation_dir.mkdir(parents=True, exist_ok=True)
26
+ filepath = correlation_dir / f"correlation_{session_id}.json"
27
+ data = {
28
+ "tool_call_id": tool_call_id,
29
+ "tool_name": tool_name,
30
+ "timestamp": time.time(),
31
+ }
32
+ filepath.write_text(json.dumps(data))
33
+
34
+ @staticmethod
35
+ def retrieve(session_id: str) -> str | None:
36
+ """Retrieve and delete correlation data from temp file."""
37
+ correlation_dir = get_correlation_dir()
38
+ filepath = correlation_dir / f"correlation_{session_id}.json"
39
+ if not filepath.exists():
40
+ return None
41
+ try:
42
+ data = json.loads(filepath.read_text())
43
+ filepath.unlink() # Delete after reading
44
+ return data.get("tool_call_id")
45
+ except (json.JSONDecodeError, OSError):
46
+ return None
47
+
48
+ @staticmethod
49
+ def cleanup_old() -> None:
50
+ """Remove correlation files older than TTL."""
51
+ correlation_dir = get_correlation_dir()
52
+ if not correlation_dir.exists():
53
+ return
54
+ now = time.time()
55
+ for filepath in correlation_dir.glob("correlation_*.json"):
56
+ try:
57
+ if now - filepath.stat().st_mtime > TTL_SECONDS:
58
+ filepath.unlink()
59
+ except OSError:
60
+ pass