claude-mpm 5.6.4__py3-none-any.whl → 5.6.30__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 (103) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/cli/commands/commander.py +174 -4
  4. claude_mpm/cli/commands/skill_source.py +51 -2
  5. claude_mpm/cli/commands/skills.py +5 -3
  6. claude_mpm/cli/parsers/commander_parser.py +43 -10
  7. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  8. claude_mpm/cli/parsers/skills_parser.py +5 -0
  9. claude_mpm/cli/startup.py +140 -20
  10. claude_mpm/cli/startup_display.py +2 -1
  11. claude_mpm/commander/__init__.py +6 -0
  12. claude_mpm/commander/adapters/__init__.py +32 -3
  13. claude_mpm/commander/adapters/auggie.py +260 -0
  14. claude_mpm/commander/adapters/base.py +98 -1
  15. claude_mpm/commander/adapters/claude_code.py +32 -1
  16. claude_mpm/commander/adapters/codex.py +237 -0
  17. claude_mpm/commander/adapters/example_usage.py +310 -0
  18. claude_mpm/commander/adapters/mpm.py +389 -0
  19. claude_mpm/commander/adapters/registry.py +204 -0
  20. claude_mpm/commander/api/app.py +32 -16
  21. claude_mpm/commander/api/errors.py +21 -0
  22. claude_mpm/commander/api/routes/messages.py +11 -11
  23. claude_mpm/commander/api/routes/projects.py +20 -20
  24. claude_mpm/commander/api/routes/sessions.py +37 -26
  25. claude_mpm/commander/api/routes/work.py +86 -50
  26. claude_mpm/commander/api/schemas.py +4 -0
  27. claude_mpm/commander/chat/cli.py +42 -3
  28. claude_mpm/commander/config.py +5 -3
  29. claude_mpm/commander/core/__init__.py +10 -0
  30. claude_mpm/commander/core/block_manager.py +325 -0
  31. claude_mpm/commander/core/response_manager.py +323 -0
  32. claude_mpm/commander/daemon.py +215 -10
  33. claude_mpm/commander/env_loader.py +59 -0
  34. claude_mpm/commander/frameworks/base.py +4 -1
  35. claude_mpm/commander/instance_manager.py +124 -11
  36. claude_mpm/commander/memory/__init__.py +45 -0
  37. claude_mpm/commander/memory/compression.py +347 -0
  38. claude_mpm/commander/memory/embeddings.py +230 -0
  39. claude_mpm/commander/memory/entities.py +310 -0
  40. claude_mpm/commander/memory/example_usage.py +290 -0
  41. claude_mpm/commander/memory/integration.py +325 -0
  42. claude_mpm/commander/memory/search.py +381 -0
  43. claude_mpm/commander/memory/store.py +657 -0
  44. claude_mpm/commander/registry.py +10 -4
  45. claude_mpm/commander/runtime/monitor.py +32 -2
  46. claude_mpm/commander/work/executor.py +38 -20
  47. claude_mpm/commander/workflow/event_handler.py +25 -3
  48. claude_mpm/config/skill_sources.py +16 -0
  49. claude_mpm/core/claude_runner.py +152 -0
  50. claude_mpm/core/config.py +30 -22
  51. claude_mpm/core/config_constants.py +74 -9
  52. claude_mpm/core/constants.py +56 -12
  53. claude_mpm/core/interactive_session.py +5 -4
  54. claude_mpm/core/logging_utils.py +4 -2
  55. claude_mpm/core/network_config.py +148 -0
  56. claude_mpm/core/oneshot_session.py +7 -6
  57. claude_mpm/core/output_style_manager.py +37 -7
  58. claude_mpm/core/socketio_pool.py +13 -5
  59. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  68. claude_mpm/hooks/claude_hooks/event_handlers.py +22 -0
  69. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -6
  70. claude_mpm/hooks/claude_hooks/installer.py +43 -2
  71. claude_mpm/hooks/claude_hooks/memory_integration.py +31 -22
  72. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  76. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  79. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  80. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  81. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  82. claude_mpm/hooks/session_resume_hook.py +22 -18
  83. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  84. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  85. claude_mpm/scripts/claude-hook-handler.sh +8 -8
  86. claude_mpm/services/agents/agent_selection_service.py +2 -2
  87. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  88. claude_mpm/services/command_deployment_service.py +44 -26
  89. claude_mpm/services/pm_skills_deployer.py +3 -2
  90. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  91. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  92. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  93. claude_mpm/services/skills_deployer.py +31 -5
  94. claude_mpm/skills/__init__.py +2 -1
  95. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  96. claude_mpm/skills/registry.py +295 -90
  97. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/METADATA +5 -3
  98. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/RECORD +103 -71
  99. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/WHEEL +0 -0
  100. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/entry_points.txt +0 -0
  101. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE +0 -0
  102. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  103. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/top_level.txt +0 -0
@@ -22,7 +22,7 @@ USAGE:
22
22
  threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
23
23
  if threshold_crossed:
24
24
  warning = auto_pause.emit_threshold_warning(threshold_crossed)
25
- print(f"\n⚠️ {warning}", file=sys.stderr)
25
+ _log(f"\n⚠️ {warning}")
26
26
 
27
27
  # Record actions during pause mode
28
28
  if auto_pause.is_pause_active():
@@ -34,7 +34,6 @@ USAGE:
34
34
  """
35
35
 
36
36
  import os
37
- import sys
38
37
  from datetime import datetime, timezone
39
38
  from pathlib import Path
40
39
  from typing import Any, Dict, Optional
@@ -45,10 +44,19 @@ from claude_mpm.services.infrastructure.context_usage_tracker import (
45
44
  ContextUsageTracker,
46
45
  )
47
46
 
47
+ # Try to import _log from hook_handler, fall back to no-op
48
+ try:
49
+ from claude_mpm.hooks.claude_hooks.hook_handler import _log
50
+ except ImportError:
51
+
52
+ def _log(msg: str) -> None:
53
+ pass # Silent fallback
54
+
55
+
48
56
  logger = get_logger(__name__)
49
57
 
50
58
  # Debug mode
51
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
59
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
52
60
 
53
61
  # Warning messages for threshold crossings
54
62
  THRESHOLD_WARNINGS = {
@@ -100,11 +108,10 @@ class AutoPauseHandler:
100
108
  self._previous_threshold = current_state.threshold_reached
101
109
 
102
110
  if DEBUG:
103
- print(
111
+ _log(
104
112
  f"AutoPauseHandler initialized: "
105
113
  f"{current_state.percentage_used:.1f}% context used, "
106
- f"threshold: {current_state.threshold_reached}",
107
- file=sys.stderr,
114
+ f"threshold: {current_state.threshold_reached}"
108
115
  )
109
116
  except Exception as e:
110
117
  logger.error(f"Failed to initialize AutoPauseHandler: {e}")
@@ -169,10 +176,9 @@ class AutoPauseHandler:
169
176
  self._previous_threshold = current_threshold
170
177
 
171
178
  if DEBUG:
172
- print(
179
+ _log(
173
180
  f"Context threshold crossed: {current_threshold} "
174
- f"({state.percentage_used:.1f}%)",
175
- file=sys.stderr,
181
+ f"({state.percentage_used:.1f}%)"
176
182
  )
177
183
 
178
184
  # Trigger auto-pause if threshold reached
@@ -184,7 +190,7 @@ class AutoPauseHandler:
184
190
  except Exception as e:
185
191
  logger.error(f"Failed to update usage: {e}")
186
192
  if DEBUG:
187
- print(f"❌ Usage update failed: {e}", file=sys.stderr)
193
+ _log(f"❌ Usage update failed: {e}")
188
194
  # Don't propagate error - auto-pause is optional
189
195
  return None
190
196
 
@@ -220,12 +226,12 @@ class AutoPauseHandler:
220
226
  )
221
227
 
222
228
  if DEBUG:
223
- print(f"Recorded tool call during pause: {tool_name}", file=sys.stderr)
229
+ _log(f"Recorded tool call during pause: {tool_name}")
224
230
 
225
231
  except Exception as e:
226
232
  logger.error(f"Failed to record tool call: {e}")
227
233
  if DEBUG:
228
- print(f"❌ Failed to record tool call: {e}", file=sys.stderr)
234
+ _log(f"❌ Failed to record tool call: {e}")
229
235
 
230
236
  def on_assistant_response(self, response_summary: str) -> None:
231
237
  """Record an assistant response if auto-pause is active.
@@ -257,15 +263,14 @@ class AutoPauseHandler:
257
263
  )
258
264
 
259
265
  if DEBUG:
260
- print(
261
- f"Recorded assistant response during pause (length: {len(summary)})",
262
- file=sys.stderr,
266
+ _log(
267
+ f"Recorded assistant response during pause (length: {len(summary)})"
263
268
  )
264
269
 
265
270
  except Exception as e:
266
271
  logger.error(f"Failed to record assistant response: {e}")
267
272
  if DEBUG:
268
- print(f"❌ Failed to record assistant response: {e}", file=sys.stderr)
273
+ _log(f"❌ Failed to record assistant response: {e}")
269
274
 
270
275
  def on_user_message(self, message_summary: str) -> None:
271
276
  """Record a user message if auto-pause is active.
@@ -297,15 +302,12 @@ class AutoPauseHandler:
297
302
  )
298
303
 
299
304
  if DEBUG:
300
- print(
301
- f"Recorded user message during pause (length: {len(summary)})",
302
- file=sys.stderr,
303
- )
305
+ _log(f"Recorded user message during pause (length: {len(summary)})")
304
306
 
305
307
  except Exception as e:
306
308
  logger.error(f"Failed to record user message: {e}")
307
309
  if DEBUG:
308
- print(f"❌ Failed to record user message: {e}", file=sys.stderr)
310
+ _log(f"❌ Failed to record user message: {e}")
309
311
 
310
312
  def on_session_end(self) -> Optional[Path]:
311
313
  """Called when session ends. Finalizes any active pause.
@@ -318,7 +320,7 @@ class AutoPauseHandler:
318
320
  """
319
321
  if not self.is_pause_active():
320
322
  if DEBUG:
321
- print("No active pause to finalize", file=sys.stderr)
323
+ _log("No active pause to finalize")
322
324
  return None
323
325
 
324
326
  try:
@@ -326,14 +328,14 @@ class AutoPauseHandler:
326
328
  session_path = self.pause_manager.finalize_pause(create_full_snapshot=True)
327
329
 
328
330
  if session_path and DEBUG:
329
- print(f"✅ Session finalized: {session_path.name}", file=sys.stderr)
331
+ _log(f"✅ Session finalized: {session_path.name}")
330
332
 
331
333
  return session_path
332
334
 
333
335
  except Exception as e:
334
336
  logger.error(f"Failed to finalize pause session: {e}")
335
337
  if DEBUG:
336
- print(f"❌ Failed to finalize pause: {e}", file=sys.stderr)
338
+ _log(f"❌ Failed to finalize pause: {e}")
337
339
  raise
338
340
 
339
341
  def is_pause_active(self) -> bool:
@@ -417,9 +419,7 @@ class AutoPauseHandler:
417
419
  # Check if pause is already active
418
420
  if self.is_pause_active():
419
421
  if DEBUG:
420
- print(
421
- "Auto-pause already active, skipping trigger", file=sys.stderr
422
- )
422
+ _log("Auto-pause already active, skipping trigger")
423
423
  return
424
424
 
425
425
  # Start incremental pause
@@ -429,16 +429,15 @@ class AutoPauseHandler:
429
429
  )
430
430
 
431
431
  if DEBUG:
432
- print(
432
+ _log(
433
433
  f"✅ Auto-pause triggered: {session_id} "
434
- f"({state.percentage_used:.1f}% context used)",
435
- file=sys.stderr,
434
+ f"({state.percentage_used:.1f}% context used)"
436
435
  )
437
436
 
438
437
  except Exception as e:
439
438
  logger.error(f"Failed to trigger auto-pause: {e}")
440
439
  if DEBUG:
441
- print(f"❌ Failed to trigger auto-pause: {e}", file=sys.stderr)
440
+ _log(f"❌ Failed to trigger auto-pause: {e}")
442
441
  # Don't propagate - auto-pause is optional
443
442
 
444
443
  def _summarize_dict(
@@ -126,6 +126,15 @@ class EventHandlers:
126
126
  # Response tracking is optional - silently continue if it fails
127
127
  pass
128
128
 
129
+ # Record user message for auto-pause if active
130
+ auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
131
+ if auto_pause and auto_pause.is_pause_active():
132
+ try:
133
+ auto_pause.on_user_message(prompt)
134
+ except Exception as e:
135
+ if DEBUG:
136
+ _log(f"Auto-pause user message recording error: {e}")
137
+
129
138
  # Emit normalized event (namespace no longer needed with normalized events)
130
139
  self.hook_handler._emit_socketio_event("", "user_prompt", prompt_data)
131
140
 
@@ -603,6 +612,19 @@ class EventHandlers:
603
612
  if DEBUG:
604
613
  _log(f"Auto-pause error in handle_stop_fast: {e}")
605
614
 
615
+ # Finalize pause session if active
616
+ try:
617
+ if auto_pause.is_pause_active():
618
+ session_file = auto_pause.on_session_end()
619
+ if session_file:
620
+ if DEBUG:
621
+ _log(
622
+ f"✅ Auto-pause session finalized: {session_file.name}"
623
+ )
624
+ except Exception as e:
625
+ if DEBUG:
626
+ _log(f"❌ Failed to finalize auto-pause session: {e}")
627
+
606
628
  # Track response if enabled
607
629
  try:
608
630
  rtm = getattr(self.hook_handler, "response_tracking_manager", None)
@@ -496,11 +496,11 @@ class ClaudeHookHandler:
496
496
  if modified_input is not None:
497
497
  # Claude Code v2.0.30+ supports modifying PreToolUse tool inputs
498
498
  print(
499
- json.dumps({"action": "continue", "tool_input": modified_input}),
499
+ json.dumps({"continue": True, "tool_input": modified_input}),
500
500
  flush=True,
501
501
  )
502
502
  else:
503
- print(json.dumps({"action": "continue"}), flush=True)
503
+ print(json.dumps({"continue": True}), flush=True)
504
504
 
505
505
  # Delegation methods for compatibility with event_handlers
506
506
  def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
@@ -673,7 +673,7 @@ def main():
673
673
  # This prevents errors on older Claude Code versions
674
674
  if version:
675
675
  _log(f"Skipping hook processing due to version incompatibility ({version})")
676
- print(json.dumps({"action": "continue"}), flush=True)
676
+ print(json.dumps({"continue": True}), flush=True)
677
677
  sys.exit(0)
678
678
 
679
679
  def cleanup_handler(signum=None, frame=None):
@@ -682,7 +682,7 @@ def main():
682
682
  _log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
683
683
  # Only output continue if we haven't already (i.e., if interrupted by signal)
684
684
  if signum is not None and not _continue_printed:
685
- print(json.dumps({"action": "continue"}), flush=True)
685
+ print(json.dumps({"continue": True}), flush=True)
686
686
  _continue_printed = True
687
687
  sys.exit(0)
688
688
 
@@ -715,7 +715,7 @@ def main():
715
715
  except Exception as e:
716
716
  # Only output continue if not already printed
717
717
  if not _continue_printed:
718
- print(json.dumps({"action": "continue"}), flush=True)
718
+ print(json.dumps({"continue": True}), flush=True)
719
719
  _continue_printed = True
720
720
  # Log error for debugging
721
721
  _log(f"Hook handler error: {e}")
@@ -727,5 +727,5 @@ if __name__ == "__main__":
727
727
  main()
728
728
  except Exception:
729
729
  # Catastrophic failure (import error, etc.) - always output valid JSON
730
- print(json.dumps({"action": "continue"}), flush=True)
730
+ print(json.dumps({"continue": True}), flush=True)
731
731
  sys.exit(0)
@@ -149,7 +149,7 @@ main() {
149
149
  if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
150
150
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Claude MPM not found, continuing..." >> /tmp/claude-mpm-hook.log
151
151
  fi
152
- echo '{"action": "continue"}'
152
+ echo '{"continue": true}'
153
153
  exit 0
154
154
  fi
155
155
 
@@ -176,7 +176,7 @@ main() {
176
176
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/claude-mpm-hook-error.log" >> /tmp/claude-mpm-hook.log
177
177
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Error: $(cat /tmp/claude-mpm-hook-error.log 2>/dev/null | head -5)" >> /tmp/claude-mpm-hook.log
178
178
  fi
179
- echo '{"action": "continue"}'
179
+ echo '{"continue": true}'
180
180
  exit 0
181
181
  fi
182
182
 
@@ -537,6 +537,44 @@ main "$@"
537
537
  except Exception as e:
538
538
  self.logger.warning(f"Could not clean up old settings file: {e}")
539
539
 
540
+ def _fix_status_line(self, settings: Dict) -> None:
541
+ """Fix statusLine command to handle both output style schema formats.
542
+
543
+ The statusLine command receives input in different formats:
544
+ - Newer format: {"activeOutputStyle": "Claude MPM", ...}
545
+ - Older format: {"output_style": {"name": "Claude MPM"}, ...}
546
+
547
+ This method ensures the jq expression checks both locations.
548
+
549
+ Args:
550
+ settings: The settings dictionary to update
551
+ """
552
+ if "statusLine" not in settings:
553
+ return
554
+
555
+ status_line = settings.get("statusLine", {})
556
+ if "command" not in status_line:
557
+ return
558
+
559
+ command = status_line["command"]
560
+
561
+ # Pattern to match: '.output_style.name // "default"'
562
+ # We need to update it to: '.output_style.name // .activeOutputStyle // "default"'
563
+ old_pattern = r'\.output_style\.name\s*//\s*"default"'
564
+ new_pattern = '.output_style.name // .activeOutputStyle // "default"'
565
+
566
+ # Check if the command needs updating
567
+ if re.search(old_pattern, command) and ".activeOutputStyle" not in command:
568
+ updated_command = re.sub(old_pattern, new_pattern, command)
569
+ settings["statusLine"]["command"] = updated_command
570
+ self.logger.info(
571
+ "Fixed statusLine command to handle both output style schemas"
572
+ )
573
+ else:
574
+ self.logger.debug(
575
+ "StatusLine command already supports both schemas or not present"
576
+ )
577
+
540
578
  def _update_claude_settings(self, hook_script_path: Path) -> None:
541
579
  """Update Claude settings to use the installed hook."""
542
580
  self.logger.info("Updating Claude settings...")
@@ -598,6 +636,9 @@ main "$@"
598
636
  }
599
637
  ]
600
638
 
639
+ # Fix statusLine command to handle both output style schemas
640
+ self._fix_status_line(settings)
641
+
601
642
  # Write settings to settings.json
602
643
  with self.settings_file.open("w") as f:
603
644
  json.dump(settings, f, indent=2)
@@ -7,7 +7,16 @@ including pre and post delegation hooks.
7
7
 
8
8
  import logging
9
9
  import os
10
- import sys
10
+ from pathlib import Path
11
+
12
+ # Try to import _log from hook_handler, fall back to no-op
13
+ try:
14
+ from claude_mpm.hooks.claude_hooks.hook_handler import _log
15
+ except ImportError:
16
+
17
+ def _log(msg: str) -> None:
18
+ pass # Silent fallback
19
+
11
20
 
12
21
  # Install-type-aware logging configuration BEFORE kuzu-memory imports
13
22
  # This overrides kuzu-memory's WARNING-level basicConfig (fixes 1M-445)
@@ -28,11 +37,15 @@ try:
28
37
  DeploymentContext.DEVELOPMENT,
29
38
  DeploymentContext.EDITABLE_INSTALL,
30
39
  ):
40
+ # Write logs to file instead of stderr to avoid hook errors
41
+ log_dir = Path.home() / ".claude-mpm"
42
+ log_dir.mkdir(parents=True, exist_ok=True)
43
+ log_file = log_dir / "hooks.log"
31
44
  logging.basicConfig(
32
45
  level=logging.INFO,
33
46
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
34
47
  force=True, # Python 3.8+ - reconfigures root logger
35
- stream=sys.stderr,
48
+ filename=str(log_file),
36
49
  )
37
50
  except ImportError:
38
51
  # Fallback: if unified_paths not available, check suppression before configuring
@@ -40,17 +53,21 @@ except ImportError:
40
53
  is_suppressed = root_logger.level > logging.CRITICAL
41
54
 
42
55
  if not is_suppressed:
56
+ # Write logs to file instead of stderr to avoid hook errors
57
+ log_dir = Path.home() / ".claude-mpm"
58
+ log_dir.mkdir(parents=True, exist_ok=True)
59
+ log_file = log_dir / "hooks.log"
43
60
  logging.basicConfig(
44
61
  level=logging.INFO,
45
62
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
46
63
  force=True,
47
- stream=sys.stderr,
64
+ filename=str(log_file),
48
65
  )
49
66
  from datetime import datetime, timezone
50
67
  from typing import Optional
51
68
 
52
69
  # Debug mode
53
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
70
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
54
71
 
55
72
  # Memory hooks integration
56
73
  MEMORY_HOOKS_AVAILABLE = False
@@ -71,7 +88,7 @@ try:
71
88
  except Exception as e:
72
89
  # Catch all exceptions to prevent any import errors from breaking the handler
73
90
  if DEBUG:
74
- print(f"Memory hooks not available: {e}", file=sys.stderr)
91
+ _log(f"Memory hooks not available: {e}")
75
92
  MEMORY_HOOKS_AVAILABLE = False
76
93
 
77
94
 
@@ -105,10 +122,7 @@ class MemoryHookManager:
105
122
  # Only initialize if memory system is enabled
106
123
  if not config.get("memory.enabled", True):
107
124
  if DEBUG:
108
- print(
109
- "Memory system disabled - skipping hook initialization",
110
- file=sys.stderr,
111
- )
125
+ _log("Memory system disabled - skipping hook initialization")
112
126
  return
113
127
 
114
128
  # Initialize pre-delegation hook for memory injection
@@ -126,14 +140,11 @@ class MemoryHookManager:
126
140
  hooks_info.append("pre-delegation")
127
141
  if self.post_delegation_hook:
128
142
  hooks_info.append("post-delegation")
129
- print(
130
- f"✅ Memory hooks initialized: {', '.join(hooks_info)}",
131
- file=sys.stderr,
132
- )
143
+ _log(f"✅ Memory hooks initialized: {', '.join(hooks_info)}")
133
144
 
134
145
  except Exception as e:
135
146
  if DEBUG:
136
- print(f"❌ Failed to initialize memory hooks: {e}", file=sys.stderr)
147
+ _log(f"❌ Failed to initialize memory hooks: {e}")
137
148
  # Don't fail the entire handler - memory system is optional
138
149
 
139
150
  def trigger_pre_delegation_hook(
@@ -182,14 +193,13 @@ class MemoryHookManager:
182
193
 
183
194
  if DEBUG:
184
195
  memory_size = len(memory_section.encode("utf-8"))
185
- print(
186
- f"✅ Injected {memory_size} bytes of memory for agent '{agent_type}'",
187
- file=sys.stderr,
196
+ _log(
197
+ f"✅ Injected {memory_size} bytes of memory for agent '{agent_type}'"
188
198
  )
189
199
 
190
200
  except Exception as e:
191
201
  if DEBUG:
192
- print(f"❌ Memory pre-delegation hook failed: {e}", file=sys.stderr)
202
+ _log(f"❌ Memory pre-delegation hook failed: {e}")
193
203
  # Don't fail the delegation - memory is optional
194
204
 
195
205
  def trigger_post_delegation_hook(
@@ -249,12 +259,11 @@ class MemoryHookManager:
249
259
  if result.success and result.metadata:
250
260
  learnings_extracted = result.metadata.get("learnings_extracted", 0)
251
261
  if learnings_extracted > 0 and DEBUG:
252
- print(
253
- f"✅ Extracted {learnings_extracted} learnings for agent '{agent_type}'",
254
- file=sys.stderr,
262
+ _log(
263
+ f"✅ Extracted {learnings_extracted} learnings for agent '{agent_type}'"
255
264
  )
256
265
 
257
266
  except Exception as e:
258
267
  if DEBUG:
259
- print(f"❌ Memory post-delegation hook failed: {e}", file=sys.stderr)
268
+ _log(f"❌ Memory post-delegation hook failed: {e}")
260
269
  # Don't fail the delegation result - memory is optional