claude-mpm 5.4.96__py3-none-any.whl → 5.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +36 -7
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +45 -5
- claude_mpm/cli/commands/commander.py +46 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/executor.py +32 -17
- claude_mpm/cli/parsers/base_parser.py +17 -0
- claude_mpm/cli/parsers/commander_parser.py +83 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/startup.py +20 -2
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +72 -0
- claude_mpm/commander/adapters/__init__.py +31 -0
- claude_mpm/commander/adapters/base.py +191 -0
- claude_mpm/commander/adapters/claude_code.py +361 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +105 -0
- claude_mpm/commander/api/errors.py +112 -0
- claude_mpm/commander/api/routes/__init__.py +8 -0
- claude_mpm/commander/api/routes/events.py +184 -0
- claude_mpm/commander/api/routes/inbox.py +171 -0
- claude_mpm/commander/api/routes/messages.py +148 -0
- claude_mpm/commander/api/routes/projects.py +271 -0
- claude_mpm/commander/api/routes/sessions.py +215 -0
- claude_mpm/commander/api/routes/work.py +260 -0
- claude_mpm/commander/api/schemas.py +182 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +107 -0
- claude_mpm/commander/chat/commands.py +96 -0
- claude_mpm/commander/chat/repl.py +310 -0
- claude_mpm/commander/config.py +49 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/daemon.py +398 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +332 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +143 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +62 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +337 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +121 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +309 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +404 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +316 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +361 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +189 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +219 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +8 -0
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/core/config.py +5 -0
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +35 -11
- claude_mpm/core/output_style_manager.py +15 -5
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +39 -1
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +75 -8
- claude_mpm/hooks/claude_hooks/memory_integration.py +22 -11
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
- claude_mpm/scripts/claude-hook-handler.sh +39 -12
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/event_log.py +8 -0
- claude_mpm/services/pm_skills_deployer.py +84 -6
- claude_mpm/services/skills/git_skill_source_manager.py +51 -2
- claude_mpm/services/skills/skill_discovery_service.py +57 -3
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/METADATA +18 -4
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/RECORD +140 -68
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/top_level.txt +0 -0
claude_mpm/core/unified_paths.py
CHANGED
|
@@ -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.
|
|
194
|
-
3.
|
|
195
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
#
|
|
276
|
-
if "
|
|
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
|
-
#
|
|
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
|
|
252
|
+
"ImportError during module path detection, checking CWD as fallback"
|
|
298
253
|
)
|
|
299
|
-
|
|
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.
|
|
63
|
+
level=level, format=format_str, handlers=[logging.StreamHandler(sys.stderr)]
|
|
63
64
|
)
|
|
64
65
|
self.debug = debug
|
|
65
66
|
|
|
@@ -598,7 +598,11 @@ class EventHandlers:
|
|
|
598
598
|
threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
|
|
599
599
|
if threshold_crossed:
|
|
600
600
|
warning = auto_pause.emit_threshold_warning(threshold_crossed)
|
|
601
|
-
|
|
601
|
+
# CRITICAL: Never write to stderr unconditionally - causes hook errors
|
|
602
|
+
# Use _log() instead which only writes to file if DEBUG=true
|
|
603
|
+
from . import _log
|
|
604
|
+
|
|
605
|
+
_log(f"⚠️ Auto-pause threshold crossed: {warning}")
|
|
602
606
|
|
|
603
607
|
if DEBUG:
|
|
604
608
|
print(
|
|
@@ -949,6 +953,7 @@ class EventHandlers:
|
|
|
949
953
|
- Provides visibility into new conversation sessions
|
|
950
954
|
- Enables tracking of session lifecycle and duration
|
|
951
955
|
- Useful for monitoring concurrent sessions and resource usage
|
|
956
|
+
- Auto-inject pending autotodos if enabled in config
|
|
952
957
|
"""
|
|
953
958
|
session_id = event.get("session_id", "")
|
|
954
959
|
working_dir = event.get("cwd", "")
|
|
@@ -962,6 +967,39 @@ class EventHandlers:
|
|
|
962
967
|
"hook_event_name": "SessionStart",
|
|
963
968
|
}
|
|
964
969
|
|
|
970
|
+
# Auto-inject pending autotodos if enabled
|
|
971
|
+
try:
|
|
972
|
+
from pathlib import Path
|
|
973
|
+
|
|
974
|
+
from claude_mpm.cli.commands.autotodos import get_pending_todos
|
|
975
|
+
from claude_mpm.core.config import Config
|
|
976
|
+
|
|
977
|
+
config = Config()
|
|
978
|
+
auto_inject_enabled = config.get("autotodos.auto_inject_on_startup", True)
|
|
979
|
+
max_todos = config.get("autotodos.max_todos_per_session", 10)
|
|
980
|
+
|
|
981
|
+
if auto_inject_enabled:
|
|
982
|
+
# Pass working directory from event to avoid Path.cwd() issues
|
|
983
|
+
working_dir_param = None
|
|
984
|
+
if working_dir:
|
|
985
|
+
working_dir_param = Path(working_dir)
|
|
986
|
+
|
|
987
|
+
pending_todos = get_pending_todos(
|
|
988
|
+
max_todos=max_todos, working_dir=working_dir_param
|
|
989
|
+
)
|
|
990
|
+
if pending_todos:
|
|
991
|
+
session_start_data["pending_autotodos"] = pending_todos
|
|
992
|
+
session_start_data["autotodos_count"] = len(pending_todos)
|
|
993
|
+
if DEBUG:
|
|
994
|
+
print(
|
|
995
|
+
f" - Auto-injected {len(pending_todos)} pending autotodos",
|
|
996
|
+
file=sys.stderr,
|
|
997
|
+
)
|
|
998
|
+
except Exception as e: # nosec B110
|
|
999
|
+
# Auto-injection is optional - continue if it fails
|
|
1000
|
+
if DEBUG:
|
|
1001
|
+
print(f" - Failed to auto-inject autotodos: {e}", file=sys.stderr)
|
|
1002
|
+
|
|
965
1003
|
# Debug logging
|
|
966
1004
|
if DEBUG:
|
|
967
1005
|
print(
|
|
@@ -22,7 +22,7 @@ import os
|
|
|
22
22
|
import re
|
|
23
23
|
import select
|
|
24
24
|
import signal
|
|
25
|
-
import subprocess
|
|
25
|
+
import subprocess # nosec B404
|
|
26
26
|
import sys
|
|
27
27
|
import threading
|
|
28
28
|
from datetime import datetime, timezone
|
|
@@ -62,14 +62,31 @@ except ImportError:
|
|
|
62
62
|
"""
|
|
63
63
|
Debug mode configuration for hook processing.
|
|
64
64
|
|
|
65
|
-
WHY
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
WHY disabled by default: Production users should see clean output without debug noise.
|
|
66
|
+
Hook errors appear less confusing when debug output is minimal.
|
|
67
|
+
Development and debugging can enable via CLAUDE_MPM_HOOK_DEBUG=true.
|
|
68
68
|
|
|
69
69
|
Performance Impact: Debug logging adds ~5-10% overhead but provides crucial
|
|
70
|
-
visibility into event flow, timing, and error conditions.
|
|
70
|
+
visibility into event flow, timing, and error conditions when enabled.
|
|
71
71
|
"""
|
|
72
|
-
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "
|
|
72
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _log(message: str) -> None:
|
|
76
|
+
"""Log message to file if DEBUG enabled. Never write to stderr.
|
|
77
|
+
|
|
78
|
+
WHY: Claude Code interprets ANY stderr output as a hook error.
|
|
79
|
+
Writing to stderr causes confusing "hook error" messages even for debug logs.
|
|
80
|
+
|
|
81
|
+
This helper ensures all debug output goes to a log file instead.
|
|
82
|
+
"""
|
|
83
|
+
if DEBUG:
|
|
84
|
+
try:
|
|
85
|
+
with open("/tmp/claude-mpm-hook.log", "a") as f: # nosec B108
|
|
86
|
+
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {message}\n")
|
|
87
|
+
except Exception: # nosec B110 - intentional silent failure
|
|
88
|
+
pass # Never disrupt hook execution
|
|
89
|
+
|
|
73
90
|
|
|
74
91
|
"""
|
|
75
92
|
Conditional imports with graceful fallbacks for testing and modularity.
|
|
@@ -111,6 +128,8 @@ WHY version checking:
|
|
|
111
128
|
Security: Version checking prevents execution on incompatible environments.
|
|
112
129
|
"""
|
|
113
130
|
MIN_CLAUDE_VERSION = "1.0.92"
|
|
131
|
+
# Minimum version for user-invocable skills support
|
|
132
|
+
MIN_SKILLS_VERSION = "2.1.3"
|
|
114
133
|
|
|
115
134
|
|
|
116
135
|
def check_claude_version() -> Tuple[bool, Optional[str]]:
|
|
@@ -155,7 +174,7 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
|
|
|
155
174
|
"""
|
|
156
175
|
try:
|
|
157
176
|
# Try to detect Claude Code version
|
|
158
|
-
result = subprocess.run( # nosec B603 - Safe: hardcoded claude CLI with --version flag, no user input
|
|
177
|
+
result = subprocess.run( # nosec B603 B607 - Safe: hardcoded claude CLI with --version flag, no user input
|
|
159
178
|
["claude", "--version"],
|
|
160
179
|
capture_output=True,
|
|
161
180
|
text=True,
|
|
@@ -186,22 +205,17 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
|
|
|
186
205
|
req_part = required[i] if i < len(required) else 0
|
|
187
206
|
|
|
188
207
|
if curr_part < req_part:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
file=sys.stderr,
|
|
194
|
-
)
|
|
208
|
+
_log(
|
|
209
|
+
f"⚠️ Claude Code {version} does not support matcher-based hooks "
|
|
210
|
+
f"(requires {MIN_CLAUDE_VERSION}+). Hook monitoring disabled."
|
|
211
|
+
)
|
|
195
212
|
return False, version
|
|
196
213
|
if curr_part > req_part:
|
|
197
214
|
return True, version
|
|
198
215
|
|
|
199
216
|
return True, version
|
|
200
217
|
except Exception as e:
|
|
201
|
-
|
|
202
|
-
print(
|
|
203
|
-
f"Warning: Could not detect Claude Code version: {e}", file=sys.stderr
|
|
204
|
-
)
|
|
218
|
+
_log(f"Warning: Could not detect Claude Code version: {e}")
|
|
205
219
|
|
|
206
220
|
return False, None
|
|
207
221
|
|
|
@@ -242,11 +256,11 @@ class ClaudeHookHandler:
|
|
|
242
256
|
)
|
|
243
257
|
except Exception as e:
|
|
244
258
|
self.auto_pause_handler = None
|
|
245
|
-
|
|
246
|
-
print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
|
|
259
|
+
_log(f"Auto-pause initialization failed: {e}")
|
|
247
260
|
|
|
248
261
|
# Backward compatibility properties for tests
|
|
249
|
-
|
|
262
|
+
# Note: HTTP-based connection manager doesn't use connection_pool
|
|
263
|
+
self.connection_pool = None # Deprecated: No longer needed with HTTP emission
|
|
250
264
|
|
|
251
265
|
# Expose state manager properties for backward compatibility
|
|
252
266
|
self.active_delegations = self.state_manager.active_delegations
|
|
@@ -275,8 +289,7 @@ class ClaudeHookHandler:
|
|
|
275
289
|
def timeout_handler(signum, frame):
|
|
276
290
|
"""Handle timeout by forcing exit."""
|
|
277
291
|
nonlocal _continue_sent
|
|
278
|
-
|
|
279
|
-
print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
|
|
292
|
+
_log(f"Hook handler timeout (pid: {os.getpid()})")
|
|
280
293
|
if not _continue_sent:
|
|
281
294
|
self._continue_execution()
|
|
282
295
|
_continue_sent = True
|
|
@@ -297,11 +310,9 @@ class ClaudeHookHandler:
|
|
|
297
310
|
|
|
298
311
|
# Check for duplicate events (same event within 100ms)
|
|
299
312
|
if self.duplicate_detector.is_duplicate(event):
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
file=sys.stderr,
|
|
304
|
-
)
|
|
313
|
+
_log(
|
|
314
|
+
f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})"
|
|
315
|
+
)
|
|
305
316
|
# Still need to output continue for this invocation
|
|
306
317
|
if not _continue_sent:
|
|
307
318
|
self._continue_execution()
|
|
@@ -309,12 +320,10 @@ class ClaudeHookHandler:
|
|
|
309
320
|
return
|
|
310
321
|
|
|
311
322
|
# Debug: Log that we're processing an event
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
file=sys.stderr,
|
|
317
|
-
)
|
|
323
|
+
hook_type = event.get("hook_event_name", "unknown")
|
|
324
|
+
_log(
|
|
325
|
+
f"\n[{datetime.now(timezone.utc).isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})"
|
|
326
|
+
)
|
|
318
327
|
|
|
319
328
|
# Perform periodic cleanup if needed
|
|
320
329
|
if self.state_manager.increment_events_processed():
|
|
@@ -323,11 +332,9 @@ class ClaudeHookHandler:
|
|
|
323
332
|
from .correlation_manager import CorrelationManager
|
|
324
333
|
|
|
325
334
|
CorrelationManager.cleanup_old()
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
file=sys.stderr,
|
|
330
|
-
)
|
|
335
|
+
_log(
|
|
336
|
+
f"🧹 Performed cleanup after {self.state_manager.events_processed} events"
|
|
337
|
+
)
|
|
331
338
|
|
|
332
339
|
# Route event to appropriate handler
|
|
333
340
|
# Handlers can optionally return modified input for PreToolUse events
|
|
@@ -367,8 +374,7 @@ class ClaudeHookHandler:
|
|
|
367
374
|
ready, _, _ = select.select([sys.stdin], [], [], 1.0)
|
|
368
375
|
if not ready:
|
|
369
376
|
# No data available within timeout
|
|
370
|
-
|
|
371
|
-
print("No hook event data received within timeout", file=sys.stderr)
|
|
377
|
+
_log("No hook event data received within timeout")
|
|
372
378
|
return None
|
|
373
379
|
|
|
374
380
|
# Data is available, read it
|
|
@@ -379,21 +385,16 @@ class ClaudeHookHandler:
|
|
|
379
385
|
|
|
380
386
|
parsed = json.loads(event_data)
|
|
381
387
|
# Debug: Log the actual event format we receive
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
for key in ["hook_event_name", "event", "type", "event_type"]:
|
|
387
|
-
if key in parsed:
|
|
388
|
-
print(f" {key} = '{parsed[key]}'", file=sys.stderr)
|
|
388
|
+
_log(f"Received event with keys: {list(parsed.keys())}")
|
|
389
|
+
for key in ["hook_event_name", "event", "type", "event_type"]:
|
|
390
|
+
if key in parsed:
|
|
391
|
+
_log(f" {key} = '{parsed[key]}'")
|
|
389
392
|
return parsed
|
|
390
393
|
except (json.JSONDecodeError, ValueError) as e:
|
|
391
|
-
|
|
392
|
-
print(f"Failed to parse hook event: {e}", file=sys.stderr)
|
|
394
|
+
_log(f"Failed to parse hook event: {e}")
|
|
393
395
|
return None
|
|
394
396
|
except Exception as e:
|
|
395
|
-
|
|
396
|
-
print(f"Error reading hook event: {e}", file=sys.stderr)
|
|
397
|
+
_log(f"Error reading hook event: {e}")
|
|
397
398
|
return None
|
|
398
399
|
|
|
399
400
|
def _route_event(self, event: dict) -> Optional[dict]:
|
|
@@ -422,9 +423,9 @@ class ClaudeHookHandler:
|
|
|
422
423
|
)
|
|
423
424
|
|
|
424
425
|
# Log the actual event structure for debugging
|
|
425
|
-
if
|
|
426
|
-
|
|
427
|
-
|
|
426
|
+
if hook_type == "unknown":
|
|
427
|
+
_log(f"Unknown event format, keys: {list(event.keys())}")
|
|
428
|
+
_log(f"Event sample: {str(event)[:200]}")
|
|
428
429
|
|
|
429
430
|
# Map event types to handlers
|
|
430
431
|
event_handlers = {
|
|
@@ -460,8 +461,7 @@ class ClaudeHookHandler:
|
|
|
460
461
|
except Exception as e:
|
|
461
462
|
error_message = str(e)
|
|
462
463
|
return_value = None
|
|
463
|
-
|
|
464
|
-
print(f"Error handling {hook_type}: {e}", file=sys.stderr)
|
|
464
|
+
_log(f"Error handling {hook_type}: {e}")
|
|
465
465
|
finally:
|
|
466
466
|
# Calculate duration
|
|
467
467
|
duration_ms = int((time.time() - start_time) * 1000)
|
|
@@ -495,9 +495,12 @@ class ClaudeHookHandler:
|
|
|
495
495
|
"""
|
|
496
496
|
if modified_input is not None:
|
|
497
497
|
# Claude Code v2.0.30+ supports modifying PreToolUse tool inputs
|
|
498
|
-
print(
|
|
498
|
+
print(
|
|
499
|
+
json.dumps({"action": "continue", "tool_input": modified_input}),
|
|
500
|
+
flush=True,
|
|
501
|
+
)
|
|
499
502
|
else:
|
|
500
|
-
print(json.dumps({"action": "continue"}))
|
|
503
|
+
print(json.dumps({"action": "continue"}), flush=True)
|
|
501
504
|
|
|
502
505
|
# Delegation methods for compatibility with event_handlers
|
|
503
506
|
def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
|
|
@@ -583,11 +586,9 @@ class ClaudeHookHandler:
|
|
|
583
586
|
# This uses the existing event infrastructure
|
|
584
587
|
self._emit_socketio_event("", "hook_execution", hook_data)
|
|
585
588
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
file=sys.stderr,
|
|
590
|
-
)
|
|
589
|
+
_log(
|
|
590
|
+
f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}"
|
|
591
|
+
)
|
|
591
592
|
|
|
592
593
|
def _generate_hook_summary(self, hook_type: str, event: dict, success: bool) -> str:
|
|
593
594
|
"""Generate a human-readable summary of what the hook did.
|
|
@@ -670,25 +671,18 @@ def main():
|
|
|
670
671
|
if not is_compatible:
|
|
671
672
|
# Version incompatible - just continue without processing
|
|
672
673
|
# This prevents errors on older Claude Code versions
|
|
673
|
-
if
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
file=sys.stderr,
|
|
677
|
-
)
|
|
678
|
-
print(json.dumps({"action": "continue"}))
|
|
674
|
+
if version:
|
|
675
|
+
_log(f"Skipping hook processing due to version incompatibility ({version})")
|
|
676
|
+
print(json.dumps({"action": "continue"}), flush=True)
|
|
679
677
|
sys.exit(0)
|
|
680
678
|
|
|
681
679
|
def cleanup_handler(signum=None, frame=None):
|
|
682
680
|
"""Cleanup handler for signals and exit."""
|
|
683
681
|
nonlocal _continue_printed
|
|
684
|
-
|
|
685
|
-
print(
|
|
686
|
-
f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
|
|
687
|
-
file=sys.stderr,
|
|
688
|
-
)
|
|
682
|
+
_log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
|
|
689
683
|
# Only output continue if we haven't already (i.e., if interrupted by signal)
|
|
690
684
|
if signum is not None and not _continue_printed:
|
|
691
|
-
print(json.dumps({"action": "continue"}))
|
|
685
|
+
print(json.dumps({"action": "continue"}), flush=True)
|
|
692
686
|
_continue_printed = True
|
|
693
687
|
sys.exit(0)
|
|
694
688
|
|
|
@@ -702,15 +696,10 @@ def main():
|
|
|
702
696
|
with _handler_lock:
|
|
703
697
|
if _global_handler is None:
|
|
704
698
|
_global_handler = ClaudeHookHandler()
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
)
|
|
710
|
-
elif DEBUG:
|
|
711
|
-
print(
|
|
712
|
-
f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
|
|
713
|
-
file=sys.stderr,
|
|
699
|
+
_log(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})")
|
|
700
|
+
else:
|
|
701
|
+
_log(
|
|
702
|
+
f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})"
|
|
714
703
|
)
|
|
715
704
|
|
|
716
705
|
handler = _global_handler
|
|
@@ -726,13 +715,17 @@ def main():
|
|
|
726
715
|
except Exception as e:
|
|
727
716
|
# Only output continue if not already printed
|
|
728
717
|
if not _continue_printed:
|
|
729
|
-
print(json.dumps({"action": "continue"}))
|
|
718
|
+
print(json.dumps({"action": "continue"}), flush=True)
|
|
730
719
|
_continue_printed = True
|
|
731
720
|
# Log error for debugging
|
|
732
|
-
|
|
733
|
-
print(f"Hook handler error: {e}", file=sys.stderr)
|
|
721
|
+
_log(f"Hook handler error: {e}")
|
|
734
722
|
sys.exit(0) # Exit cleanly even on error
|
|
735
723
|
|
|
736
724
|
|
|
737
725
|
if __name__ == "__main__":
|
|
738
|
-
|
|
726
|
+
try:
|
|
727
|
+
main()
|
|
728
|
+
except Exception:
|
|
729
|
+
# Catastrophic failure (import error, etc.) - always output valid JSON
|
|
730
|
+
print(json.dumps({"action": "continue"}), flush=True)
|
|
731
|
+
sys.exit(0)
|
|
@@ -48,15 +48,10 @@ echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/hook
|
|
|
48
48
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Running: $PYTHON_CMD -m claude_mpm.hooks.claude_hooks.hook_handler" >> /tmp/hook-wrapper.log
|
|
49
49
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] SOCKETIO_PORT: $CLAUDE_MPM_SOCKETIO_PORT" >> /tmp/hook-wrapper.log
|
|
50
50
|
|
|
51
|
-
# Run the Python hook handler as a module
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
echo '{"action": "continue"}'
|
|
56
|
-
# Log the error for debugging
|
|
57
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/hook-error.log" >> /tmp/hook-wrapper.log
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
51
|
+
# Run the Python hook handler as a module
|
|
52
|
+
# Python handler is responsible for ALL stdout output (including error fallback)
|
|
53
|
+
# Redirect stderr to log file for debugging
|
|
54
|
+
"$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/hook-error.log
|
|
60
55
|
|
|
61
|
-
#
|
|
62
|
-
exit
|
|
56
|
+
# Exit with Python's exit code (should always be 0)
|
|
57
|
+
exit $?
|