claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- 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 +44 -10
- 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 +216 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +32 -17
- claude_mpm/cli/parsers/base_parser.py +17 -0
- claude_mpm/cli/parsers/commander_parser.py +116 -0
- claude_mpm/cli/parsers/run_parser.py +10 -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 +124 -3
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +78 -0
- claude_mpm/commander/adapters/__init__.py +60 -0
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +288 -0
- claude_mpm/commander/adapters/claude_code.py +392 -0
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/communication.py +366 -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/__init__.py +16 -0
- claude_mpm/commander/api/app.py +121 -0
- claude_mpm/commander/api/errors.py +133 -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 +226 -0
- claude_mpm/commander/api/routes/work.py +296 -0
- claude_mpm/commander/api/schemas.py +186 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +111 -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/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 +594 -0
- claude_mpm/commander/env_loader.py +59 -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/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/__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 +410 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +346 -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 +207 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +241 -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/config/skill_sources.py +16 -0
- claude_mpm/core/claude_runner.py +143 -0
- claude_mpm/core/config.py +32 -19
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +35 -11
- claude_mpm/core/output_style_manager.py +49 -12
- 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/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
- claude_mpm/hooks/claude_hooks/event_handlers.py +112 -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 +116 -8
- claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
- claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.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__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
- claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
- claude_mpm/scripts/claude-hook-handler.sh +43 -16
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- 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 +130 -10
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +74 -4
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- 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-pause/SKILL.md +170 -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/skills/registry.py +295 -90
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Tmux orchestration layer for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module wraps tmux commands to manage sessions, panes, and I/O for
|
|
4
|
+
coordinating multiple project-level MPM instances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess # nosec B404 - Required for tmux interaction
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Dict, List
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TmuxNotFoundError(Exception):
|
|
17
|
+
"""Raised when tmux is not installed or not found in PATH."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str = "tmux not found. Please install tmux to use commander mode.",
|
|
22
|
+
):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TmuxOrchestrator:
|
|
29
|
+
"""Orchestrate multiple MPM sessions via tmux.
|
|
30
|
+
|
|
31
|
+
This class provides a high-level API for managing tmux sessions and panes,
|
|
32
|
+
enabling the MPM Commander to coordinate multiple project-level MPM instances.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
session_name: Name of the tmux session (default: "mpm-commander")
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
39
|
+
>>> orchestrator.create_session()
|
|
40
|
+
>>> target = orchestrator.create_pane("proj1", "/path/to/project")
|
|
41
|
+
>>> orchestrator.send_keys(target, "echo 'Hello from pane'")
|
|
42
|
+
>>> output = orchestrator.capture_output(target)
|
|
43
|
+
>>> print(output)
|
|
44
|
+
>>> orchestrator.kill_session()
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
session_name: str = "mpm-commander"
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
"""Verify tmux is available on initialization."""
|
|
51
|
+
if not shutil.which("tmux"):
|
|
52
|
+
raise TmuxNotFoundError()
|
|
53
|
+
|
|
54
|
+
def _run_tmux(
|
|
55
|
+
self, args: List[str], check: bool = True
|
|
56
|
+
) -> subprocess.CompletedProcess:
|
|
57
|
+
"""Execute tmux command and return result.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args: List of tmux command arguments
|
|
61
|
+
check: Whether to raise exception on non-zero exit code
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
CompletedProcess with stdout/stderr captured
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TmuxNotFoundError: If tmux binary not found
|
|
68
|
+
subprocess.CalledProcessError: If check=True and command fails
|
|
69
|
+
"""
|
|
70
|
+
cmd = ["tmux"] + args
|
|
71
|
+
logger.debug(f"Running tmux command: {' '.join(cmd)}")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=check) # nosec B603
|
|
75
|
+
|
|
76
|
+
if result.stdout:
|
|
77
|
+
logger.debug(f"tmux stdout: {result.stdout.strip()}")
|
|
78
|
+
if result.stderr:
|
|
79
|
+
logger.debug(f"tmux stderr: {result.stderr.strip()}")
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
except FileNotFoundError as err:
|
|
84
|
+
raise TmuxNotFoundError() from err
|
|
85
|
+
|
|
86
|
+
def session_exists(self) -> bool:
|
|
87
|
+
"""Check if commander session exists.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if session exists, False otherwise
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
94
|
+
>>> if not orchestrator.session_exists():
|
|
95
|
+
... orchestrator.create_session()
|
|
96
|
+
"""
|
|
97
|
+
result = self._run_tmux(["has-session", "-t", self.session_name], check=False)
|
|
98
|
+
exists = result.returncode == 0
|
|
99
|
+
logger.debug(f"Session '{self.session_name}' exists: {exists}")
|
|
100
|
+
return exists
|
|
101
|
+
|
|
102
|
+
def create_session(self) -> bool:
|
|
103
|
+
"""Create main commander tmux session if not exists.
|
|
104
|
+
|
|
105
|
+
Creates a detached tmux session for the commander. If the session
|
|
106
|
+
already exists, this is a no-op.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if session was created, False if it already existed
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
113
|
+
>>> orchestrator.create_session()
|
|
114
|
+
True
|
|
115
|
+
>>> orchestrator.create_session() # Already exists
|
|
116
|
+
False
|
|
117
|
+
"""
|
|
118
|
+
if self.session_exists():
|
|
119
|
+
logger.info(f"Session '{self.session_name}' already exists")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
logger.info(f"Creating tmux session '{self.session_name}'")
|
|
123
|
+
self._run_tmux(
|
|
124
|
+
[
|
|
125
|
+
"new-session",
|
|
126
|
+
"-d", # Detached
|
|
127
|
+
"-s",
|
|
128
|
+
self.session_name,
|
|
129
|
+
"-n",
|
|
130
|
+
"commander", # Window name
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def create_pane(self, pane_id: str, working_dir: str) -> str:
|
|
137
|
+
"""Create new pane for a project.
|
|
138
|
+
|
|
139
|
+
Creates a new split pane in the commander session with the specified
|
|
140
|
+
working directory.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
pane_id: Identifier for this pane (used in logging)
|
|
144
|
+
working_dir: Working directory for the pane
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Tmux target string (pane ID like "%0", "%1", etc.)
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
subprocess.CalledProcessError: If pane creation fails
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
154
|
+
>>> orchestrator.create_session()
|
|
155
|
+
>>> target = orchestrator.create_pane("my-project", "/Users/user/projects/my-project")
|
|
156
|
+
>>> print(target)
|
|
157
|
+
%1
|
|
158
|
+
"""
|
|
159
|
+
logger.info(f"Creating pane '{pane_id}' in {working_dir}")
|
|
160
|
+
|
|
161
|
+
# Split window to create new pane
|
|
162
|
+
self._run_tmux(
|
|
163
|
+
[
|
|
164
|
+
"split-window",
|
|
165
|
+
"-t",
|
|
166
|
+
self.session_name,
|
|
167
|
+
"-c",
|
|
168
|
+
working_dir, # Working directory
|
|
169
|
+
"-P", # Print target of new pane
|
|
170
|
+
"-F",
|
|
171
|
+
"#{pane_id}", # Format: just pane ID
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Get the newly created pane's target
|
|
176
|
+
# List panes and get the last one (most recently created)
|
|
177
|
+
result = self._run_tmux(
|
|
178
|
+
[
|
|
179
|
+
"list-panes",
|
|
180
|
+
"-t",
|
|
181
|
+
self.session_name,
|
|
182
|
+
"-F",
|
|
183
|
+
"#{pane_id}",
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
panes = [p for p in result.stdout.strip().split("\n") if p]
|
|
188
|
+
if not panes:
|
|
189
|
+
raise RuntimeError(f"Failed to create pane '{pane_id}'")
|
|
190
|
+
|
|
191
|
+
# Get last pane ID (most recently created)
|
|
192
|
+
# Pane ID already includes % prefix and can be used directly as target
|
|
193
|
+
new_pane_id = panes[-1]
|
|
194
|
+
target = new_pane_id # Use pane ID directly as target
|
|
195
|
+
|
|
196
|
+
logger.debug(f"Created pane with target: {target}")
|
|
197
|
+
return target
|
|
198
|
+
|
|
199
|
+
def send_keys(self, target: str, keys: str, enter: bool = True) -> bool:
|
|
200
|
+
"""Send keystrokes to a pane.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
target: Tmux target (from create_pane)
|
|
204
|
+
keys: Keys to send to the pane
|
|
205
|
+
enter: Whether to send Enter key after keys
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if successful
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
subprocess.CalledProcessError: If target pane doesn't exist
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
215
|
+
>>> orchestrator.create_session()
|
|
216
|
+
>>> target = orchestrator.create_pane("proj", "/tmp")
|
|
217
|
+
>>> orchestrator.send_keys(target, "echo 'Hello'")
|
|
218
|
+
>>> orchestrator.send_keys(target, "ls -la", enter=False)
|
|
219
|
+
"""
|
|
220
|
+
logger.debug(f"Sending keys to {target}: {keys}")
|
|
221
|
+
|
|
222
|
+
args = ["send-keys", "-t", target, keys]
|
|
223
|
+
if enter:
|
|
224
|
+
args.append("Enter")
|
|
225
|
+
|
|
226
|
+
self._run_tmux(args)
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
def capture_output(self, target: str, lines: int = 100) -> str:
|
|
230
|
+
"""Capture recent output from pane.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
target: Tmux target (from create_pane)
|
|
234
|
+
lines: Number of lines to capture from history
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Captured output as string
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
subprocess.CalledProcessError: If target pane doesn't exist
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
244
|
+
>>> orchestrator.create_session()
|
|
245
|
+
>>> target = orchestrator.create_pane("proj", "/tmp")
|
|
246
|
+
>>> orchestrator.send_keys(target, "echo 'Test output'")
|
|
247
|
+
>>> output = orchestrator.capture_output(target, lines=10)
|
|
248
|
+
>>> print(output)
|
|
249
|
+
Test output
|
|
250
|
+
"""
|
|
251
|
+
logger.debug(f"Capturing {lines} lines from {target}")
|
|
252
|
+
|
|
253
|
+
result = self._run_tmux(
|
|
254
|
+
[
|
|
255
|
+
"capture-pane",
|
|
256
|
+
"-t",
|
|
257
|
+
target,
|
|
258
|
+
"-p", # Print to stdout
|
|
259
|
+
"-S",
|
|
260
|
+
f"-{lines}", # Start from N lines back
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return result.stdout
|
|
265
|
+
|
|
266
|
+
def list_panes(self) -> List[Dict[str, str]]:
|
|
267
|
+
"""List all panes with their status.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of dicts with pane info (id, path, pid, active)
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
274
|
+
>>> orchestrator.create_session()
|
|
275
|
+
>>> panes = orchestrator.list_panes()
|
|
276
|
+
>>> for pane in panes:
|
|
277
|
+
... print(f"{pane['id']}: {pane['path']}")
|
|
278
|
+
%0: /Users/user/projects/proj1
|
|
279
|
+
%1: /Users/user/projects/proj2
|
|
280
|
+
"""
|
|
281
|
+
if not self.session_exists():
|
|
282
|
+
logger.warning(f"Session '{self.session_name}' does not exist")
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
logger.debug(f"Listing panes for session '{self.session_name}'")
|
|
286
|
+
|
|
287
|
+
result = self._run_tmux(
|
|
288
|
+
[
|
|
289
|
+
"list-panes",
|
|
290
|
+
"-t",
|
|
291
|
+
self.session_name,
|
|
292
|
+
"-F",
|
|
293
|
+
"#{pane_id}|#{pane_current_path}|#{pane_pid}|#{pane_active}",
|
|
294
|
+
]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
panes = []
|
|
298
|
+
for line in result.stdout.strip().split("\n"):
|
|
299
|
+
if not line:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
parts = line.split("|")
|
|
303
|
+
if len(parts) >= 4:
|
|
304
|
+
panes.append(
|
|
305
|
+
{
|
|
306
|
+
"id": parts[0],
|
|
307
|
+
"path": parts[1],
|
|
308
|
+
"pid": parts[2],
|
|
309
|
+
"active": parts[3] == "1",
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
logger.debug(f"Found {len(panes)} panes")
|
|
314
|
+
return panes
|
|
315
|
+
|
|
316
|
+
def kill_pane(self, target: str) -> bool:
|
|
317
|
+
"""Kill a specific pane.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
target: Tmux target (from create_pane or list_panes)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if successful
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
subprocess.CalledProcessError: If target pane doesn't exist
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
330
|
+
>>> orchestrator.create_session()
|
|
331
|
+
>>> target = orchestrator.create_pane("proj", "/tmp")
|
|
332
|
+
>>> orchestrator.kill_pane(target)
|
|
333
|
+
True
|
|
334
|
+
"""
|
|
335
|
+
logger.info(f"Killing pane {target}")
|
|
336
|
+
|
|
337
|
+
self._run_tmux(["kill-pane", "-t", target])
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
def kill_session(self) -> bool:
|
|
341
|
+
"""Kill the entire commander session.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
True if session was killed, False if it didn't exist
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> orchestrator = TmuxOrchestrator()
|
|
348
|
+
>>> orchestrator.create_session()
|
|
349
|
+
>>> orchestrator.kill_session()
|
|
350
|
+
True
|
|
351
|
+
>>> orchestrator.kill_session() # Already killed
|
|
352
|
+
False
|
|
353
|
+
"""
|
|
354
|
+
if not self.session_exists():
|
|
355
|
+
logger.info(f"Session '{self.session_name}' does not exist")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
logger.info(f"Killing session '{self.session_name}'")
|
|
359
|
+
self._run_tmux(["kill-session", "-t", self.session_name])
|
|
360
|
+
|
|
361
|
+
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web UI module for MPM Commander."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Work queue and execution module for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module provides work queue management and execution capabilities:
|
|
4
|
+
- WorkItem: Data model for work items
|
|
5
|
+
- WorkState: Lifecycle states (PENDING, QUEUED, IN_PROGRESS, etc.)
|
|
6
|
+
- WorkPriority: Priority levels (CRITICAL, HIGH, MEDIUM, LOW)
|
|
7
|
+
- WorkQueue: Queue management with priority and dependencies
|
|
8
|
+
- WorkExecutor: Execution via RuntimeExecutor
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from claude_mpm.commander.work import (
|
|
12
|
+
... WorkQueue, WorkExecutor, WorkItem, WorkState, WorkPriority
|
|
13
|
+
... )
|
|
14
|
+
>>> queue = WorkQueue("proj-123")
|
|
15
|
+
>>> work = queue.add("Implement feature", WorkPriority.HIGH)
|
|
16
|
+
>>> executor = WorkExecutor(runtime, queue)
|
|
17
|
+
>>> await executor.execute_next()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ..models.work import WorkItem, WorkPriority, WorkState
|
|
21
|
+
from .executor import WorkExecutor
|
|
22
|
+
from .queue import WorkQueue
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"WorkExecutor",
|
|
26
|
+
"WorkItem",
|
|
27
|
+
"WorkPriority",
|
|
28
|
+
"WorkQueue",
|
|
29
|
+
"WorkState",
|
|
30
|
+
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Work executor for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module provides WorkExecutor which executes work items
|
|
4
|
+
via RuntimeExecutor and handles completion/failure callbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..models.work import WorkItem
|
|
11
|
+
from ..runtime.executor import RuntimeExecutor
|
|
12
|
+
from .queue import WorkQueue
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkExecutor:
|
|
18
|
+
"""Executes work items via RuntimeExecutor.
|
|
19
|
+
|
|
20
|
+
Coordinates between work queue and runtime execution,
|
|
21
|
+
handling work item lifecycle and callbacks.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
runtime: RuntimeExecutor for spawning and managing tools
|
|
25
|
+
queue: WorkQueue for work item management
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> executor = WorkExecutor(runtime, queue)
|
|
29
|
+
>>> executed = await executor.execute_next()
|
|
30
|
+
>>> if executed:
|
|
31
|
+
... print("Work item started")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, runtime: RuntimeExecutor, queue: WorkQueue):
|
|
35
|
+
"""Initialize work executor.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
runtime: RuntimeExecutor instance
|
|
39
|
+
queue: WorkQueue instance
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If runtime or queue is None
|
|
43
|
+
"""
|
|
44
|
+
if runtime is None:
|
|
45
|
+
raise ValueError("Runtime cannot be None")
|
|
46
|
+
if queue is None:
|
|
47
|
+
raise ValueError("Queue cannot be None")
|
|
48
|
+
|
|
49
|
+
self.runtime = runtime
|
|
50
|
+
self.queue = queue
|
|
51
|
+
|
|
52
|
+
logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
|
|
53
|
+
|
|
54
|
+
async def execute_next(self, pane_target: Optional[str] = None) -> bool:
|
|
55
|
+
"""Execute next available work item.
|
|
56
|
+
|
|
57
|
+
Gets next work from queue, starts it, and executes via RuntimeExecutor.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
pane_target: Optional tmux pane target for execution
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if work was executed, False if queue empty/blocked
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> executed = await executor.execute_next("%5")
|
|
67
|
+
>>> if not executed:
|
|
68
|
+
... print("No work available")
|
|
69
|
+
"""
|
|
70
|
+
# Get next work item
|
|
71
|
+
work_item = self.queue.get_next()
|
|
72
|
+
if not work_item:
|
|
73
|
+
logger.debug(f"No work available for project {self.queue.project_id}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Execute the work item
|
|
77
|
+
await self.execute(work_item, pane_target)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
async def execute(
|
|
81
|
+
self, work_item: WorkItem, pane_target: Optional[str] = None
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Execute a specific work item.
|
|
84
|
+
|
|
85
|
+
Marks work as IN_PROGRESS and sends to RuntimeExecutor.
|
|
86
|
+
Note: This is async but returns immediately - actual execution
|
|
87
|
+
happens in the background via tmux.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
work_item: WorkItem to execute
|
|
91
|
+
pane_target: Optional tmux pane target for execution
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RuntimeError: If execution fails
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> await executor.execute(work_item, "%5")
|
|
98
|
+
"""
|
|
99
|
+
# Mark as in progress
|
|
100
|
+
if not self.queue.start(work_item.id):
|
|
101
|
+
logger.error(
|
|
102
|
+
f"Failed to start work item {work_item.id} - "
|
|
103
|
+
f"invalid state: {work_item.state.value}"
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Executing work item {work_item.id} for project {work_item.project_id}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
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
|
+
)
|
|
123
|
+
|
|
124
|
+
# Store work item ID in metadata for callback tracking
|
|
125
|
+
work_item.metadata["execution_started"] = True
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to execute work item {work_item.id}: {e}")
|
|
129
|
+
await self.handle_failure(work_item.id, str(e))
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
async def handle_completion(
|
|
133
|
+
self, work_id: str, result: Optional[str] = None
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Handle work completion callback.
|
|
136
|
+
|
|
137
|
+
Called when RuntimeExecutor completes work successfully.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
work_id: Work item ID that completed
|
|
141
|
+
result: Optional result message
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> await executor.handle_completion("work-123", "Feature implemented")
|
|
145
|
+
"""
|
|
146
|
+
if self.queue.complete(work_id, result):
|
|
147
|
+
logger.info(f"Work item {work_id} completed successfully")
|
|
148
|
+
else:
|
|
149
|
+
logger.warning(f"Failed to mark work item {work_id} as completed")
|
|
150
|
+
|
|
151
|
+
async def handle_failure(self, work_id: str, error: str) -> None:
|
|
152
|
+
"""Handle work failure callback.
|
|
153
|
+
|
|
154
|
+
Called when RuntimeExecutor encounters an error.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
work_id: Work item ID that failed
|
|
158
|
+
error: Error message
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> await executor.handle_failure("work-123", "Tool crashed")
|
|
162
|
+
"""
|
|
163
|
+
if self.queue.fail(work_id, error):
|
|
164
|
+
logger.error(f"Work item {work_id} failed: {error}")
|
|
165
|
+
else:
|
|
166
|
+
logger.warning(f"Failed to mark work item {work_id} as failed")
|
|
167
|
+
|
|
168
|
+
async def handle_block(self, work_id: str, reason: str) -> bool:
|
|
169
|
+
"""Handle work being blocked by an event.
|
|
170
|
+
|
|
171
|
+
Called when RuntimeMonitor detects a blocking event.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
work_id: Work item ID that is blocked
|
|
175
|
+
reason: Reason for blocking (e.g., "Waiting for approval")
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if work was successfully blocked, False otherwise
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> success = await executor.handle_block("work-123", "Decision needed")
|
|
182
|
+
"""
|
|
183
|
+
if self.queue.block(work_id, reason):
|
|
184
|
+
logger.info(f"Work item {work_id} blocked: {reason}")
|
|
185
|
+
return True
|
|
186
|
+
logger.warning(f"Failed to mark work item {work_id} as blocked")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
async def handle_unblock(self, work_id: str) -> bool:
|
|
190
|
+
"""Handle work being unblocked after event resolution.
|
|
191
|
+
|
|
192
|
+
Called when EventHandler resolves a blocking event.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
work_id: Work item ID to unblock
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if work was successfully unblocked, False otherwise
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> success = await executor.handle_unblock("work-123")
|
|
202
|
+
"""
|
|
203
|
+
if self.queue.unblock(work_id):
|
|
204
|
+
logger.info(f"Work item {work_id} unblocked, resuming execution")
|
|
205
|
+
return True
|
|
206
|
+
logger.warning(f"Failed to unblock work item {work_id}")
|
|
207
|
+
return False
|