claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -10,8 +10,9 @@ import logging
|
|
|
10
10
|
import tempfile
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
14
|
|
|
15
|
+
from ..frameworks.base import RegisteredInstance
|
|
15
16
|
from ..models import Project, ProjectState, ToolSession
|
|
16
17
|
from ..registry import ProjectRegistry
|
|
17
18
|
|
|
@@ -48,6 +49,7 @@ class StateStore:
|
|
|
48
49
|
|
|
49
50
|
self.projects_path = self.state_dir / "projects.json"
|
|
50
51
|
self.sessions_path = self.state_dir / "sessions.json"
|
|
52
|
+
self.instances_path = self.state_dir / "instances.json"
|
|
51
53
|
|
|
52
54
|
logger.info(f"Initialized StateStore at {self.state_dir}")
|
|
53
55
|
|
|
@@ -307,3 +309,95 @@ class StateStore:
|
|
|
307
309
|
else None
|
|
308
310
|
),
|
|
309
311
|
)
|
|
312
|
+
|
|
313
|
+
# Instance persistence methods
|
|
314
|
+
|
|
315
|
+
def save_instances(self, instances: Dict[str, RegisteredInstance]) -> None:
|
|
316
|
+
"""Save registered instances to disk.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
instances: Dict mapping instance name to RegisteredInstance
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
IOError: If write fails
|
|
323
|
+
"""
|
|
324
|
+
data = {
|
|
325
|
+
"version": self.VERSION,
|
|
326
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
327
|
+
"instances": {name: inst.to_dict() for name, inst in instances.items()},
|
|
328
|
+
}
|
|
329
|
+
self._atomic_write(self.instances_path, data)
|
|
330
|
+
logger.info(f"Saved {len(instances)} instances to {self.instances_path}")
|
|
331
|
+
|
|
332
|
+
def load_instances(self) -> Dict[str, RegisteredInstance]:
|
|
333
|
+
"""Load registered instances from disk.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dict mapping instance name to RegisteredInstance
|
|
337
|
+
(empty if file missing or corrupt)
|
|
338
|
+
"""
|
|
339
|
+
if not self.instances_path.exists():
|
|
340
|
+
logger.info("No instances file found, returning empty dict")
|
|
341
|
+
return {}
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
data = self._read_json(self.instances_path)
|
|
345
|
+
|
|
346
|
+
if data.get("version") != self.VERSION:
|
|
347
|
+
logger.warning(
|
|
348
|
+
f"Version mismatch: expected {self.VERSION}, "
|
|
349
|
+
f"got {data.get('version')}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
instances = {
|
|
353
|
+
name: RegisteredInstance.from_dict(inst_data)
|
|
354
|
+
for name, inst_data in data.get("instances", {}).items()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
logger.info(f"Loaded {len(instances)} instances from {self.instances_path}")
|
|
358
|
+
return instances
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Failed to load instances: {e}", exc_info=True)
|
|
362
|
+
return {}
|
|
363
|
+
|
|
364
|
+
def register_instance(self, instance: RegisteredInstance) -> None:
|
|
365
|
+
"""Register a single instance (add to existing).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
instance: RegisteredInstance to add
|
|
369
|
+
"""
|
|
370
|
+
instances = self.load_instances()
|
|
371
|
+
instances[instance.name] = instance
|
|
372
|
+
self.save_instances(instances)
|
|
373
|
+
logger.info(f"Registered instance '{instance.name}'")
|
|
374
|
+
|
|
375
|
+
def unregister_instance(self, name: str) -> bool:
|
|
376
|
+
"""Remove an instance registration.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
name: Instance name to remove
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if instance was found and removed, False if not found
|
|
383
|
+
"""
|
|
384
|
+
instances = self.load_instances()
|
|
385
|
+
if name in instances:
|
|
386
|
+
del instances[name]
|
|
387
|
+
self.save_instances(instances)
|
|
388
|
+
logger.info(f"Unregistered instance '{name}'")
|
|
389
|
+
return True
|
|
390
|
+
logger.warning(f"Instance '{name}' not found for unregistration")
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
def get_registered_instance(self, name: str) -> Optional[RegisteredInstance]:
|
|
394
|
+
"""Get a single registered instance by name.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
name: Instance name to look up
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
RegisteredInstance if found, None otherwise
|
|
401
|
+
"""
|
|
402
|
+
instances = self.load_instances()
|
|
403
|
+
return instances.get(name)
|
claude_mpm/commander/registry.py
CHANGED
|
@@ -42,15 +42,18 @@ class ProjectRegistry:
|
|
|
42
42
|
self._lock = threading.RLock()
|
|
43
43
|
logger.info("Initialized ProjectRegistry")
|
|
44
44
|
|
|
45
|
-
def register(
|
|
45
|
+
def register(
|
|
46
|
+
self, path: str, name: Optional[str] = None, project_id: Optional[str] = None
|
|
47
|
+
) -> Project:
|
|
46
48
|
"""Register a new project.
|
|
47
49
|
|
|
48
|
-
Creates a new project with unique UUID and adds it to
|
|
50
|
+
Creates a new project with unique UUID (or user-provided ID) and adds it to registry.
|
|
49
51
|
Path must be a valid directory and cannot already be registered.
|
|
50
52
|
|
|
51
53
|
Args:
|
|
52
54
|
path: Absolute filesystem path to project directory
|
|
53
55
|
name: Optional human-readable name (defaults to directory name)
|
|
56
|
+
project_id: Optional project identifier (UUID generated if omitted)
|
|
54
57
|
|
|
55
58
|
Returns:
|
|
56
59
|
Newly created Project instance
|
|
@@ -92,8 +95,11 @@ class ProjectRegistry:
|
|
|
92
95
|
if name is None:
|
|
93
96
|
name = path_obj.name
|
|
94
97
|
|
|
95
|
-
# Generate unique project ID
|
|
96
|
-
project_id
|
|
98
|
+
# Generate unique project ID if not provided
|
|
99
|
+
if project_id is None:
|
|
100
|
+
project_id = str(uuid.uuid4())
|
|
101
|
+
elif project_id in self._projects:
|
|
102
|
+
raise ValueError(f"Project ID already exists: {project_id}")
|
|
97
103
|
|
|
98
104
|
# Create project instance
|
|
99
105
|
project = Project(
|
|
@@ -6,13 +6,16 @@ and detects events using OutputParser.
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Dict, List, Optional
|
|
9
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
10
10
|
|
|
11
11
|
from ..events.manager import EventManager
|
|
12
12
|
from ..models.events import Event
|
|
13
13
|
from ..parsing.output_parser import OutputParser
|
|
14
14
|
from ..tmux_orchestrator import TmuxOrchestrator
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..core.block_manager import BlockManager
|
|
18
|
+
|
|
16
19
|
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
@@ -44,6 +47,7 @@ class RuntimeMonitor:
|
|
|
44
47
|
event_manager: EventManager,
|
|
45
48
|
poll_interval: float = 2.0,
|
|
46
49
|
capture_lines: int = 1000,
|
|
50
|
+
block_manager: Optional["BlockManager"] = None,
|
|
47
51
|
):
|
|
48
52
|
"""Initialize runtime monitor.
|
|
49
53
|
|
|
@@ -53,6 +57,7 @@ class RuntimeMonitor:
|
|
|
53
57
|
event_manager: EventManager for emitting events
|
|
54
58
|
poll_interval: Seconds between polls (default: 2.0)
|
|
55
59
|
capture_lines: Number of lines to capture (default: 1000)
|
|
60
|
+
block_manager: Optional BlockManager for automatic work blocking
|
|
56
61
|
|
|
57
62
|
Raises:
|
|
58
63
|
ValueError: If any required parameter is None
|
|
@@ -69,15 +74,17 @@ class RuntimeMonitor:
|
|
|
69
74
|
self.event_manager = event_manager
|
|
70
75
|
self.poll_interval = poll_interval
|
|
71
76
|
self.capture_lines = capture_lines
|
|
77
|
+
self.block_manager = block_manager
|
|
72
78
|
|
|
73
79
|
# Track active monitors: pane_target -> (project_id, task, last_output_hash)
|
|
74
80
|
self._monitors: Dict[str, tuple[str, Optional[asyncio.Task], int]] = {}
|
|
75
81
|
self._running = False
|
|
76
82
|
|
|
77
83
|
logger.debug(
|
|
78
|
-
"RuntimeMonitor initialized (interval: %.2fs, lines: %d)",
|
|
84
|
+
"RuntimeMonitor initialized (interval: %.2fs, lines: %d, block_manager: %s)",
|
|
79
85
|
poll_interval,
|
|
80
86
|
capture_lines,
|
|
87
|
+
"enabled" if block_manager else "disabled",
|
|
81
88
|
)
|
|
82
89
|
|
|
83
90
|
async def start_monitoring(self, pane_target: str, project_id: str) -> None:
|
|
@@ -284,6 +291,29 @@ class RuntimeMonitor:
|
|
|
284
291
|
pane_target,
|
|
285
292
|
)
|
|
286
293
|
|
|
294
|
+
# Automatically block work for blocking events
|
|
295
|
+
if self.block_manager:
|
|
296
|
+
for parse_result in parse_results:
|
|
297
|
+
# Get the created event from EventManager
|
|
298
|
+
# Events are created with matching titles, so find by title
|
|
299
|
+
pending_events = self.event_manager.get_pending(project_id)
|
|
300
|
+
for event in pending_events:
|
|
301
|
+
if (
|
|
302
|
+
event.title == parse_result.title
|
|
303
|
+
and event.is_blocking
|
|
304
|
+
):
|
|
305
|
+
blocked_work = (
|
|
306
|
+
await self.block_manager.check_and_block(event)
|
|
307
|
+
)
|
|
308
|
+
if blocked_work:
|
|
309
|
+
logger.info(
|
|
310
|
+
"Event %s blocked %d work items: %s",
|
|
311
|
+
event.id,
|
|
312
|
+
len(blocked_work),
|
|
313
|
+
blocked_work,
|
|
314
|
+
)
|
|
315
|
+
break
|
|
316
|
+
|
|
287
317
|
except Exception as e:
|
|
288
318
|
logger.error(
|
|
289
319
|
"Error in monitor loop for pane %s: %s",
|
|
@@ -158,10 +158,11 @@ class TmuxOrchestrator:
|
|
|
158
158
|
"""
|
|
159
159
|
logger.info(f"Creating pane '{pane_id}' in {working_dir}")
|
|
160
160
|
|
|
161
|
-
#
|
|
161
|
+
# Create new window instead of splitting pane to avoid "no space for new pane" error
|
|
162
|
+
# when tmux window is too small to split
|
|
162
163
|
self._run_tmux(
|
|
163
164
|
[
|
|
164
|
-
"
|
|
165
|
+
"new-window",
|
|
165
166
|
"-t",
|
|
166
167
|
self.session_name,
|
|
167
168
|
"-c",
|
|
@@ -51,16 +51,19 @@ class WorkExecutor:
|
|
|
51
51
|
|
|
52
52
|
logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
|
|
53
53
|
|
|
54
|
-
async def execute_next(self) -> bool:
|
|
54
|
+
async def execute_next(self, pane_target: Optional[str] = None) -> bool:
|
|
55
55
|
"""Execute next available work item.
|
|
56
56
|
|
|
57
57
|
Gets next work from queue, starts it, and executes via RuntimeExecutor.
|
|
58
58
|
|
|
59
|
+
Args:
|
|
60
|
+
pane_target: Optional tmux pane target for execution
|
|
61
|
+
|
|
59
62
|
Returns:
|
|
60
63
|
True if work was executed, False if queue empty/blocked
|
|
61
64
|
|
|
62
65
|
Example:
|
|
63
|
-
>>> executed = await executor.execute_next()
|
|
66
|
+
>>> executed = await executor.execute_next("%5")
|
|
64
67
|
>>> if not executed:
|
|
65
68
|
... print("No work available")
|
|
66
69
|
"""
|
|
@@ -71,10 +74,12 @@ class WorkExecutor:
|
|
|
71
74
|
return False
|
|
72
75
|
|
|
73
76
|
# Execute the work item
|
|
74
|
-
await self.execute(work_item)
|
|
77
|
+
await self.execute(work_item, pane_target)
|
|
75
78
|
return True
|
|
76
79
|
|
|
77
|
-
async def execute(
|
|
80
|
+
async def execute(
|
|
81
|
+
self, work_item: WorkItem, pane_target: Optional[str] = None
|
|
82
|
+
) -> None:
|
|
78
83
|
"""Execute a specific work item.
|
|
79
84
|
|
|
80
85
|
Marks work as IN_PROGRESS and sends to RuntimeExecutor.
|
|
@@ -83,12 +88,13 @@ class WorkExecutor:
|
|
|
83
88
|
|
|
84
89
|
Args:
|
|
85
90
|
work_item: WorkItem to execute
|
|
91
|
+
pane_target: Optional tmux pane target for execution
|
|
86
92
|
|
|
87
93
|
Raises:
|
|
88
94
|
RuntimeError: If execution fails
|
|
89
95
|
|
|
90
96
|
Example:
|
|
91
|
-
>>> await executor.execute(work_item)
|
|
97
|
+
>>> await executor.execute(work_item, "%5")
|
|
92
98
|
"""
|
|
93
99
|
# Mark as in progress
|
|
94
100
|
if not self.queue.start(work_item.id):
|
|
@@ -103,17 +109,21 @@ class WorkExecutor:
|
|
|
103
109
|
)
|
|
104
110
|
|
|
105
111
|
try:
|
|
106
|
-
# Send work content to runtime
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
# Send work content to runtime if pane target provided
|
|
113
|
+
if pane_target:
|
|
114
|
+
await self.runtime.send_message(pane_target, work_item.content)
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Work item {work_item.id} sent to pane {pane_target} for execution"
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
logger.warning(
|
|
120
|
+
f"No pane target provided for work item {work_item.id}, "
|
|
121
|
+
f"work marked as in-progress but not sent to runtime"
|
|
122
|
+
)
|
|
111
123
|
|
|
112
124
|
# Store work item ID in metadata for callback tracking
|
|
113
125
|
work_item.metadata["execution_started"] = True
|
|
114
126
|
|
|
115
|
-
logger.info(f"Work item {work_item.id} sent to runtime for execution")
|
|
116
|
-
|
|
117
127
|
except Exception as e:
|
|
118
128
|
logger.error(f"Failed to execute work item {work_item.id}: {e}")
|
|
119
129
|
await self.handle_failure(work_item.id, str(e))
|
|
@@ -155,7 +165,7 @@ class WorkExecutor:
|
|
|
155
165
|
else:
|
|
156
166
|
logger.warning(f"Failed to mark work item {work_id} as failed")
|
|
157
167
|
|
|
158
|
-
async def handle_block(self, work_id: str, reason: str) ->
|
|
168
|
+
async def handle_block(self, work_id: str, reason: str) -> bool:
|
|
159
169
|
"""Handle work being blocked by an event.
|
|
160
170
|
|
|
161
171
|
Called when RuntimeMonitor detects a blocking event.
|
|
@@ -164,15 +174,19 @@ class WorkExecutor:
|
|
|
164
174
|
work_id: Work item ID that is blocked
|
|
165
175
|
reason: Reason for blocking (e.g., "Waiting for approval")
|
|
166
176
|
|
|
177
|
+
Returns:
|
|
178
|
+
True if work was successfully blocked, False otherwise
|
|
179
|
+
|
|
167
180
|
Example:
|
|
168
|
-
>>> await executor.handle_block("work-123", "Decision needed")
|
|
181
|
+
>>> success = await executor.handle_block("work-123", "Decision needed")
|
|
169
182
|
"""
|
|
170
183
|
if self.queue.block(work_id, reason):
|
|
171
184
|
logger.info(f"Work item {work_id} blocked: {reason}")
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
return True
|
|
186
|
+
logger.warning(f"Failed to mark work item {work_id} as blocked")
|
|
187
|
+
return False
|
|
174
188
|
|
|
175
|
-
async def handle_unblock(self, work_id: str) ->
|
|
189
|
+
async def handle_unblock(self, work_id: str) -> bool:
|
|
176
190
|
"""Handle work being unblocked after event resolution.
|
|
177
191
|
|
|
178
192
|
Called when EventHandler resolves a blocking event.
|
|
@@ -180,10 +194,14 @@ class WorkExecutor:
|
|
|
180
194
|
Args:
|
|
181
195
|
work_id: Work item ID to unblock
|
|
182
196
|
|
|
197
|
+
Returns:
|
|
198
|
+
True if work was successfully unblocked, False otherwise
|
|
199
|
+
|
|
183
200
|
Example:
|
|
184
|
-
>>> await executor.handle_unblock("work-123")
|
|
201
|
+
>>> success = await executor.handle_unblock("work-123")
|
|
185
202
|
"""
|
|
186
203
|
if self.queue.unblock(work_id):
|
|
187
204
|
logger.info(f"Work item {work_id} unblocked, resuming execution")
|
|
188
|
-
|
|
189
|
-
|
|
205
|
+
return True
|
|
206
|
+
logger.warning(f"Failed to unblock work item {work_id}")
|
|
207
|
+
return False
|
|
@@ -5,12 +5,15 @@ user input and coordinates session pause/resume.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Dict, List, Optional
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
from ..inbox import Inbox
|
|
11
11
|
from ..models.events import BLOCKING_EVENTS, Event, EventStatus
|
|
12
12
|
from ..project_session import ProjectSession
|
|
13
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..core.block_manager import BlockManager
|
|
16
|
+
|
|
14
17
|
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -32,13 +35,17 @@ class EventHandler:
|
|
|
32
35
|
"""
|
|
33
36
|
|
|
34
37
|
def __init__(
|
|
35
|
-
self,
|
|
38
|
+
self,
|
|
39
|
+
inbox: Inbox,
|
|
40
|
+
session_manager: Dict[str, ProjectSession],
|
|
41
|
+
block_manager: Optional["BlockManager"] = None,
|
|
36
42
|
) -> None:
|
|
37
43
|
"""Initialize event handler.
|
|
38
44
|
|
|
39
45
|
Args:
|
|
40
46
|
inbox: Inbox instance for event access
|
|
41
47
|
session_manager: Dict mapping project_id -> ProjectSession
|
|
48
|
+
block_manager: Optional BlockManager for automatic work unblocking
|
|
42
49
|
|
|
43
50
|
Raises:
|
|
44
51
|
ValueError: If inbox or session_manager is None
|
|
@@ -51,8 +58,12 @@ class EventHandler:
|
|
|
51
58
|
self.inbox = inbox
|
|
52
59
|
self.session_manager = session_manager
|
|
53
60
|
self._event_manager = inbox.events
|
|
61
|
+
self.block_manager = block_manager
|
|
54
62
|
|
|
55
|
-
logger.debug(
|
|
63
|
+
logger.debug(
|
|
64
|
+
"EventHandler initialized (block_manager: %s)",
|
|
65
|
+
"enabled" if block_manager else "disabled",
|
|
66
|
+
)
|
|
56
67
|
|
|
57
68
|
async def process_event(self, event: Event) -> None:
|
|
58
69
|
"""Process an event - pause session if blocking.
|
|
@@ -137,6 +148,17 @@ class EventHandler:
|
|
|
137
148
|
# Mark event as resolved
|
|
138
149
|
self._event_manager.respond(event_id, response)
|
|
139
150
|
|
|
151
|
+
# Automatically unblock work items if BlockManager is available
|
|
152
|
+
if self.block_manager and was_blocking:
|
|
153
|
+
unblocked_work = await self.block_manager.check_and_unblock(event_id)
|
|
154
|
+
if unblocked_work:
|
|
155
|
+
logger.info(
|
|
156
|
+
"Event %s resolution unblocked %d work items: %s",
|
|
157
|
+
event_id,
|
|
158
|
+
len(unblocked_work),
|
|
159
|
+
unblocked_work,
|
|
160
|
+
)
|
|
161
|
+
|
|
140
162
|
# If event was NOT blocking, no need to resume
|
|
141
163
|
if not was_blocking:
|
|
142
164
|
logger.debug("Event %s was non-blocking, no resume needed", event_id)
|
|
@@ -54,6 +54,7 @@ class SkillSource:
|
|
|
54
54
|
branch: Git branch to use (default: "main")
|
|
55
55
|
priority: Priority for skill resolution (lower = higher precedence)
|
|
56
56
|
enabled: Whether this source should be synced
|
|
57
|
+
token: Optional GitHub token or env var reference (e.g., "$MY_TOKEN")
|
|
57
58
|
|
|
58
59
|
Priority System:
|
|
59
60
|
- 0: Reserved for system repository (highest precedence)
|
|
@@ -61,6 +62,12 @@ class SkillSource:
|
|
|
61
62
|
- 100-999: Normal priority custom sources
|
|
62
63
|
- 1000+: Low priority custom sources
|
|
63
64
|
|
|
65
|
+
Token Authentication:
|
|
66
|
+
- Direct token: "ghp_xxxxx" (stored in config, not recommended)
|
|
67
|
+
- Env var reference: "$PRIVATE_REPO_TOKEN" (resolved at runtime)
|
|
68
|
+
- If None, falls back to GITHUB_TOKEN or GH_TOKEN env vars
|
|
69
|
+
- Priority: source.token > GITHUB_TOKEN > GH_TOKEN
|
|
70
|
+
|
|
64
71
|
Example:
|
|
65
72
|
>>> source = SkillSource(
|
|
66
73
|
... id="system",
|
|
@@ -70,6 +77,12 @@ class SkillSource:
|
|
|
70
77
|
... )
|
|
71
78
|
>>> source.validate()
|
|
72
79
|
[]
|
|
80
|
+
>>> private_source = SkillSource(
|
|
81
|
+
... id="private",
|
|
82
|
+
... type="git",
|
|
83
|
+
... url="https://github.com/myorg/private-skills",
|
|
84
|
+
... token="$PRIVATE_REPO_TOKEN"
|
|
85
|
+
... )
|
|
73
86
|
"""
|
|
74
87
|
|
|
75
88
|
id: str
|
|
@@ -78,6 +91,7 @@ class SkillSource:
|
|
|
78
91
|
branch: str = "main"
|
|
79
92
|
priority: int = 100
|
|
80
93
|
enabled: bool = True
|
|
94
|
+
token: Optional[str] = None
|
|
81
95
|
|
|
82
96
|
def __post_init__(self):
|
|
83
97
|
"""Validate skill source configuration after initialization.
|
|
@@ -262,6 +276,7 @@ class SkillSourceConfiguration:
|
|
|
262
276
|
branch=source_data.get("branch", "main"),
|
|
263
277
|
priority=source_data.get("priority", 100),
|
|
264
278
|
enabled=source_data.get("enabled", True),
|
|
279
|
+
token=source_data.get("token"),
|
|
265
280
|
)
|
|
266
281
|
sources.append(source)
|
|
267
282
|
except (KeyError, ValueError) as e:
|
|
@@ -326,6 +341,7 @@ class SkillSourceConfiguration:
|
|
|
326
341
|
"branch": source.branch,
|
|
327
342
|
"priority": source.priority,
|
|
328
343
|
"enabled": source.enabled,
|
|
344
|
+
**({"token": source.token} if source.token else {}),
|
|
329
345
|
}
|
|
330
346
|
for source in sources
|
|
331
347
|
]
|
claude_mpm/constants.py
CHANGED
|
@@ -46,6 +46,7 @@ class CLICommands(str, Enum):
|
|
|
46
46
|
DASHBOARD = "dashboard"
|
|
47
47
|
UPGRADE = "upgrade"
|
|
48
48
|
SKILLS = "skills"
|
|
49
|
+
OAUTH = "oauth"
|
|
49
50
|
|
|
50
51
|
def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
|
|
51
52
|
"""Get command with prefix."""
|
|
@@ -143,6 +144,10 @@ class MCPCommands(str, Enum):
|
|
|
143
144
|
CONFIG = "config"
|
|
144
145
|
SERVER = "server"
|
|
145
146
|
EXTERNAL = "external"
|
|
147
|
+
# Service management commands
|
|
148
|
+
ENABLE = "enable"
|
|
149
|
+
DISABLE = "disable"
|
|
150
|
+
LIST = "list"
|
|
146
151
|
|
|
147
152
|
|
|
148
153
|
class TicketCommands(str, Enum):
|