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
|
@@ -10,12 +10,10 @@ import os
|
|
|
10
10
|
import re
|
|
11
11
|
import shutil
|
|
12
12
|
import stat
|
|
13
|
-
import subprocess
|
|
13
|
+
import subprocess # nosec B404 - Safe: only uses hardcoded 'claude' CLI command, no user input
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
|
-
from ...core.logger import get_logger
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
class HookInstaller:
|
|
21
19
|
"""Manages installation and configuration of Claude MPM hooks."""
|
|
@@ -194,10 +192,17 @@ main "$@"
|
|
|
194
192
|
MIN_CLAUDE_VERSION = "1.0.92"
|
|
195
193
|
# Minimum version for PreToolUse input modification support
|
|
196
194
|
MIN_PRETOOL_MODIFY_VERSION = "2.0.30"
|
|
195
|
+
# Minimum version for user-invocable skills support
|
|
196
|
+
MIN_SKILLS_VERSION = "2.1.3"
|
|
197
197
|
|
|
198
198
|
def __init__(self):
|
|
199
199
|
"""Initialize the hook installer."""
|
|
200
|
-
|
|
200
|
+
# Use __name__ directly to avoid double prefix
|
|
201
|
+
# __name__ is already 'claude_mpm.hooks.claude_hooks.installer'
|
|
202
|
+
# get_logger() adds 'claude_mpm.' prefix, causing duplicate
|
|
203
|
+
import logging
|
|
204
|
+
|
|
205
|
+
self.logger = logging.getLogger(__name__)
|
|
201
206
|
self.claude_dir = Path.home() / ".claude"
|
|
202
207
|
self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
|
|
203
208
|
# Use settings.json for hooks (Claude Code reads from this file)
|
|
@@ -220,7 +225,7 @@ main "$@"
|
|
|
220
225
|
|
|
221
226
|
try:
|
|
222
227
|
# Run claude --version command
|
|
223
|
-
result = subprocess.run(
|
|
228
|
+
result = subprocess.run( # nosec B607 B603 - Safe: hardcoded command, no user input
|
|
224
229
|
["claude", "--version"],
|
|
225
230
|
capture_output=True,
|
|
226
231
|
text=True,
|
|
@@ -331,6 +336,53 @@ main "$@"
|
|
|
331
336
|
|
|
332
337
|
return True
|
|
333
338
|
|
|
339
|
+
def _version_meets_minimum(self, version: str, min_version: str) -> bool:
|
|
340
|
+
"""Check if a version meets minimum requirements.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
version: Current version string (e.g., "2.1.3")
|
|
344
|
+
min_version: Minimum required version string (e.g., "2.1.3")
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if version meets or exceeds minimum, False otherwise
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
def parse_version(v: str) -> List[int]:
|
|
351
|
+
"""Parse semantic version string to list of integers."""
|
|
352
|
+
try:
|
|
353
|
+
return [int(x) for x in v.split(".")]
|
|
354
|
+
except (ValueError, AttributeError):
|
|
355
|
+
return [0]
|
|
356
|
+
|
|
357
|
+
current = parse_version(version)
|
|
358
|
+
required = parse_version(min_version)
|
|
359
|
+
|
|
360
|
+
# Compare versions
|
|
361
|
+
for i in range(max(len(current), len(required))):
|
|
362
|
+
curr_part = current[i] if i < len(current) else 0
|
|
363
|
+
req_part = required[i] if i < len(required) else 0
|
|
364
|
+
|
|
365
|
+
if curr_part < req_part:
|
|
366
|
+
return False
|
|
367
|
+
if curr_part > req_part:
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
def supports_user_invocable_skills(self) -> bool:
|
|
373
|
+
"""Check if Claude Code version supports user-invocable skills.
|
|
374
|
+
|
|
375
|
+
User-invocable skills were added in Claude Code v2.1.3.
|
|
376
|
+
This feature allows users to invoke skills via slash commands.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
True if version supports user-invocable skills, False otherwise
|
|
380
|
+
"""
|
|
381
|
+
version = self.get_claude_version()
|
|
382
|
+
if not version:
|
|
383
|
+
return False
|
|
384
|
+
return self._version_meets_minimum(version, self.MIN_SKILLS_VERSION)
|
|
385
|
+
|
|
334
386
|
def get_hook_script_path(self) -> Path:
|
|
335
387
|
"""Get the path to the hook handler script based on installation method.
|
|
336
388
|
|
|
@@ -556,7 +608,22 @@ main "$@"
|
|
|
556
608
|
self._cleanup_old_settings()
|
|
557
609
|
|
|
558
610
|
def _install_commands(self) -> None:
|
|
559
|
-
"""Install custom commands for Claude Code.
|
|
611
|
+
"""Install custom commands for Claude Code.
|
|
612
|
+
|
|
613
|
+
For Claude Code >= 2.1.3, commands are deployed as skills via PMSkillsDeployerService.
|
|
614
|
+
This method provides backward compatibility for older versions.
|
|
615
|
+
"""
|
|
616
|
+
# Check if skills-based commands are supported
|
|
617
|
+
if self.supports_user_invocable_skills():
|
|
618
|
+
self.logger.info(
|
|
619
|
+
"Claude Code >= 2.1.3 detected. Commands deployed as skills - "
|
|
620
|
+
"skipping legacy command installation."
|
|
621
|
+
)
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
# Legacy installation for older Claude Code versions
|
|
625
|
+
self.logger.info("Installing legacy commands for Claude Code < 2.1.3")
|
|
626
|
+
|
|
560
627
|
# Find commands directory using proper resource resolution
|
|
561
628
|
try:
|
|
562
629
|
from ...core.unified_paths import get_package_resource_path
|
|
@@ -782,7 +849,7 @@ main "$@"
|
|
|
782
849
|
if "hooks" in settings:
|
|
783
850
|
status["configured_events"] = list(settings["hooks"].keys())
|
|
784
851
|
configured_in_local = True
|
|
785
|
-
except Exception:
|
|
852
|
+
except Exception: # nosec B110 - Intentional: ignore errors reading settings file
|
|
786
853
|
pass
|
|
787
854
|
|
|
788
855
|
# Also check old settings file
|
|
@@ -796,7 +863,7 @@ main "$@"
|
|
|
796
863
|
status["warning"] = (
|
|
797
864
|
"Hooks found in settings.local.json but Claude Code reads from settings.json"
|
|
798
865
|
)
|
|
799
|
-
except Exception:
|
|
866
|
+
except Exception: # nosec B110 - Intentional: ignore errors reading old settings file
|
|
800
867
|
pass
|
|
801
868
|
|
|
802
869
|
status["settings_location"] = (
|
|
@@ -11,15 +11,23 @@ import sys
|
|
|
11
11
|
|
|
12
12
|
# Install-type-aware logging configuration BEFORE kuzu-memory imports
|
|
13
13
|
# This overrides kuzu-memory's WARNING-level basicConfig (fixes 1M-445)
|
|
14
|
-
# but respects production install silence
|
|
14
|
+
# but respects production install silence AND startup suppression
|
|
15
15
|
try:
|
|
16
16
|
from claude_mpm.core.unified_paths import DeploymentContext, PathContext
|
|
17
17
|
|
|
18
18
|
context = PathContext.detect_deployment_context()
|
|
19
19
|
|
|
20
|
+
# CRITICAL: Check if root logger is already suppressed (CRITICAL+1 from startup.py)
|
|
21
|
+
# If so, don't call basicConfig as it will reset the level to INFO
|
|
22
|
+
root_logger = logging.getLogger()
|
|
23
|
+
is_suppressed = root_logger.level > logging.CRITICAL # CRITICAL+1 = 51
|
|
24
|
+
|
|
20
25
|
# Only configure verbose logging for development/editable installs
|
|
21
|
-
#
|
|
22
|
-
if context in (
|
|
26
|
+
# AND if logging isn't already suppressed by startup.py
|
|
27
|
+
if not is_suppressed and context in (
|
|
28
|
+
DeploymentContext.DEVELOPMENT,
|
|
29
|
+
DeploymentContext.EDITABLE_INSTALL,
|
|
30
|
+
):
|
|
23
31
|
logging.basicConfig(
|
|
24
32
|
level=logging.INFO,
|
|
25
33
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
@@ -27,14 +35,17 @@ try:
|
|
|
27
35
|
stream=sys.stderr,
|
|
28
36
|
)
|
|
29
37
|
except ImportError:
|
|
30
|
-
# Fallback: if unified_paths not available,
|
|
31
|
-
|
|
32
|
-
logging.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
# Fallback: if unified_paths not available, check suppression before configuring
|
|
39
|
+
root_logger = logging.getLogger()
|
|
40
|
+
is_suppressed = root_logger.level > logging.CRITICAL
|
|
41
|
+
|
|
42
|
+
if not is_suppressed:
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
level=logging.INFO,
|
|
45
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
46
|
+
force=True,
|
|
47
|
+
stream=sys.stderr,
|
|
48
|
+
)
|
|
38
49
|
from datetime import datetime, timezone
|
|
39
50
|
from typing import Optional
|
|
40
51
|
|
|
@@ -7,9 +7,14 @@ This service manages:
|
|
|
7
7
|
DESIGN DECISION: Use stateless HTTP POST instead of persistent SocketIO
|
|
8
8
|
connections because hook handlers are ephemeral processes (< 1 second lifetime).
|
|
9
9
|
This eliminates disconnection issues and matches the process lifecycle.
|
|
10
|
+
|
|
11
|
+
DESIGN DECISION: Synchronous HTTP POST only (no async)
|
|
12
|
+
Hook handlers are too short-lived (~25ms lifecycle) to benefit from async.
|
|
13
|
+
Using asyncio.run() creates event loops that close before HTTP operations complete,
|
|
14
|
+
causing "Event loop is closed" errors. Synchronous HTTP POST in a thread pool
|
|
15
|
+
is simpler and more reliable for ephemeral processes.
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
|
-
import asyncio
|
|
13
18
|
import os
|
|
14
19
|
import sys
|
|
15
20
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -27,9 +32,6 @@ except ImportError:
|
|
|
27
32
|
REQUESTS_AVAILABLE = False
|
|
28
33
|
requests = None
|
|
29
34
|
|
|
30
|
-
# Import high-performance event emitter - lazy loaded in _async_emit()
|
|
31
|
-
# to reduce hook handler initialization time by ~85% (792ms -> minimal)
|
|
32
|
-
|
|
33
35
|
# Import EventNormalizer for consistent event formatting
|
|
34
36
|
try:
|
|
35
37
|
from claude_mpm.services.socketio.event_normalizer import EventNormalizer
|
|
@@ -55,10 +57,6 @@ except ImportError:
|
|
|
55
57
|
)
|
|
56
58
|
|
|
57
59
|
|
|
58
|
-
# EventBus removed - using direct HTTP POST only
|
|
59
|
-
# This eliminates duplicate events and simplifies the architecture
|
|
60
|
-
|
|
61
|
-
|
|
62
60
|
class ConnectionManagerService:
|
|
63
61
|
"""Manages connections for the Claude hook handler using HTTP POST."""
|
|
64
62
|
|
|
@@ -72,17 +70,9 @@ class ConnectionManagerService:
|
|
|
72
70
|
self.server_port = int(os.environ.get("CLAUDE_MPM_SERVER_PORT", "8765"))
|
|
73
71
|
self.http_endpoint = f"http://{self.server_host}:{self.server_port}/api/events"
|
|
74
72
|
|
|
75
|
-
# EventBus removed - using direct HTTP POST only
|
|
76
|
-
|
|
77
|
-
# For backward compatibility with tests
|
|
78
|
-
self.connection_pool = None # No longer used
|
|
79
|
-
|
|
80
|
-
# Track async emit tasks to prevent garbage collection
|
|
81
|
-
self._emit_tasks: set = set()
|
|
82
|
-
|
|
83
73
|
# Thread pool for non-blocking HTTP requests
|
|
84
74
|
# WHY: Prevents HTTP POST from blocking hook processing (2s timeout → 0ms blocking)
|
|
85
|
-
# max_workers=2: Sufficient for low-frequency
|
|
75
|
+
# max_workers=2: Sufficient for low-frequency hook events
|
|
86
76
|
self._http_executor = ThreadPoolExecutor(
|
|
87
77
|
max_workers=2, thread_name_prefix="http-emit"
|
|
88
78
|
)
|
|
@@ -94,13 +84,13 @@ class ConnectionManagerService:
|
|
|
94
84
|
)
|
|
95
85
|
|
|
96
86
|
def emit_event(self, namespace: str, event: str, data: dict):
|
|
97
|
-
"""Emit event using
|
|
87
|
+
"""Emit event using HTTP POST.
|
|
98
88
|
|
|
99
|
-
WHY
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
89
|
+
WHY HTTP POST only:
|
|
90
|
+
- Hook handlers are ephemeral (~25ms lifecycle)
|
|
91
|
+
- Async emission causes "Event loop is closed" errors
|
|
92
|
+
- HTTP POST in thread pool is simpler and more reliable
|
|
93
|
+
- Completes in 20-50ms, which is acceptable for hook handlers
|
|
104
94
|
"""
|
|
105
95
|
# Create event data for normalization
|
|
106
96
|
raw_event = {
|
|
@@ -132,62 +122,9 @@ class ConnectionManagerService:
|
|
|
132
122
|
file=sys.stderr,
|
|
133
123
|
)
|
|
134
124
|
|
|
135
|
-
#
|
|
136
|
-
success = self._try_async_emit(namespace, event, claude_event_data)
|
|
137
|
-
if success:
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
# Fallback to HTTP POST for cross-process communication
|
|
125
|
+
# Emit via HTTP POST (non-blocking, runs in thread pool)
|
|
141
126
|
self._try_http_emit(namespace, event, claude_event_data)
|
|
142
127
|
|
|
143
|
-
def _try_async_emit(self, namespace: str, event: str, data: dict) -> bool:
|
|
144
|
-
"""Try to emit event using high-performance async emitter."""
|
|
145
|
-
try:
|
|
146
|
-
# Run async emission in the current event loop or create one
|
|
147
|
-
loop = None
|
|
148
|
-
try:
|
|
149
|
-
loop = asyncio.get_running_loop()
|
|
150
|
-
except RuntimeError:
|
|
151
|
-
# No running loop, create a new one
|
|
152
|
-
pass
|
|
153
|
-
|
|
154
|
-
if loop:
|
|
155
|
-
# We're in an async context, create a task with tracking
|
|
156
|
-
task = loop.create_task(self._async_emit(namespace, event, data))
|
|
157
|
-
self._emit_tasks.add(task)
|
|
158
|
-
task.add_done_callback(self._emit_tasks.discard)
|
|
159
|
-
# Don't wait for completion to maintain low latency
|
|
160
|
-
if DEBUG:
|
|
161
|
-
print(f"✅ Async emit scheduled: {event}", file=sys.stderr)
|
|
162
|
-
return True
|
|
163
|
-
# No event loop, run synchronously
|
|
164
|
-
success = asyncio.run(self._async_emit(namespace, event, data))
|
|
165
|
-
if DEBUG and success:
|
|
166
|
-
print(f"✅ Async emit successful: {event}", file=sys.stderr)
|
|
167
|
-
return success
|
|
168
|
-
|
|
169
|
-
except Exception as e:
|
|
170
|
-
if DEBUG:
|
|
171
|
-
print(f"⚠️ Async emit failed: {e}", file=sys.stderr)
|
|
172
|
-
return False
|
|
173
|
-
|
|
174
|
-
async def _async_emit(self, namespace: str, event: str, data: dict) -> bool:
|
|
175
|
-
"""Async helper for event emission."""
|
|
176
|
-
try:
|
|
177
|
-
# Lazy load event emitter to reduce initialization overhead
|
|
178
|
-
from claude_mpm.services.monitor.event_emitter import get_event_emitter
|
|
179
|
-
|
|
180
|
-
emitter = await get_event_emitter()
|
|
181
|
-
return await emitter.emit_event(namespace, "claude_event", data)
|
|
182
|
-
except ImportError:
|
|
183
|
-
if DEBUG:
|
|
184
|
-
print("⚠️ Event emitter not available", file=sys.stderr)
|
|
185
|
-
return False
|
|
186
|
-
except Exception as e:
|
|
187
|
-
if DEBUG:
|
|
188
|
-
print(f"⚠️ Async emitter error: {e}", file=sys.stderr)
|
|
189
|
-
return False
|
|
190
|
-
|
|
191
128
|
def _try_http_emit(self, namespace: str, event: str, data: dict):
|
|
192
129
|
"""Try to emit event using HTTP POST fallback (non-blocking).
|
|
193
130
|
|
|
@@ -62,8 +62,13 @@ set -e
|
|
|
62
62
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
63
63
|
|
|
64
64
|
# Determine the claude-mpm root based on installation type
|
|
65
|
+
# Check if we're in a UV tools installation
|
|
66
|
+
if [[ "$SCRIPT_DIR" == *"/.local/share/uv/tools/"* ]]; then
|
|
67
|
+
# UV tools installation - script is at lib/python*/site-packages/claude_mpm/scripts/
|
|
68
|
+
# The tool root is what we need for Python detection
|
|
69
|
+
CLAUDE_MPM_ROOT="$(echo "$SCRIPT_DIR" | sed 's|/lib/python.*/site-packages.*||')"
|
|
65
70
|
# Check if we're in a pipx installation
|
|
66
|
-
|
|
71
|
+
elif [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
|
|
67
72
|
# pipx installation - script is at lib/python*/site-packages/claude_mpm/scripts/
|
|
68
73
|
# The venv root is what we need for Python detection
|
|
69
74
|
CLAUDE_MPM_ROOT="$(echo "$SCRIPT_DIR" | sed 's|/lib/python.*/site-packages/.*||')"
|
|
@@ -89,11 +94,12 @@ fi
|
|
|
89
94
|
# STRATEGY:
|
|
90
95
|
# This function implements a fallback chain to find Python with claude-mpm dependencies:
|
|
91
96
|
# 1. UV-managed projects (uv.lock detected) - uses "uv run python"
|
|
92
|
-
# 2.
|
|
93
|
-
# 3.
|
|
94
|
-
# 4.
|
|
95
|
-
# 5.
|
|
96
|
-
# 6. System
|
|
97
|
+
# 2. UV tools installations (~/.local/share/uv/tools/) - uses tool's venv Python
|
|
98
|
+
# 3. pipx installations - uses pipx venv Python
|
|
99
|
+
# 4. Project-specific virtual environments (venv, .venv)
|
|
100
|
+
# 5. Currently active virtual environment ($VIRTUAL_ENV)
|
|
101
|
+
# 6. System python3 (may lack dependencies)
|
|
102
|
+
# 7. System python (last resort)
|
|
97
103
|
#
|
|
98
104
|
# WHY THIS APPROACH:
|
|
99
105
|
# - Claude MPM requires specific packages (socketio, eventlet) not in system Python
|
|
@@ -124,7 +130,21 @@ find_python_command() {
|
|
|
124
130
|
fi
|
|
125
131
|
fi
|
|
126
132
|
|
|
127
|
-
# 2. Check if we're in a
|
|
133
|
+
# 2. Check if we're in a UV tools installation
|
|
134
|
+
if [[ "$SCRIPT_DIR" == *"/.local/share/uv/tools/"* ]]; then
|
|
135
|
+
# UV tools installation - extract the tool root directory
|
|
136
|
+
CLAUDE_MPM_ROOT="$(echo "$SCRIPT_DIR" | sed 's|/lib/python.*/site-packages.*||')"
|
|
137
|
+
local uv_python="$CLAUDE_MPM_ROOT/bin/python"
|
|
138
|
+
if [ -x "$uv_python" ]; then
|
|
139
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
140
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] UV tools Python found: $uv_python" >> /tmp/claude-mpm-hook.log
|
|
141
|
+
fi
|
|
142
|
+
echo "$uv_python"
|
|
143
|
+
return
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# 3. Check if we're in a pipx installation
|
|
128
148
|
if [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
|
|
129
149
|
# pipx installation - use the pipx venv's Python directly
|
|
130
150
|
if [ -f "$CLAUDE_MPM_ROOT/bin/python" ]; then
|
|
@@ -133,7 +153,7 @@ find_python_command() {
|
|
|
133
153
|
fi
|
|
134
154
|
fi
|
|
135
155
|
|
|
136
|
-
#
|
|
156
|
+
# 4. Check for project-local virtual environment (common in development)
|
|
137
157
|
if [ -f "$CLAUDE_MPM_ROOT/venv/bin/activate" ]; then
|
|
138
158
|
source "$CLAUDE_MPM_ROOT/venv/bin/activate"
|
|
139
159
|
echo "$CLAUDE_MPM_ROOT/venv/bin/python"
|
|
@@ -154,7 +174,13 @@ find_python_command() {
|
|
|
154
174
|
PYTHON_CMD=$(find_python_command)
|
|
155
175
|
|
|
156
176
|
# Check installation type and set PYTHONPATH accordingly
|
|
157
|
-
if [[ "$SCRIPT_DIR" == *"/.local/
|
|
177
|
+
if [[ "$SCRIPT_DIR" == *"/.local/share/uv/tools/"* ]]; then
|
|
178
|
+
# UV tools installation - claude_mpm is already in the tool's site-packages
|
|
179
|
+
# No need to modify PYTHONPATH
|
|
180
|
+
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
181
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] UV tools installation detected" >> /tmp/claude-mpm-hook.log
|
|
182
|
+
fi
|
|
183
|
+
elif [[ "$SCRIPT_DIR" == *"/.local/pipx/venvs/claude-mpm/"* ]]; then
|
|
158
184
|
# pipx installation - claude_mpm is already in the venv's site-packages
|
|
159
185
|
# No need to modify PYTHONPATH
|
|
160
186
|
if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
@@ -210,10 +236,11 @@ fi
|
|
|
210
236
|
# Run the Python hook handler with all input
|
|
211
237
|
# Use exec to replace the shell process with Python
|
|
212
238
|
# Handle UV's multi-word command specially
|
|
239
|
+
# Suppress RuntimeWarning to prevent stderr output (which causes hook errors)
|
|
213
240
|
if [[ "$PYTHON_CMD" == "uv run python" ]]; then
|
|
214
|
-
exec uv run python -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
241
|
+
exec uv run python -W ignore::RuntimeWarning -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
215
242
|
else
|
|
216
|
-
exec "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
243
|
+
exec "$PYTHON_CMD" -W ignore::RuntimeWarning -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/claude-mpm-hook-error.log
|
|
217
244
|
fi
|
|
218
245
|
|
|
219
246
|
# Note: exec replaces the shell process, so code below only runs if exec fails
|
|
@@ -224,4 +251,4 @@ if [ "${CLAUDE_MPM_HOOK_DEBUG}" = "true" ]; then
|
|
|
224
251
|
fi
|
|
225
252
|
# Return continue action to prevent blocking Claude Code
|
|
226
253
|
echo '{"action": "continue"}'
|
|
227
|
-
exit 0
|
|
254
|
+
exit 0
|
|
@@ -30,16 +30,16 @@ class AgentRecommendationService:
|
|
|
30
30
|
Can be used by CLI, API, or future auto-configuration features.
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
# Core agents always included -
|
|
33
|
+
# Core agents always included - Standard 6 core agents for essential PM workflow
|
|
34
|
+
# These agents are auto-deployed when no configuration exists
|
|
34
35
|
# Uses exact agent IDs from repository for consistency
|
|
35
36
|
CORE_AGENTS = {
|
|
36
|
-
"engineer",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"security-agent",
|
|
37
|
+
"engineer", # General-purpose implementation
|
|
38
|
+
"research", # Codebase exploration and analysis
|
|
39
|
+
"qa", # Testing and quality assurance
|
|
40
|
+
"documentation", # Documentation generation
|
|
41
|
+
"ops", # Basic deployment operations
|
|
42
|
+
"ticketing", # Ticket tracking (essential for PM workflow)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
# Map detected languages to recommended engineer agents
|
|
@@ -10,10 +10,19 @@ Loading precedence: Project → User → System
|
|
|
10
10
|
|
|
11
11
|
This service integrates with the main agent_loader.py to provide
|
|
12
12
|
markdown-based agent profiles alongside JSON-based templates.
|
|
13
|
+
|
|
14
|
+
Auto-Deployment: When no agents are configured, the standard 6 core agents
|
|
15
|
+
are automatically deployed:
|
|
16
|
+
- engineer: General-purpose implementation
|
|
17
|
+
- research: Codebase exploration and analysis
|
|
18
|
+
- qa: Testing and quality assurance
|
|
19
|
+
- documentation: Documentation generation
|
|
20
|
+
- ops: Basic deployment operations
|
|
21
|
+
- ticketing: Ticket tracking (essential for PM workflow)
|
|
13
22
|
"""
|
|
14
23
|
|
|
15
24
|
from pathlib import Path
|
|
16
|
-
from typing import Any, Dict, Optional
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
17
26
|
|
|
18
27
|
from claude_mpm.agents.agent_loader import AgentTier, list_agents_by_tier
|
|
19
28
|
from claude_mpm.core.logging_utils import get_logger
|
|
@@ -21,6 +30,17 @@ from claude_mpm.core.unified_paths import get_path_manager
|
|
|
21
30
|
|
|
22
31
|
logger = get_logger(__name__)
|
|
23
32
|
|
|
33
|
+
# Standard 6 core agents that are auto-deployed when no agents are specified
|
|
34
|
+
# This list is the canonical source - other modules should import from here
|
|
35
|
+
CORE_AGENTS: List[str] = [
|
|
36
|
+
"engineer", # General-purpose implementation
|
|
37
|
+
"research", # Codebase exploration and analysis
|
|
38
|
+
"qa", # Testing and quality assurance
|
|
39
|
+
"documentation", # Documentation generation
|
|
40
|
+
"ops", # Basic deployment operations
|
|
41
|
+
"ticketing", # Ticket tracking (essential for PM workflow)
|
|
42
|
+
]
|
|
43
|
+
|
|
24
44
|
|
|
25
45
|
class FrameworkAgentLoader:
|
|
26
46
|
"""Loads agent profiles from project, user, and system directories with proper precedence"""
|
|
@@ -86,7 +106,7 @@ class FrameworkAgentLoader:
|
|
|
86
106
|
data_claude = package_path / "data" / "agents" / "CLAUDE.md"
|
|
87
107
|
if data_instructions.exists() or data_claude.exists():
|
|
88
108
|
return package_path / "data"
|
|
89
|
-
except Exception:
|
|
109
|
+
except Exception: # nosec B110 - intentional fallthrough to next location
|
|
90
110
|
pass
|
|
91
111
|
|
|
92
112
|
current = Path.cwd()
|
|
@@ -431,3 +451,56 @@ Please operate according to your profile specifications and maintain quality sta
|
|
|
431
451
|
"""
|
|
432
452
|
|
|
433
453
|
return instruction.strip()
|
|
454
|
+
|
|
455
|
+
def get_core_agents(self) -> List[str]:
|
|
456
|
+
"""
|
|
457
|
+
Get the standard 6 core agents for auto-deployment.
|
|
458
|
+
|
|
459
|
+
These agents are automatically deployed when no agents are specified
|
|
460
|
+
in the configuration. They provide essential PM workflow functionality.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of core agent IDs
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> loader = FrameworkAgentLoader()
|
|
467
|
+
>>> core = loader.get_core_agents()
|
|
468
|
+
>>> 'engineer' in core
|
|
469
|
+
True
|
|
470
|
+
>>> len(core)
|
|
471
|
+
6
|
|
472
|
+
"""
|
|
473
|
+
return CORE_AGENTS.copy()
|
|
474
|
+
|
|
475
|
+
def get_agents_with_fallback(self) -> Dict[str, list]:
|
|
476
|
+
"""
|
|
477
|
+
Get available agents, falling back to core agents if none found.
|
|
478
|
+
|
|
479
|
+
This method implements the auto-deployment logic: when no agents
|
|
480
|
+
are found in any tier (project, user, system), it returns the
|
|
481
|
+
standard 6 core agents as a fallback.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Dictionary with agent lists by tier. If no agents found in any tier,
|
|
485
|
+
returns core agents under 'fallback' key.
|
|
486
|
+
|
|
487
|
+
Example:
|
|
488
|
+
>>> loader = FrameworkAgentLoader()
|
|
489
|
+
>>> loader.initialize()
|
|
490
|
+
>>> agents = loader.get_agents_with_fallback()
|
|
491
|
+
>>> if 'fallback' in agents:
|
|
492
|
+
... print("Using core agents as fallback")
|
|
493
|
+
"""
|
|
494
|
+
available = self.get_available_agents()
|
|
495
|
+
|
|
496
|
+
# Check if any agents are found
|
|
497
|
+
total_agents = sum(len(agents) for agents in available.values())
|
|
498
|
+
|
|
499
|
+
if total_agents == 0:
|
|
500
|
+
logger.info(
|
|
501
|
+
"No agents found in configuration. "
|
|
502
|
+
"Auto-deploying standard 6 core agents."
|
|
503
|
+
)
|
|
504
|
+
return {"fallback": CORE_AGENTS.copy()}
|
|
505
|
+
|
|
506
|
+
return available
|
claude_mpm/services/event_log.py
CHANGED
|
@@ -310,8 +310,16 @@ def get_event_log(log_file: Optional[Path] = None) -> EventLog:
|
|
|
310
310
|
|
|
311
311
|
Returns:
|
|
312
312
|
EventLog instance
|
|
313
|
+
|
|
314
|
+
Note:
|
|
315
|
+
If log_file is provided and differs from the current instance,
|
|
316
|
+
a new EventLog is created and replaces the global instance.
|
|
317
|
+
This allows hooks to use project-specific event logs.
|
|
313
318
|
"""
|
|
314
319
|
global _event_log
|
|
315
320
|
if _event_log is None:
|
|
316
321
|
_event_log = EventLog(log_file)
|
|
322
|
+
elif log_file is not None and _event_log.log_file != log_file:
|
|
323
|
+
# Create new instance if log file differs from current
|
|
324
|
+
_event_log = EventLog(log_file)
|
|
317
325
|
return _event_log
|