claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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 (214) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +216 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +116 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +124 -3
  19. claude_mpm/cli/startup_display.py +2 -1
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +78 -0
  22. claude_mpm/commander/adapters/__init__.py +60 -0
  23. claude_mpm/commander/adapters/auggie.py +260 -0
  24. claude_mpm/commander/adapters/base.py +288 -0
  25. claude_mpm/commander/adapters/claude_code.py +392 -0
  26. claude_mpm/commander/adapters/codex.py +237 -0
  27. claude_mpm/commander/adapters/communication.py +366 -0
  28. claude_mpm/commander/adapters/example_usage.py +310 -0
  29. claude_mpm/commander/adapters/mpm.py +389 -0
  30. claude_mpm/commander/adapters/registry.py +204 -0
  31. claude_mpm/commander/api/__init__.py +16 -0
  32. claude_mpm/commander/api/app.py +121 -0
  33. claude_mpm/commander/api/errors.py +133 -0
  34. claude_mpm/commander/api/routes/__init__.py +8 -0
  35. claude_mpm/commander/api/routes/events.py +184 -0
  36. claude_mpm/commander/api/routes/inbox.py +171 -0
  37. claude_mpm/commander/api/routes/messages.py +148 -0
  38. claude_mpm/commander/api/routes/projects.py +271 -0
  39. claude_mpm/commander/api/routes/sessions.py +226 -0
  40. claude_mpm/commander/api/routes/work.py +296 -0
  41. claude_mpm/commander/api/schemas.py +186 -0
  42. claude_mpm/commander/chat/__init__.py +7 -0
  43. claude_mpm/commander/chat/cli.py +111 -0
  44. claude_mpm/commander/chat/commands.py +96 -0
  45. claude_mpm/commander/chat/repl.py +310 -0
  46. claude_mpm/commander/config.py +49 -0
  47. claude_mpm/commander/config_loader.py +115 -0
  48. claude_mpm/commander/core/__init__.py +10 -0
  49. claude_mpm/commander/core/block_manager.py +325 -0
  50. claude_mpm/commander/core/response_manager.py +323 -0
  51. claude_mpm/commander/daemon.py +594 -0
  52. claude_mpm/commander/env_loader.py +59 -0
  53. claude_mpm/commander/events/__init__.py +26 -0
  54. claude_mpm/commander/events/manager.py +332 -0
  55. claude_mpm/commander/frameworks/__init__.py +12 -0
  56. claude_mpm/commander/frameworks/base.py +143 -0
  57. claude_mpm/commander/frameworks/claude_code.py +58 -0
  58. claude_mpm/commander/frameworks/mpm.py +62 -0
  59. claude_mpm/commander/inbox/__init__.py +16 -0
  60. claude_mpm/commander/inbox/dedup.py +128 -0
  61. claude_mpm/commander/inbox/inbox.py +224 -0
  62. claude_mpm/commander/inbox/models.py +70 -0
  63. claude_mpm/commander/instance_manager.py +337 -0
  64. claude_mpm/commander/llm/__init__.py +6 -0
  65. claude_mpm/commander/llm/openrouter_client.py +167 -0
  66. claude_mpm/commander/llm/summarizer.py +70 -0
  67. claude_mpm/commander/memory/__init__.py +45 -0
  68. claude_mpm/commander/memory/compression.py +347 -0
  69. claude_mpm/commander/memory/embeddings.py +230 -0
  70. claude_mpm/commander/memory/entities.py +310 -0
  71. claude_mpm/commander/memory/example_usage.py +290 -0
  72. claude_mpm/commander/memory/integration.py +325 -0
  73. claude_mpm/commander/memory/search.py +381 -0
  74. claude_mpm/commander/memory/store.py +657 -0
  75. claude_mpm/commander/models/__init__.py +18 -0
  76. claude_mpm/commander/models/events.py +121 -0
  77. claude_mpm/commander/models/project.py +162 -0
  78. claude_mpm/commander/models/work.py +214 -0
  79. claude_mpm/commander/parsing/__init__.py +20 -0
  80. claude_mpm/commander/parsing/extractor.py +132 -0
  81. claude_mpm/commander/parsing/output_parser.py +270 -0
  82. claude_mpm/commander/parsing/patterns.py +100 -0
  83. claude_mpm/commander/persistence/__init__.py +11 -0
  84. claude_mpm/commander/persistence/event_store.py +274 -0
  85. claude_mpm/commander/persistence/state_store.py +309 -0
  86. claude_mpm/commander/persistence/work_store.py +164 -0
  87. claude_mpm/commander/polling/__init__.py +13 -0
  88. claude_mpm/commander/polling/event_detector.py +104 -0
  89. claude_mpm/commander/polling/output_buffer.py +49 -0
  90. claude_mpm/commander/polling/output_poller.py +153 -0
  91. claude_mpm/commander/project_session.py +268 -0
  92. claude_mpm/commander/proxy/__init__.py +12 -0
  93. claude_mpm/commander/proxy/formatter.py +89 -0
  94. claude_mpm/commander/proxy/output_handler.py +191 -0
  95. claude_mpm/commander/proxy/relay.py +155 -0
  96. claude_mpm/commander/registry.py +410 -0
  97. claude_mpm/commander/runtime/__init__.py +10 -0
  98. claude_mpm/commander/runtime/executor.py +191 -0
  99. claude_mpm/commander/runtime/monitor.py +346 -0
  100. claude_mpm/commander/session/__init__.py +6 -0
  101. claude_mpm/commander/session/context.py +81 -0
  102. claude_mpm/commander/session/manager.py +59 -0
  103. claude_mpm/commander/tmux_orchestrator.py +361 -0
  104. claude_mpm/commander/web/__init__.py +1 -0
  105. claude_mpm/commander/work/__init__.py +30 -0
  106. claude_mpm/commander/work/executor.py +207 -0
  107. claude_mpm/commander/work/queue.py +405 -0
  108. claude_mpm/commander/workflow/__init__.py +27 -0
  109. claude_mpm/commander/workflow/event_handler.py +241 -0
  110. claude_mpm/commander/workflow/notifier.py +146 -0
  111. claude_mpm/commands/mpm-config.md +8 -0
  112. claude_mpm/commands/mpm-doctor.md +8 -0
  113. claude_mpm/commands/mpm-help.md +8 -0
  114. claude_mpm/commands/mpm-init.md +8 -0
  115. claude_mpm/commands/mpm-monitor.md +8 -0
  116. claude_mpm/commands/mpm-organize.md +8 -0
  117. claude_mpm/commands/mpm-postmortem.md +8 -0
  118. claude_mpm/commands/mpm-session-resume.md +8 -0
  119. claude_mpm/commands/mpm-status.md +8 -0
  120. claude_mpm/commands/mpm-ticket-view.md +8 -0
  121. claude_mpm/commands/mpm-version.md +8 -0
  122. claude_mpm/commands/mpm.md +8 -0
  123. claude_mpm/config/agent_presets.py +8 -7
  124. claude_mpm/config/skill_sources.py +16 -0
  125. claude_mpm/core/claude_runner.py +143 -0
  126. claude_mpm/core/config.py +32 -19
  127. claude_mpm/core/logger.py +26 -9
  128. claude_mpm/core/logging_utils.py +35 -11
  129. claude_mpm/core/output_style_manager.py +49 -12
  130. claude_mpm/core/unified_config.py +10 -6
  131. claude_mpm/core/unified_paths.py +68 -80
  132. claude_mpm/experimental/cli_enhancements.py +2 -1
  133. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  136. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  155. claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
  156. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  157. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  158. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  159. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  160. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  164. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  165. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  166. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  167. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  168. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  169. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  170. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  171. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  172. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  173. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  174. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  175. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  176. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  177. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  178. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  179. claude_mpm/hooks/session_resume_hook.py +22 -18
  180. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  181. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  182. claude_mpm/scripts/start_activity_logging.py +0 -0
  183. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  184. claude_mpm/services/agents/agent_selection_service.py +2 -2
  185. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  186. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  187. claude_mpm/services/event_log.py +8 -0
  188. claude_mpm/services/pm_skills_deployer.py +84 -6
  189. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  190. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  191. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  192. claude_mpm/services/skills_deployer.py +31 -5
  193. claude_mpm/skills/__init__.py +2 -1
  194. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  195. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  196. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  197. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  198. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  199. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  200. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  201. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  202. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  203. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  204. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  205. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  206. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  207. claude_mpm/skills/registry.py +295 -90
  208. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
  209. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
  210. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  211. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  212. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  213. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  214. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
claude_mpm/core/config.py CHANGED
@@ -12,11 +12,10 @@ import threading
12
12
  from pathlib import Path
13
13
  from typing import Any, Dict, List, Optional, Tuple, Union
14
14
 
15
- import yaml
16
-
17
15
  from claude_mpm.core.logging_utils import get_logger
18
16
 
19
- from ..utils.config_manager import ConfigurationManager
17
+ # Lazy import ConfigurationManager to avoid importing yaml at module level
18
+ # This prevents hook errors when yaml isn't available in the execution environment
20
19
  from .exceptions import ConfigurationError, FileOperationError
21
20
  from .unified_paths import get_path_manager
22
21
 
@@ -104,6 +103,9 @@ class Config:
104
103
  Config._initialized = True
105
104
  logger.debug("Initializing Config singleton for the first time")
106
105
 
106
+ # Lazy import ConfigurationManager at runtime to avoid yaml import at module level
107
+ from ..utils.config_manager import ConfigurationManager
108
+
107
109
  # Initialize instance variables inside the lock to ensure thread safety
108
110
  self._config: Dict[str, Any] = {}
109
111
  self._env_prefix = env_prefix
@@ -224,21 +226,6 @@ class Config:
224
226
  f"Response logging format: {response_logging.get('format', 'json')}"
225
227
  )
226
228
 
227
- except yaml.YAMLError as e:
228
- logger.error(f"YAML syntax error in {file_path}: {e}")
229
- if hasattr(e, "problem_mark"):
230
- mark = e.problem_mark
231
- logger.error(f"Error at line {mark.line + 1}, column {mark.column + 1}")
232
- logger.info(
233
- "TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
234
- )
235
- logger.info(
236
- "TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
237
- + str(file_path)
238
- )
239
- # Store error for later retrieval
240
- self._config["_load_error"] = str(e)
241
-
242
229
  except json.JSONDecodeError as e:
243
230
  logger.error(f"JSON syntax error in {file_path}: {e}")
244
231
  logger.error(f"Error at line {e.lineno}, column {e.colno}")
@@ -255,7 +242,28 @@ class Config:
255
242
  },
256
243
  ) from e
257
244
  except Exception as e:
258
- # Catch any remaining unexpected errors and wrap them as configuration errors
245
+ # Handle YAML errors without importing yaml at module level
246
+ # ConfigurationManager.load_yaml raises yaml.YAMLError, but we don't want to import yaml
247
+ # Check if it's a YAML error by class name to avoid import
248
+ if e.__class__.__name__ == "YAMLError":
249
+ logger.error(f"YAML syntax error in {file_path}: {e}")
250
+ if hasattr(e, "problem_mark"):
251
+ mark = e.problem_mark
252
+ logger.error(
253
+ f"Error at line {mark.line + 1}, column {mark.column + 1}"
254
+ )
255
+ logger.info(
256
+ "TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
257
+ )
258
+ logger.info(
259
+ "TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
260
+ + str(file_path)
261
+ )
262
+ # Store error for later retrieval
263
+ self._config["_load_error"] = str(e)
264
+ return # Don't re-raise, we handled it
265
+
266
+ # Not a YAML error, wrap as configuration error
259
267
  raise ConfigurationError(
260
268
  f"Unexpected error loading configuration from {file_path}: {e}",
261
269
  context={
@@ -605,6 +613,11 @@ class Config:
605
613
  "sync_interval": "startup", # Options: "startup", "hourly", "daily", "manual"
606
614
  "cache_dir": str(Path.home() / ".claude-mpm" / "cache" / "agents"),
607
615
  },
616
+ # Autotodos configuration
617
+ "autotodos": {
618
+ "auto_inject_on_startup": True, # Auto-inject pending todos on PM session start
619
+ "max_todos_per_session": 10, # Max todos to inject per session
620
+ },
608
621
  }
609
622
 
610
623
  # Apply defaults for missing keys
claude_mpm/core/logger.py CHANGED
@@ -214,19 +214,22 @@ def setup_logging(
214
214
 
215
215
  # Console handler
216
216
  if console_output:
217
+ # MUST use stderr to avoid corrupting hook JSON output
218
+ # WHY stderr: Hook handlers output JSON to stdout. Logging to stdout
219
+ # corrupts this JSON and causes "hook error" messages from Claude Code.
217
220
  if use_streaming:
218
221
  # Use streaming handler for single-line INFO messages
219
- console_handler = StreamingHandler(sys.stdout)
222
+ console_handler = StreamingHandler(sys.stderr)
220
223
  console_handler.setFormatter(simple_formatter)
221
224
  elif use_rich and not json_format:
222
225
  # Rich support has been removed, use standard handler
223
- console_handler = logging.StreamHandler(sys.stdout)
226
+ console_handler = logging.StreamHandler(sys.stderr)
224
227
  console_handler.setFormatter(simple_formatter)
225
228
  else:
226
- console_handler = logging.StreamHandler(sys.stdout)
229
+ console_handler = logging.StreamHandler(sys.stderr)
227
230
  console_handler.setFormatter(formatter if json_format else simple_formatter)
228
231
 
229
- console_handler.setLevel(logging.INFO)
232
+ console_handler.setLevel(log_level) # Respect the requested log level
230
233
  logger.addHandler(console_handler)
231
234
 
232
235
  # File handler
@@ -263,7 +266,7 @@ def setup_logging(
263
266
  if deleted_count > 0:
264
267
  # Log to the new file handler that we're about to add
265
268
  pass # Deletion count will be logged when logger is ready
266
- except Exception:
269
+ except Exception: # nosec B110 - intentional: logging should not break app
267
270
  pass # Ignore cleanup errors
268
271
 
269
272
  # Also create a symlink to latest log (with thread safety)
@@ -305,7 +308,7 @@ def setup_logging(
305
308
  # Fallback: try to create a regular file with reference to actual log
306
309
  try:
307
310
  latest_link.write_text(f"Latest log: {log_file.name}\n")
308
- except Exception:
311
+ except Exception: # nosec B110 - intentional: logging should not break app
309
312
  pass # Silently fail - logging should not break the application
310
313
  except Exception as e:
311
314
  # Catch any other unexpected errors to ensure logging doesn't break
@@ -381,15 +384,29 @@ def cleanup_old_mpm_logs(
381
384
  try:
382
385
  log_file.unlink()
383
386
  deleted_count += 1
384
- except Exception:
387
+ except Exception: # nosec B110 - intentional: log cleanup is best-effort
385
388
  pass # Ignore deletion errors
386
389
 
387
390
  return deleted_count
388
391
 
389
392
 
390
393
  def get_logger(name: str) -> logging.Logger:
391
- """Get a logger instance."""
392
- return logging.getLogger(f"claude_mpm.{name}")
394
+ """Get a logger instance.
395
+
396
+ CRITICAL: Respects startup suppression mode (CRITICAL+1) to prevent
397
+ early INFO logs before setup_logging() is called.
398
+ """
399
+ logger = logging.getLogger(f"claude_mpm.{name}")
400
+
401
+ # Check if root logger is suppressed (startup.py sets CRITICAL+1)
402
+ root_logger = logging.getLogger()
403
+ if root_logger.level > logging.CRITICAL:
404
+ # Suppression active - ensure this logger is also suppressed
405
+ logger.setLevel(logging.CRITICAL + 1)
406
+ logger.handlers = []
407
+ logger.propagate = False
408
+
409
+ return logger
393
410
 
394
411
 
395
412
  def setup_streaming_logger(name: str, level: str = "INFO") -> logging.Logger:
@@ -103,21 +103,45 @@ class LoggerFactory:
103
103
 
104
104
  # Set up root logger
105
105
  root_logger = logging.getLogger()
106
- root_logger.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
106
+
107
+ # CRITICAL FIX: Respect existing root logger suppression
108
+ # If root logger is already set to CRITICAL+1 (suppressed by startup.py),
109
+ # don't override it. This prevents logging from appearing during startup
110
+ # before the CLI's setup_logging() runs.
111
+ current_level = root_logger.level
112
+ desired_level = LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
113
+
114
+ # Only set level if current is unset (0) or lower than desired
115
+ # CRITICAL+1 is 51, so this check preserves suppression
116
+ should_configure_logging = current_level == 0 or (
117
+ current_level < desired_level and current_level <= logging.CRITICAL
118
+ )
119
+
120
+ if should_configure_logging:
121
+ root_logger.setLevel(desired_level)
122
+ # else: root logger is suppressed (CRITICAL+1), keep it suppressed
107
123
 
108
124
  # Remove existing handlers
109
125
  root_logger.handlers = []
110
126
 
111
- # Console handler
112
- console_handler = logging.StreamHandler(sys.stdout)
113
- console_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
114
- console_formatter = logging.Formatter(
115
- log_format or LoggingConfig.DEFAULT_FORMAT,
116
- date_format or LoggingConfig.DATE_FORMAT,
117
- )
118
- console_handler.setFormatter(console_formatter)
119
- root_logger.addHandler(console_handler)
120
- cls._handlers["console"] = console_handler
127
+ # CRITICAL FIX: Don't add handlers if logging is suppressed
128
+ # If root logger is at CRITICAL+1 (startup suppression), don't add any handlers
129
+ # This prevents early imports from logging before CLI setup_logging() runs
130
+ if should_configure_logging:
131
+ # Console handler - MUST use stderr to avoid corrupting hook JSON output
132
+ # WHY stderr: Hook handlers output JSON to stdout. Logging to stdout
133
+ # corrupts this JSON and causes "hook error" messages from Claude Code.
134
+ console_handler = logging.StreamHandler(sys.stderr)
135
+ console_handler.setLevel(
136
+ LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
137
+ )
138
+ console_formatter = logging.Formatter(
139
+ log_format or LoggingConfig.DEFAULT_FORMAT,
140
+ date_format or LoggingConfig.DATE_FORMAT,
141
+ )
142
+ console_handler.setFormatter(console_formatter)
143
+ root_logger.addHandler(console_handler)
144
+ cls._handlers["console"] = console_handler
121
145
 
122
146
  # File handler (optional)
123
147
  if log_to_file and cls._log_dir:
@@ -27,7 +27,9 @@ _CACHED_CLAUDE_VERSION: Optional[str] = None
27
27
  _VERSION_DETECTED: bool = False
28
28
 
29
29
  # Output style types
30
- OutputStyleType = Literal["professional", "teaching", "founders"]
30
+ OutputStyleType = Literal[
31
+ "professional", "teaching", "research", "founders"
32
+ ] # "founders" is deprecated, use "research"
31
33
 
32
34
 
33
35
  class StyleConfig(TypedDict):
@@ -44,7 +46,7 @@ class OutputStyleManager:
44
46
  Supports three output styles:
45
47
  - professional: Default Claude MPM style (claude-mpm.md)
46
48
  - teaching: Adaptive teaching mode (claude-mpm-teacher.md)
47
- - founders: Non-technical mode for startup founders (claude-mpm-founders.md)
49
+ - research: Codebase research mode for founders, PMs, and developers (claude-mpm-research.md)
48
50
  """
49
51
 
50
52
  def __init__(self) -> None:
@@ -72,12 +74,20 @@ class OutputStyleManager:
72
74
  target=self.output_style_dir / "claude-mpm-teacher.md",
73
75
  name="Claude MPM Teacher",
74
76
  ),
77
+ "research": StyleConfig(
78
+ source=Path(__file__).parent.parent
79
+ / "agents"
80
+ / "CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md",
81
+ target=self.output_style_dir / "claude-mpm-research.md",
82
+ name="Claude MPM Research",
83
+ ),
84
+ # Backward compatibility alias (deprecated)
75
85
  "founders": StyleConfig(
76
86
  source=Path(__file__).parent.parent
77
87
  / "agents"
78
- / "CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md",
79
- target=self.output_style_dir / "claude-mpm-founders.md",
80
- name="Claude MPM Founders",
88
+ / "CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md",
89
+ target=self.output_style_dir / "claude-mpm-research.md",
90
+ name="Claude MPM Research",
81
91
  ),
82
92
  }
83
93
 
@@ -287,6 +297,9 @@ class OutputStyleManager:
287
297
  target_path = style_config["target"]
288
298
  style_name = style_config["name"]
289
299
 
300
+ # Check if this is a fresh install (file doesn't exist yet)
301
+ is_fresh_install = not target_path.exists()
302
+
290
303
  # If content not provided, read from source
291
304
  if content is None:
292
305
  content = self.extract_output_style_content(style=style)
@@ -300,7 +313,9 @@ class OutputStyleManager:
300
313
 
301
314
  # Activate the style if requested
302
315
  if activate:
303
- self._activate_output_style(style_name)
316
+ self._activate_output_style(
317
+ style_name, is_fresh_install=is_fresh_install
318
+ )
304
319
 
305
320
  return True
306
321
 
@@ -308,12 +323,21 @@ class OutputStyleManager:
308
323
  self.logger.error(f"Failed to deploy {style} style: {e}")
309
324
  return False
310
325
 
311
- def _activate_output_style(self, style_name: str = "Claude MPM") -> bool:
326
+ def _activate_output_style(
327
+ self, style_name: str = "Claude MPM", is_fresh_install: bool = False
328
+ ) -> bool:
312
329
  """
313
330
  Update Claude Code settings to activate a specific output style.
314
331
 
332
+ Only activates the style if:
333
+ 1. No active style is currently set (first deployment), OR
334
+ 2. This is a fresh install (style file didn't exist before deployment)
335
+
336
+ This preserves user preferences if they've manually changed their active style.
337
+
315
338
  Args:
316
339
  style_name: Name of the style to activate (e.g., "Claude MPM", "Claude MPM Teacher")
340
+ is_fresh_install: Whether this is a fresh install (style file didn't exist before)
317
341
 
318
342
  Returns:
319
343
  True if activated successfully, False otherwise
@@ -332,8 +356,12 @@ class OutputStyleManager:
332
356
  # Check current active style
333
357
  current_style = settings.get("activeOutputStyle")
334
358
 
335
- # Update active output style if different
336
- if current_style != style_name:
359
+ # Only set activeOutputStyle if:
360
+ # 1. No active style is set (first deployment), OR
361
+ # 2. This is a fresh install (file didn't exist before deployment)
362
+ should_activate = current_style is None or is_fresh_install
363
+
364
+ if should_activate and current_style != style_name:
337
365
  settings["activeOutputStyle"] = style_name
338
366
 
339
367
  # Ensure settings directory exists
@@ -348,7 +376,10 @@ class OutputStyleManager:
348
376
  f"✅ Activated {style_name} output style (was: {current_style or 'none'})"
349
377
  )
350
378
  else:
351
- self.logger.debug(f"{style_name} output style already active")
379
+ self.logger.debug(
380
+ f"Preserving user preference: {current_style or 'none'} "
381
+ f"(skipping activation of {style_name})"
382
+ )
352
383
 
353
384
  return True
354
385
 
@@ -442,6 +473,10 @@ class OutputStyleManager:
442
473
  """
443
474
  results: Dict[str, bool] = {}
444
475
 
476
+ # Check if professional style exists BEFORE deployment
477
+ # This determines if this is a fresh install
478
+ professional_style_existed = self.styles["professional"]["target"].exists()
479
+
445
480
  for style_type_key in self.styles:
446
481
  # Deploy without activation
447
482
  # Cast is safe because we know self.styles keys are OutputStyleType
@@ -449,9 +484,11 @@ class OutputStyleManager:
449
484
  success = self.deploy_output_style(style=style_type, activate=False)
450
485
  results[style_type] = success
451
486
 
452
- # Activate the default style if requested
487
+ # Activate the default style if requested AND this is first deployment
453
488
  if activate_default and results.get("professional", False):
454
- self._activate_output_style("Claude MPM")
489
+ self._activate_output_style(
490
+ "Claude MPM", is_fresh_install=not professional_style_existed
491
+ )
455
492
 
456
493
  return results
457
494
 
@@ -73,15 +73,19 @@ class AgentConfig(BaseModel):
73
73
  )
74
74
 
75
75
  # Required agents that are always deployed
76
+ # Standard 7 core agents for essential PM workflow functionality
77
+ # These are auto-deployed when no agents are specified in configuration
76
78
  required: List[str] = Field(
77
79
  default_factory=lambda: [
78
- "research",
79
- "mpm-skills-manager",
80
- "mpm-agent-manager",
81
- "memory-manager-agent", # Specific agent ID (not generic "memory-manager")
82
- "ticketing",
80
+ "engineer", # General-purpose implementation
81
+ "research", # Codebase exploration and analysis
82
+ "qa", # Testing and quality assurance
83
+ "web-qa", # Browser-based testing specialist
84
+ "documentation", # Documentation generation
85
+ "ops", # Basic deployment operations
86
+ "ticketing", # Ticket tracking (essential for PM workflow)
83
87
  ],
84
- description="Agents that are always deployed (core system agents)",
88
+ description="Agents that are always deployed (standard 7 core agents)",
85
89
  )
86
90
 
87
91
  include_universal: bool = Field(
@@ -76,6 +76,7 @@ class DeploymentContext(Enum):
76
76
  EDITABLE_INSTALL = "editable_install"
77
77
  PIP_INSTALL = "pip_install"
78
78
  PIPX_INSTALL = "pipx_install"
79
+ UV_TOOLS = "uv_tools"
79
80
  SYSTEM_PACKAGE = "system_package"
80
81
 
81
82
 
@@ -190,113 +191,100 @@ class PathContext:
190
191
 
191
192
  Priority order:
192
193
  1. Environment variable override (CLAUDE_MPM_DEV_MODE)
193
- 2. Current working directory is a claude-mpm development project
194
- 3. Editable installation detection
195
- 4. Path-based detection (development, pipx, system, pip)
194
+ 2. Package installation path (uv tools, pipx, site-packages, editable)
195
+ 3. Current working directory (opt-in with CLAUDE_MPM_PREFER_LOCAL_SOURCE)
196
+
197
+ This ensures installed packages use their installation paths rather than
198
+ accidentally picking up development paths from CWD.
196
199
  """
197
- # Check for environment variable override
200
+ # 1. Explicit environment variable override
198
201
  if os.environ.get("CLAUDE_MPM_DEV_MODE", "").lower() in ("1", "true", "yes"):
199
202
  logger.debug(
200
203
  "Development mode forced via CLAUDE_MPM_DEV_MODE environment variable"
201
204
  )
202
205
  return DeploymentContext.DEVELOPMENT
203
206
 
204
- # Check if current working directory is a claude-mpm development project
205
- # This handles the case where pipx claude-mpm is run from within the dev directory
206
- cwd = _safe_cwd()
207
- current = cwd
208
- for _ in range(5): # Check up to 5 levels up from current directory
209
- if (current / "pyproject.toml").exists() and (
210
- current / "src" / "claude_mpm"
211
- ).exists():
212
- # Check if this is the claude-mpm project
213
- try:
214
- pyproject_content = (current / "pyproject.toml").read_text()
215
- if (
216
- 'name = "claude-mpm"' in pyproject_content
217
- or '"claude-mpm"' in pyproject_content
218
- ):
219
- logger.debug(
220
- f"Detected claude-mpm development directory at {current}"
221
- )
222
- logger.debug(
223
- "Using development mode for local source preference"
224
- )
225
- return DeploymentContext.DEVELOPMENT
226
- except Exception: # nosec B110
227
- pass
228
- if current == current.parent:
229
- break
230
- current = current.parent
231
-
207
+ # 2. Check where the actual package is installed
232
208
  try:
233
209
  import claude_mpm
234
210
 
235
211
  module_path = Path(claude_mpm.__file__).parent
212
+ package_str = str(module_path)
236
213
 
237
- # First check if this is an editable install, regardless of path
238
- # This is important for cases where pipx points to a development installation
239
- if PathContext._is_editable_install():
240
- logger.debug("Detected editable/development installation")
241
- # Check if we should use development paths
242
- # This could be because we're in a src/ directory or running from dev directory
243
- if module_path.parent.name == "src":
244
- return DeploymentContext.DEVELOPMENT
245
- if "pipx" in str(module_path):
246
- # Running via pipx but from within a development directory
247
- # Use development mode to prefer local source over pipx installation
248
- cwd = _safe_cwd()
249
- current = cwd
250
- for _ in range(5):
251
- if (current / "src" / "claude_mpm").exists() and (
252
- current / "pyproject.toml"
253
- ).exists():
254
- logger.debug(
255
- "Running pipx from development directory, using development mode"
256
- )
257
- return DeploymentContext.DEVELOPMENT
258
- if current == current.parent:
259
- break
260
- current = current.parent
261
- return DeploymentContext.EDITABLE_INSTALL
262
- return DeploymentContext.EDITABLE_INSTALL
214
+ # UV tools installation (~/.local/share/uv/tools/)
215
+ if "/.local/share/uv/tools/" in package_str:
216
+ logger.debug(f"Detected uv tools installation at {module_path}")
217
+ return DeploymentContext.UV_TOOLS
263
218
 
264
- # Check for development mode based on directory structure
265
- # module_path is typically /path/to/project/src/claude_mpm
266
- if (
267
- module_path.parent.name == "src"
268
- and (module_path.parent.parent / "src" / "claude_mpm").exists()
269
- ):
219
+ # pipx installation (~/.local/pipx/venvs/)
220
+ if "/.local/pipx/venvs/" in package_str or "/pipx/" in package_str:
221
+ logger.debug(f"Detected pipx installation at {module_path}")
222
+ return DeploymentContext.PIPX_INSTALL
223
+
224
+ # site-packages (pip install) - but not editable
225
+ if "/site-packages/" in package_str and "/src/" not in package_str:
226
+ logger.debug(f"Detected pip installation at {module_path}")
227
+ return DeploymentContext.PIP_INSTALL
228
+
229
+ # Editable install (pip install -e) - module in src/
230
+ if module_path.parent.name == "src":
231
+ # Check if this is truly an editable install
232
+ if PathContext._is_editable_install():
233
+ logger.debug(f"Detected editable installation at {module_path}")
234
+ return DeploymentContext.EDITABLE_INSTALL
235
+ # Module in src/ but not editable - development mode
270
236
  logger.debug(
271
237
  f"Detected development mode via directory structure at {module_path}"
272
238
  )
273
239
  return DeploymentContext.DEVELOPMENT
274
240
 
275
- # Check for pipx install
276
- if "pipx" in str(module_path):
277
- logger.debug(f"Detected pipx installation at {module_path}")
278
- return DeploymentContext.PIPX_INSTALL
279
-
280
- # Check for system package
281
- if "dist-packages" in str(module_path):
241
+ # dist-packages (system package manager)
242
+ if "dist-packages" in package_str:
282
243
  logger.debug(f"Detected system package installation at {module_path}")
283
244
  return DeploymentContext.SYSTEM_PACKAGE
284
245
 
285
- # Check for site-packages (could be pip or editable)
286
- if "site-packages" in str(module_path):
287
- # Already checked for editable above, so this is a regular pip install
288
- logger.debug(f"Detected pip installation at {module_path}")
289
- return DeploymentContext.PIP_INSTALL
290
-
291
- # Default to pip install
246
+ # Default to pip install for any other installation
292
247
  logger.debug(f"Defaulting to pip installation for {module_path}")
293
248
  return DeploymentContext.PIP_INSTALL
294
249
 
295
250
  except ImportError:
296
251
  logger.debug(
297
- "ImportError during context detection, defaulting to development"
252
+ "ImportError during module path detection, checking CWD as fallback"
298
253
  )
299
- return DeploymentContext.DEVELOPMENT
254
+
255
+ # 3. CWD-based detection (OPT-IN ONLY for explicit development work)
256
+ # Only use CWD if explicitly requested or no package installation found
257
+ if os.environ.get("CLAUDE_MPM_PREFER_LOCAL_SOURCE", "").lower() in (
258
+ "1",
259
+ "true",
260
+ "yes",
261
+ ):
262
+ cwd = _safe_cwd()
263
+ current = cwd
264
+ for _ in range(5): # Check up to 5 levels up from current directory
265
+ if (current / "pyproject.toml").exists() and (
266
+ current / "src" / "claude_mpm"
267
+ ).exists():
268
+ # Check if this is the claude-mpm project
269
+ try:
270
+ pyproject_content = (current / "pyproject.toml").read_text()
271
+ if (
272
+ 'name = "claude-mpm"' in pyproject_content
273
+ or '"claude-mpm"' in pyproject_content
274
+ ):
275
+ logger.debug(
276
+ f"CLAUDE_MPM_PREFER_LOCAL_SOURCE: Using development directory at {current}"
277
+ )
278
+ return DeploymentContext.DEVELOPMENT
279
+ except Exception: # nosec B110
280
+ pass
281
+ if current == current.parent:
282
+ break
283
+ current = current.parent
284
+
285
+ # Final fallback: assume development mode
286
+ logger.debug("No installation detected, defaulting to development mode")
287
+ return DeploymentContext.DEVELOPMENT
300
288
 
301
289
 
302
290
  class UnifiedPathManager:
@@ -58,8 +58,9 @@ class CLIContext:
58
58
  else "%(message)s"
59
59
  )
60
60
 
61
+ # MUST use stderr to avoid corrupting hook JSON output
61
62
  logging.basicConfig(
62
- level=level, format=format_str, handlers=[logging.StreamHandler(sys.stdout)]
63
+ level=level, format=format_str, handlers=[logging.StreamHandler(sys.stderr)]
63
64
  )
64
65
  self.debug = debug
65
66