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,346 @@
|
|
|
1
|
+
"""Runtime monitor for continuous output monitoring and event detection.
|
|
2
|
+
|
|
3
|
+
This module provides RuntimeMonitor which continuously polls tmux pane output
|
|
4
|
+
and detects events using OutputParser.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..events.manager import EventManager
|
|
12
|
+
from ..models.events import Event
|
|
13
|
+
from ..parsing.output_parser import OutputParser
|
|
14
|
+
from ..tmux_orchestrator import TmuxOrchestrator
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..core.block_manager import BlockManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RuntimeMonitor:
|
|
23
|
+
"""Monitors tmux pane output and detects events.
|
|
24
|
+
|
|
25
|
+
This class continuously polls tmux pane output, uses OutputParser to detect
|
|
26
|
+
events, and emits them via EventManager. Supports starting/stopping monitoring
|
|
27
|
+
for individual panes and tracking which panes are actively monitored.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
orchestrator: TmuxOrchestrator for capturing output
|
|
31
|
+
parser: OutputParser for event detection
|
|
32
|
+
event_manager: EventManager for emitting events
|
|
33
|
+
poll_interval: Seconds between polls (default: 2.0)
|
|
34
|
+
capture_lines: Number of lines to capture from tmux (default: 1000)
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> monitor = RuntimeMonitor(orchestrator, parser, event_manager)
|
|
38
|
+
>>> await monitor.start_monitoring("%5", "proj_123")
|
|
39
|
+
>>> events = await monitor.poll_once("%5")
|
|
40
|
+
>>> await monitor.stop_monitoring("%5")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
orchestrator: TmuxOrchestrator,
|
|
46
|
+
parser: OutputParser,
|
|
47
|
+
event_manager: EventManager,
|
|
48
|
+
poll_interval: float = 2.0,
|
|
49
|
+
capture_lines: int = 1000,
|
|
50
|
+
block_manager: Optional["BlockManager"] = None,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize runtime monitor.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
orchestrator: TmuxOrchestrator for capturing output
|
|
56
|
+
parser: OutputParser for event detection
|
|
57
|
+
event_manager: EventManager for emitting events
|
|
58
|
+
poll_interval: Seconds between polls (default: 2.0)
|
|
59
|
+
capture_lines: Number of lines to capture (default: 1000)
|
|
60
|
+
block_manager: Optional BlockManager for automatic work blocking
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If any required parameter is None
|
|
64
|
+
"""
|
|
65
|
+
if orchestrator is None:
|
|
66
|
+
raise ValueError("Orchestrator cannot be None")
|
|
67
|
+
if parser is None:
|
|
68
|
+
raise ValueError("Parser cannot be None")
|
|
69
|
+
if event_manager is None:
|
|
70
|
+
raise ValueError("EventManager cannot be None")
|
|
71
|
+
|
|
72
|
+
self.orchestrator = orchestrator
|
|
73
|
+
self.parser = parser
|
|
74
|
+
self.event_manager = event_manager
|
|
75
|
+
self.poll_interval = poll_interval
|
|
76
|
+
self.capture_lines = capture_lines
|
|
77
|
+
self.block_manager = block_manager
|
|
78
|
+
|
|
79
|
+
# Track active monitors: pane_target -> (project_id, task, last_output_hash)
|
|
80
|
+
self._monitors: Dict[str, tuple[str, Optional[asyncio.Task], int]] = {}
|
|
81
|
+
self._running = False
|
|
82
|
+
|
|
83
|
+
logger.debug(
|
|
84
|
+
"RuntimeMonitor initialized (interval: %.2fs, lines: %d, block_manager: %s)",
|
|
85
|
+
poll_interval,
|
|
86
|
+
capture_lines,
|
|
87
|
+
"enabled" if block_manager else "disabled",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def start_monitoring(self, pane_target: str, project_id: str) -> None:
|
|
91
|
+
"""Start monitoring a pane for events.
|
|
92
|
+
|
|
93
|
+
Creates a background task that continuously polls the pane output
|
|
94
|
+
and detects events. Only one monitor per pane is allowed.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
pane_target: Tmux pane target to monitor (e.g., '%5')
|
|
98
|
+
project_id: Project ID for event attribution
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If pane_target or project_id is None/empty
|
|
102
|
+
RuntimeError: If monitoring already active for this pane
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> await monitor.start_monitoring("%5", "proj_123")
|
|
106
|
+
"""
|
|
107
|
+
if not pane_target:
|
|
108
|
+
raise ValueError("Pane target cannot be None or empty")
|
|
109
|
+
if not project_id:
|
|
110
|
+
raise ValueError("Project ID cannot be None or empty")
|
|
111
|
+
|
|
112
|
+
if pane_target in self._monitors:
|
|
113
|
+
raise RuntimeError(f"Monitoring already active for pane {pane_target}")
|
|
114
|
+
|
|
115
|
+
logger.info(
|
|
116
|
+
"Starting monitoring for pane %s (project: %s)", pane_target, project_id
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Create background polling task
|
|
120
|
+
task = asyncio.create_task(
|
|
121
|
+
self._monitor_loop(pane_target, project_id), name=f"monitor-{pane_target}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Track monitor with initial output hash of 0
|
|
125
|
+
self._monitors[pane_target] = (project_id, task, 0)
|
|
126
|
+
|
|
127
|
+
async def stop_monitoring(self, pane_target: str) -> None:
|
|
128
|
+
"""Stop monitoring a pane.
|
|
129
|
+
|
|
130
|
+
Cancels the background polling task and removes the monitor.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
pane_target: Tmux pane target to stop monitoring
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If pane_target is None/empty
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> await monitor.stop_monitoring("%5")
|
|
140
|
+
"""
|
|
141
|
+
if not pane_target:
|
|
142
|
+
raise ValueError("Pane target cannot be None or empty")
|
|
143
|
+
|
|
144
|
+
if pane_target not in self._monitors:
|
|
145
|
+
logger.debug("No monitoring active for pane %s", pane_target)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
logger.info("Stopping monitoring for pane %s", pane_target)
|
|
149
|
+
|
|
150
|
+
_project_id, task, _ = self._monitors[pane_target]
|
|
151
|
+
|
|
152
|
+
# Cancel the monitoring task
|
|
153
|
+
if task and not task.done():
|
|
154
|
+
task.cancel()
|
|
155
|
+
try:
|
|
156
|
+
await task
|
|
157
|
+
except asyncio.CancelledError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Remove from monitors
|
|
161
|
+
del self._monitors[pane_target]
|
|
162
|
+
logger.debug("Stopped monitoring pane %s", pane_target)
|
|
163
|
+
|
|
164
|
+
async def poll_once(self, pane_target: str) -> List[Event]:
|
|
165
|
+
"""Poll pane output once and return any detected events.
|
|
166
|
+
|
|
167
|
+
This is a one-time poll that doesn't require starting a monitor.
|
|
168
|
+
Useful for on-demand event checking.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
pane_target: Tmux pane target to poll
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of Event objects detected in the output
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValueError: If pane_target is None/empty
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> events = await monitor.poll_once("%5")
|
|
181
|
+
>>> for event in events:
|
|
182
|
+
... print(f"Event: {event.title}")
|
|
183
|
+
"""
|
|
184
|
+
if not pane_target:
|
|
185
|
+
raise ValueError("Pane target cannot be None or empty")
|
|
186
|
+
|
|
187
|
+
# Determine project_id if this pane is being monitored
|
|
188
|
+
project_id = "unknown"
|
|
189
|
+
session_id = None
|
|
190
|
+
if pane_target in self._monitors:
|
|
191
|
+
project_id, _, _ = self._monitors[pane_target]
|
|
192
|
+
|
|
193
|
+
logger.debug("Polling pane %s once", pane_target)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Capture output from tmux
|
|
197
|
+
output = self.orchestrator.capture_output(
|
|
198
|
+
pane_target, lines=self.capture_lines
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Parse for events - don't create events automatically
|
|
202
|
+
parse_results = self.parser.parse(
|
|
203
|
+
content=output,
|
|
204
|
+
project_id=project_id,
|
|
205
|
+
session_id=session_id,
|
|
206
|
+
create_events=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Create events manually so we can return them
|
|
210
|
+
events = []
|
|
211
|
+
for result in parse_results:
|
|
212
|
+
event = self.event_manager.create(
|
|
213
|
+
project_id=project_id,
|
|
214
|
+
session_id=session_id,
|
|
215
|
+
event_type=result.event_type,
|
|
216
|
+
title=result.title,
|
|
217
|
+
content=result.content,
|
|
218
|
+
options=result.options,
|
|
219
|
+
context=result.context,
|
|
220
|
+
)
|
|
221
|
+
events.append(event)
|
|
222
|
+
|
|
223
|
+
logger.debug(
|
|
224
|
+
"Poll detected %d events from pane %s", len(events), pane_target
|
|
225
|
+
)
|
|
226
|
+
return events
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.warning("Failed to poll pane %s: %s", pane_target, e)
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def active_monitors(self) -> Dict[str, str]:
|
|
234
|
+
"""Get map of pane_target -> project_id for active monitors.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dict mapping pane targets to their project IDs
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> monitors = monitor.active_monitors
|
|
241
|
+
>>> print(monitors)
|
|
242
|
+
{'%5': 'proj_123', '%6': 'proj_456'}
|
|
243
|
+
"""
|
|
244
|
+
return {pane: project_id for pane, (project_id, _, _) in self._monitors.items()}
|
|
245
|
+
|
|
246
|
+
async def _monitor_loop(self, pane_target: str, project_id: str) -> None:
|
|
247
|
+
"""Background monitoring loop for a single pane.
|
|
248
|
+
|
|
249
|
+
Continuously polls the pane output at the configured interval and
|
|
250
|
+
detects events. Uses output hashing to avoid reprocessing identical output.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
pane_target: Tmux pane target to monitor
|
|
254
|
+
project_id: Project ID for event attribution
|
|
255
|
+
"""
|
|
256
|
+
logger.debug("Monitor loop started for pane %s", pane_target)
|
|
257
|
+
|
|
258
|
+
while pane_target in self._monitors:
|
|
259
|
+
try:
|
|
260
|
+
# Capture output from tmux
|
|
261
|
+
output = self.orchestrator.capture_output(
|
|
262
|
+
pane_target, lines=self.capture_lines
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Check if output has changed since last poll
|
|
266
|
+
output_hash = hash(output)
|
|
267
|
+
_, task, last_hash = self._monitors.get(
|
|
268
|
+
pane_target, (project_id, None, 0)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if output_hash == last_hash:
|
|
272
|
+
# No change in output, skip parsing
|
|
273
|
+
await asyncio.sleep(self.poll_interval)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Update hash
|
|
277
|
+
self._monitors[pane_target] = (project_id, task, output_hash)
|
|
278
|
+
|
|
279
|
+
# Parse for events
|
|
280
|
+
parse_results = self.parser.parse(
|
|
281
|
+
content=output,
|
|
282
|
+
project_id=project_id,
|
|
283
|
+
session_id=None,
|
|
284
|
+
create_events=True, # Create events via EventManager
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if parse_results:
|
|
288
|
+
logger.info(
|
|
289
|
+
"Detected %d events from pane %s",
|
|
290
|
+
len(parse_results),
|
|
291
|
+
pane_target,
|
|
292
|
+
)
|
|
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
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(
|
|
319
|
+
"Error in monitor loop for pane %s: %s",
|
|
320
|
+
pane_target,
|
|
321
|
+
e,
|
|
322
|
+
exc_info=True,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Wait before next poll
|
|
326
|
+
await asyncio.sleep(self.poll_interval)
|
|
327
|
+
|
|
328
|
+
logger.debug("Monitor loop stopped for pane %s", pane_target)
|
|
329
|
+
|
|
330
|
+
async def stop_all(self) -> None:
|
|
331
|
+
"""Stop all active monitors.
|
|
332
|
+
|
|
333
|
+
Cancels all background polling tasks and clears the monitor registry.
|
|
334
|
+
|
|
335
|
+
Example:
|
|
336
|
+
>>> await monitor.stop_all()
|
|
337
|
+
"""
|
|
338
|
+
logger.info("Stopping all %d active monitors", len(self._monitors))
|
|
339
|
+
|
|
340
|
+
# Get all pane targets before iteration (avoid dict size change during iteration)
|
|
341
|
+
pane_targets = list(self._monitors.keys())
|
|
342
|
+
|
|
343
|
+
for pane_target in pane_targets:
|
|
344
|
+
await self.stop_monitoring(pane_target)
|
|
345
|
+
|
|
346
|
+
logger.info("All monitors stopped")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Session context for Commander chat interface."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Message:
|
|
10
|
+
"""A single message in the conversation."""
|
|
11
|
+
|
|
12
|
+
role: str # "user" or "assistant"
|
|
13
|
+
content: str
|
|
14
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SessionContext:
|
|
19
|
+
"""Tracks the state of a Commander chat session.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
connected_instance: Name of currently connected instance, or None.
|
|
23
|
+
messages: Conversation history.
|
|
24
|
+
session_id: Unique session identifier.
|
|
25
|
+
created_at: When session was created.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> context = SessionContext()
|
|
29
|
+
>>> context.is_connected
|
|
30
|
+
False
|
|
31
|
+
>>> context.connect_to("myapp")
|
|
32
|
+
>>> context.is_connected
|
|
33
|
+
True
|
|
34
|
+
>>> context.connected_instance
|
|
35
|
+
'myapp'
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
connected_instance: Optional[str] = None
|
|
39
|
+
messages: list[Message] = field(default_factory=list)
|
|
40
|
+
session_id: str = field(
|
|
41
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
42
|
+
)
|
|
43
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_connected(self) -> bool:
|
|
47
|
+
"""Check if connected to an instance."""
|
|
48
|
+
return self.connected_instance is not None
|
|
49
|
+
|
|
50
|
+
def connect_to(self, instance_name: str) -> None:
|
|
51
|
+
"""Connect to an instance.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
instance_name: Name of instance to connect to.
|
|
55
|
+
"""
|
|
56
|
+
self.connected_instance = instance_name
|
|
57
|
+
|
|
58
|
+
def disconnect(self) -> None:
|
|
59
|
+
"""Disconnect from current instance."""
|
|
60
|
+
self.connected_instance = None
|
|
61
|
+
|
|
62
|
+
def add_message(self, role: str, content: str) -> None:
|
|
63
|
+
"""Add a message to conversation history.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
role: "user" or "assistant".
|
|
67
|
+
content: Message content.
|
|
68
|
+
"""
|
|
69
|
+
self.messages.append(Message(role=role, content=content))
|
|
70
|
+
|
|
71
|
+
def get_messages_for_llm(self) -> list[dict]:
|
|
72
|
+
"""Get messages formatted for LLM API.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of message dicts with 'role' and 'content' keys.
|
|
76
|
+
"""
|
|
77
|
+
return [{"role": msg.role, "content": msg.content} for msg in self.messages]
|
|
78
|
+
|
|
79
|
+
def clear_history(self) -> None:
|
|
80
|
+
"""Clear conversation history."""
|
|
81
|
+
self.messages.clear()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Session manager for Commander chat interface."""
|
|
2
|
+
|
|
3
|
+
from .context import SessionContext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SessionManager:
|
|
7
|
+
"""Manages Commander chat session state.
|
|
8
|
+
|
|
9
|
+
Coordinates session context, connection state, and conversation history.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> manager = SessionManager()
|
|
13
|
+
>>> manager.connect_to("myapp")
|
|
14
|
+
>>> manager.context.is_connected
|
|
15
|
+
True
|
|
16
|
+
>>> manager.add_user_message("Fix the bug")
|
|
17
|
+
>>> len(manager.context.messages)
|
|
18
|
+
1
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize session manager with fresh context."""
|
|
23
|
+
self.context = SessionContext()
|
|
24
|
+
|
|
25
|
+
def connect_to(self, instance_name: str) -> None:
|
|
26
|
+
"""Connect to an instance.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
instance_name: Name of instance to connect to.
|
|
30
|
+
"""
|
|
31
|
+
self.context.connect_to(instance_name)
|
|
32
|
+
|
|
33
|
+
def disconnect(self) -> None:
|
|
34
|
+
"""Disconnect from current instance."""
|
|
35
|
+
self.context.disconnect()
|
|
36
|
+
|
|
37
|
+
def add_user_message(self, content: str) -> None:
|
|
38
|
+
"""Add a user message to conversation history.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
content: User message content.
|
|
42
|
+
"""
|
|
43
|
+
self.context.add_message("user", content)
|
|
44
|
+
|
|
45
|
+
def add_assistant_message(self, content: str) -> None:
|
|
46
|
+
"""Add an assistant message to conversation history.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
content: Assistant message content.
|
|
50
|
+
"""
|
|
51
|
+
self.context.add_message("assistant", content)
|
|
52
|
+
|
|
53
|
+
def clear_history(self) -> None:
|
|
54
|
+
"""Clear conversation history."""
|
|
55
|
+
self.context.clear_history()
|
|
56
|
+
|
|
57
|
+
def reset(self) -> None:
|
|
58
|
+
"""Reset session to initial state."""
|
|
59
|
+
self.context = SessionContext()
|