claude-mpm 5.4.96__py3-none-any.whl → 5.6.4__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 +90 -99
- 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.4.dist-info}/METADATA +18 -4
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.4.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.4.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.4.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -50,9 +50,16 @@ from claude_mpm.core.mixins import LoggerMixin
|
|
|
50
50
|
# Security constants
|
|
51
51
|
MAX_YAML_SIZE = 10 * 1024 * 1024 # 10MB limit to prevent YAML bombs
|
|
52
52
|
|
|
53
|
-
# Required PM skills that MUST be deployed for PM agent to function properly
|
|
54
|
-
# These are framework management skills
|
|
53
|
+
# Tier 1: Required PM skills that MUST be deployed for PM agent to function properly
|
|
54
|
+
# These are core framework management skills for basic PM operation
|
|
55
55
|
REQUIRED_PM_SKILLS = [
|
|
56
|
+
# Core command-based skills (new consolidated CLI)
|
|
57
|
+
"mpm",
|
|
58
|
+
"mpm-init",
|
|
59
|
+
"mpm-status",
|
|
60
|
+
"mpm-help",
|
|
61
|
+
"mpm-doctor",
|
|
62
|
+
# Legacy framework management skills
|
|
56
63
|
"mpm-git-file-tracking",
|
|
57
64
|
"mpm-pr-workflow",
|
|
58
65
|
"mpm-ticketing-integration",
|
|
@@ -66,6 +73,23 @@ REQUIRED_PM_SKILLS = [
|
|
|
66
73
|
"mpm-session-management",
|
|
67
74
|
]
|
|
68
75
|
|
|
76
|
+
# Tier 2: Recommended skills (deployed with standard install)
|
|
77
|
+
# These provide enhanced functionality for common workflows
|
|
78
|
+
RECOMMENDED_PM_SKILLS = [
|
|
79
|
+
"mpm-config",
|
|
80
|
+
"mpm-ticket-view",
|
|
81
|
+
"mpm-session-resume",
|
|
82
|
+
"mpm-postmortem",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# Tier 3: Optional skills (deployed with full install)
|
|
86
|
+
# These provide additional features for advanced use cases
|
|
87
|
+
OPTIONAL_PM_SKILLS = [
|
|
88
|
+
"mpm-monitor",
|
|
89
|
+
"mpm-version",
|
|
90
|
+
"mpm-organize",
|
|
91
|
+
]
|
|
92
|
+
|
|
69
93
|
|
|
70
94
|
@dataclass
|
|
71
95
|
class PMSkillInfo:
|
|
@@ -383,17 +407,45 @@ class PMSkillsDeployerService(LoggerMixin):
|
|
|
383
407
|
self.logger.info(f"Discovered {len(skills)} bundled PM skills")
|
|
384
408
|
return skills
|
|
385
409
|
|
|
410
|
+
def _get_skills_for_tier(self, tier: str) -> List[str]:
|
|
411
|
+
"""Get list of skills to deploy based on tier.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
tier: Deployment tier - "minimal", "standard", or "full"
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of skill names to deploy
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ValueError: If tier is invalid
|
|
421
|
+
"""
|
|
422
|
+
if tier == "minimal":
|
|
423
|
+
return REQUIRED_PM_SKILLS
|
|
424
|
+
if tier == "standard":
|
|
425
|
+
return REQUIRED_PM_SKILLS + RECOMMENDED_PM_SKILLS
|
|
426
|
+
if tier == "full":
|
|
427
|
+
return REQUIRED_PM_SKILLS + RECOMMENDED_PM_SKILLS + OPTIONAL_PM_SKILLS
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"Invalid tier '{tier}'. Must be 'minimal', 'standard', or 'full'"
|
|
430
|
+
)
|
|
431
|
+
|
|
386
432
|
def deploy_pm_skills(
|
|
387
433
|
self,
|
|
388
434
|
project_dir: Path,
|
|
389
435
|
force: bool = False,
|
|
436
|
+
tier: str = "standard",
|
|
390
437
|
progress_callback: Optional[Callable[[str, int, int], None]] = None,
|
|
391
438
|
) -> DeploymentResult:
|
|
392
|
-
"""Deploy bundled PM skills to project directory.
|
|
439
|
+
"""Deploy bundled PM skills to project directory with tier-based selection.
|
|
393
440
|
|
|
394
441
|
Copies PM skills from bundled templates to .claude/skills/{name}/SKILL.md
|
|
395
442
|
and updates registry with version and checksum information.
|
|
396
443
|
|
|
444
|
+
Deployment Tiers:
|
|
445
|
+
- "minimal": Only REQUIRED_PM_SKILLS (Tier 1 - core functionality)
|
|
446
|
+
- "standard": REQUIRED_PM_SKILLS + RECOMMENDED_PM_SKILLS (Tier 1+2 - common workflows)
|
|
447
|
+
- "full": All skills (Tier 1+2+3 - advanced features)
|
|
448
|
+
|
|
397
449
|
Conflict Resolution:
|
|
398
450
|
- mpm-* skills from src WIN (overwrite existing)
|
|
399
451
|
- Non-mpm-* skills in .claude/skills/ are untouched
|
|
@@ -401,16 +453,42 @@ class PMSkillsDeployerService(LoggerMixin):
|
|
|
401
453
|
Args:
|
|
402
454
|
project_dir: Project root directory
|
|
403
455
|
force: If True, redeploy even if skill already exists
|
|
456
|
+
tier: Deployment tier - "minimal", "standard" (default), or "full"
|
|
404
457
|
progress_callback: Optional callback(skill_name, current, total) for progress
|
|
405
458
|
|
|
406
459
|
Returns:
|
|
407
460
|
DeploymentResult with deployment status and details
|
|
408
461
|
|
|
409
462
|
Example:
|
|
410
|
-
>>>
|
|
463
|
+
>>> # Standard deployment (Tier 1 + Tier 2)
|
|
464
|
+
>>> result = deployer.deploy_pm_skills(Path("/project"))
|
|
411
465
|
>>> print(f"Deployed: {len(result.deployed)} to .claude/skills/")
|
|
466
|
+
>>>
|
|
467
|
+
>>> # Minimal deployment (Tier 1 only)
|
|
468
|
+
>>> result = deployer.deploy_pm_skills(Path("/project"), tier="minimal")
|
|
469
|
+
>>>
|
|
470
|
+
>>> # Full deployment (all tiers)
|
|
471
|
+
>>> result = deployer.deploy_pm_skills(Path("/project"), tier="full")
|
|
412
472
|
"""
|
|
413
|
-
|
|
473
|
+
# Get tier-based skill filter
|
|
474
|
+
try:
|
|
475
|
+
tier_skills = self._get_skills_for_tier(tier)
|
|
476
|
+
except ValueError as e:
|
|
477
|
+
return DeploymentResult(
|
|
478
|
+
success=False,
|
|
479
|
+
deployed=[],
|
|
480
|
+
skipped=[],
|
|
481
|
+
errors=[{"skill": "all", "error": str(e)}],
|
|
482
|
+
message=str(e),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Discover all bundled skills, then filter by tier
|
|
486
|
+
all_skills = self._discover_bundled_pm_skills()
|
|
487
|
+
skills = [s for s in all_skills if s["name"] in tier_skills]
|
|
488
|
+
|
|
489
|
+
self.logger.info(
|
|
490
|
+
f"Deploying {len(skills)}/{len(all_skills)} skills for tier '{tier}'"
|
|
491
|
+
)
|
|
414
492
|
deployed = []
|
|
415
493
|
skipped = []
|
|
416
494
|
errors = []
|
|
@@ -526,7 +604,7 @@ class PMSkillsDeployerService(LoggerMixin):
|
|
|
526
604
|
|
|
527
605
|
success = len(errors) == 0
|
|
528
606
|
message = (
|
|
529
|
-
f"Deployed {len(deployed)} skills, skipped {len(skipped)}, "
|
|
607
|
+
f"Deployed {len(deployed)} skills (tier: {tier}), skipped {len(skipped)}, "
|
|
530
608
|
f"{len(errors)} errors"
|
|
531
609
|
)
|
|
532
610
|
|
|
@@ -682,7 +682,7 @@ class GitSkillSourceManager:
|
|
|
682
682
|
try:
|
|
683
683
|
with open(etag_cache_file, encoding="utf-8") as f:
|
|
684
684
|
etag_cache = json.load(f)
|
|
685
|
-
except Exception:
|
|
685
|
+
except Exception: # nosec B110 - intentional: proceed without cache on read failure
|
|
686
686
|
pass
|
|
687
687
|
|
|
688
688
|
cached_etag = etag_cache.get(str(local_path))
|
|
@@ -1163,6 +1163,10 @@ class GitSkillSourceManager:
|
|
|
1163
1163
|
) -> List[str]:
|
|
1164
1164
|
"""Remove skills from target directory that aren't in the filtered skill list.
|
|
1165
1165
|
|
|
1166
|
+
CRITICAL: Only removes MPM-managed skills (those in our cache). Custom user skills
|
|
1167
|
+
are preserved. This prevents accidental deletion of user-created skills that were
|
|
1168
|
+
never part of MPM's skill repository.
|
|
1169
|
+
|
|
1166
1170
|
Uses fuzzy matching to handle both exact deployment names and short skill names.
|
|
1167
1171
|
For example:
|
|
1168
1172
|
- "toolchains-python-frameworks-flask" (deployed dir) matches "flask" (filter)
|
|
@@ -1213,6 +1217,40 @@ class GitSkillSourceManager:
|
|
|
1213
1217
|
|
|
1214
1218
|
return False
|
|
1215
1219
|
|
|
1220
|
+
def is_mpm_managed_skill(skill_dir_name: str) -> bool:
|
|
1221
|
+
"""Check if skill is managed by MPM (exists in our cache).
|
|
1222
|
+
|
|
1223
|
+
Custom user skills (not in cache) are NEVER deleted, even if not in filter.
|
|
1224
|
+
Only MPM-managed skills (in cache but not in filter) are candidates for removal.
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
skill_dir_name: Name of deployed skill directory
|
|
1228
|
+
|
|
1229
|
+
Returns:
|
|
1230
|
+
True if skill exists in MPM cache (MPM-managed), False if custom user skill
|
|
1231
|
+
"""
|
|
1232
|
+
# Check all configured skill sources for this skill
|
|
1233
|
+
for source in self.config.get_enabled_sources():
|
|
1234
|
+
cache_path = self._get_source_cache_path(source)
|
|
1235
|
+
if not cache_path.exists():
|
|
1236
|
+
continue
|
|
1237
|
+
|
|
1238
|
+
# Check if this skill directory exists anywhere in the cache
|
|
1239
|
+
# Use glob to find matching directories recursively
|
|
1240
|
+
matches = list(cache_path.rglob(f"*{skill_dir_name}*"))
|
|
1241
|
+
if matches:
|
|
1242
|
+
# Found in cache - this is MPM-managed
|
|
1243
|
+
self.logger.debug(
|
|
1244
|
+
f"Skill '{skill_dir_name}' found in cache at {matches[0]} - MPM-managed"
|
|
1245
|
+
)
|
|
1246
|
+
return True
|
|
1247
|
+
|
|
1248
|
+
# Not found in any cache - this is a custom user skill
|
|
1249
|
+
self.logger.debug(
|
|
1250
|
+
f"Skill '{skill_dir_name}' not found in cache - custom user skill, preserving"
|
|
1251
|
+
)
|
|
1252
|
+
return False
|
|
1253
|
+
|
|
1216
1254
|
# Check each directory in target_dir
|
|
1217
1255
|
if not target_dir.exists():
|
|
1218
1256
|
return removed_skills
|
|
@@ -1229,6 +1267,15 @@ class GitSkillSourceManager:
|
|
|
1229
1267
|
|
|
1230
1268
|
# Check if this skill directory should be kept (fuzzy matching)
|
|
1231
1269
|
if not should_keep_skill(item.name):
|
|
1270
|
+
# CRITICAL: Check if this is an MPM-managed skill before deletion
|
|
1271
|
+
if not is_mpm_managed_skill(item.name):
|
|
1272
|
+
# This is a custom user skill - NEVER delete
|
|
1273
|
+
self.logger.debug(
|
|
1274
|
+
f"Preserving custom user skill (not in MPM cache): {item.name}"
|
|
1275
|
+
)
|
|
1276
|
+
continue
|
|
1277
|
+
|
|
1278
|
+
# It's MPM-managed but not in filter - safe to remove
|
|
1232
1279
|
try:
|
|
1233
1280
|
# Security: Validate path is within target_dir
|
|
1234
1281
|
if not self._validate_safe_path(target_dir, item):
|
|
@@ -1244,7 +1291,9 @@ class GitSkillSourceManager:
|
|
|
1244
1291
|
shutil.rmtree(item)
|
|
1245
1292
|
|
|
1246
1293
|
removed_skills.append(item.name)
|
|
1247
|
-
self.logger.info(
|
|
1294
|
+
self.logger.info(
|
|
1295
|
+
f"Removed orphaned MPM-managed skill: {item.name}"
|
|
1296
|
+
)
|
|
1248
1297
|
|
|
1249
1298
|
except Exception as e:
|
|
1250
1299
|
self.logger.warning(
|