claude-mpm 5.4.96__py3-none-any.whl → 5.6.10__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 +46 -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 +83 -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 +20 -2
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +72 -0
- claude_mpm/commander/adapters/__init__.py +31 -0
- claude_mpm/commander/adapters/base.py +191 -0
- claude_mpm/commander/adapters/claude_code.py +361 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +105 -0
- claude_mpm/commander/api/errors.py +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 +228 -0
- claude_mpm/commander/api/routes/work.py +260 -0
- claude_mpm/commander/api/schemas.py +182 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +107 -0
- claude_mpm/commander/chat/commands.py +96 -0
- claude_mpm/commander/chat/repl.py +310 -0
- claude_mpm/commander/config.py +49 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/daemon.py +398 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +332 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +143 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +62 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +337 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +121 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +309 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +404 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +316 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +361 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +189 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +219 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +8 -0
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/config/skill_sources.py +16 -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 +15 -5
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/__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 +90 -99
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +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_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/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/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/METADATA +18 -4
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/RECORD +190 -79
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Commander daemon for autonomous multi-project orchestration.
|
|
2
|
+
|
|
3
|
+
This module implements the main daemon process that coordinates multiple
|
|
4
|
+
projects, manages their lifecycles, and handles graceful shutdown.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import signal
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from .api.app import (
|
|
15
|
+
app,
|
|
16
|
+
)
|
|
17
|
+
from .config import DaemonConfig
|
|
18
|
+
from .events.manager import EventManager
|
|
19
|
+
from .inbox import Inbox
|
|
20
|
+
from .persistence import EventStore, StateStore
|
|
21
|
+
from .project_session import ProjectSession, SessionState
|
|
22
|
+
from .registry import ProjectRegistry
|
|
23
|
+
from .tmux_orchestrator import TmuxOrchestrator
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CommanderDaemon:
|
|
29
|
+
"""Main daemon process for MPM Commander.
|
|
30
|
+
|
|
31
|
+
Orchestrates multiple projects, manages their sessions, handles events,
|
|
32
|
+
and provides REST API for external control.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
config: Daemon configuration
|
|
36
|
+
registry: Project registry
|
|
37
|
+
orchestrator: Tmux orchestrator
|
|
38
|
+
event_manager: Event manager
|
|
39
|
+
inbox: Event inbox
|
|
40
|
+
sessions: Active project sessions by project_id
|
|
41
|
+
state_store: StateStore for project/session persistence
|
|
42
|
+
event_store: EventStore for event queue persistence
|
|
43
|
+
running: Whether daemon is currently running
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> config = DaemonConfig(port=8765)
|
|
47
|
+
>>> daemon = CommanderDaemon(config)
|
|
48
|
+
>>> await daemon.start()
|
|
49
|
+
>>> # Daemon runs until stopped
|
|
50
|
+
>>> await daemon.stop()
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, config: DaemonConfig):
|
|
54
|
+
"""Initialize Commander daemon.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config: Daemon configuration
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If config is invalid
|
|
61
|
+
"""
|
|
62
|
+
if config is None:
|
|
63
|
+
raise ValueError("Config cannot be None")
|
|
64
|
+
|
|
65
|
+
self.config = config
|
|
66
|
+
self.registry = ProjectRegistry()
|
|
67
|
+
self.orchestrator = TmuxOrchestrator()
|
|
68
|
+
self.event_manager = EventManager()
|
|
69
|
+
self.inbox = Inbox(self.event_manager, self.registry)
|
|
70
|
+
self.sessions: Dict[str, ProjectSession] = {}
|
|
71
|
+
self._running = False
|
|
72
|
+
self._server_task: Optional[asyncio.Task] = None
|
|
73
|
+
self._main_loop_task: Optional[asyncio.Task] = None
|
|
74
|
+
|
|
75
|
+
# Initialize persistence stores
|
|
76
|
+
self.state_store = StateStore(config.state_dir)
|
|
77
|
+
self.event_store = EventStore(config.state_dir)
|
|
78
|
+
|
|
79
|
+
# Configure logging
|
|
80
|
+
logging.basicConfig(
|
|
81
|
+
level=getattr(logging, config.log_level.upper()),
|
|
82
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
83
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
f"Initialized CommanderDaemon (host={config.host}, "
|
|
88
|
+
f"port={config.port}, state_dir={config.state_dir})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_running(self) -> bool:
|
|
93
|
+
"""Check if daemon is running.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if daemon main loop is active
|
|
97
|
+
"""
|
|
98
|
+
return self._running
|
|
99
|
+
|
|
100
|
+
async def start(self) -> None:
|
|
101
|
+
"""Start daemon and all subsystems.
|
|
102
|
+
|
|
103
|
+
Initializes:
|
|
104
|
+
- Load state from disk (projects, sessions, events)
|
|
105
|
+
- Signal handlers for graceful shutdown
|
|
106
|
+
- REST API server
|
|
107
|
+
- Main daemon loop
|
|
108
|
+
- Tmux session for project management
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
RuntimeError: If daemon already running
|
|
112
|
+
"""
|
|
113
|
+
if self._running:
|
|
114
|
+
raise RuntimeError("Daemon already running")
|
|
115
|
+
|
|
116
|
+
logger.info("Starting Commander daemon...")
|
|
117
|
+
self._running = True
|
|
118
|
+
|
|
119
|
+
# Load state from disk
|
|
120
|
+
await self._load_state()
|
|
121
|
+
|
|
122
|
+
# Set up signal handlers
|
|
123
|
+
self._setup_signal_handlers()
|
|
124
|
+
|
|
125
|
+
# Inject global instances into API app
|
|
126
|
+
global api_registry, api_tmux, api_event_manager, api_inbox
|
|
127
|
+
api_registry = self.registry
|
|
128
|
+
api_tmux = self.orchestrator
|
|
129
|
+
api_event_manager = self.event_manager
|
|
130
|
+
api_inbox = self.inbox
|
|
131
|
+
|
|
132
|
+
# Start API server in background
|
|
133
|
+
logger.info(f"Starting API server on {self.config.host}:{self.config.port}")
|
|
134
|
+
config_uvicorn = uvicorn.Config(
|
|
135
|
+
app,
|
|
136
|
+
host=self.config.host,
|
|
137
|
+
port=self.config.port,
|
|
138
|
+
log_level=self.config.log_level.lower(),
|
|
139
|
+
)
|
|
140
|
+
server = uvicorn.Server(config_uvicorn)
|
|
141
|
+
self._server_task = asyncio.create_task(server.serve())
|
|
142
|
+
|
|
143
|
+
# Create tmux session for projects
|
|
144
|
+
if not self.orchestrator.session_exists():
|
|
145
|
+
self.orchestrator.create_session()
|
|
146
|
+
logger.info("Created tmux session for project management")
|
|
147
|
+
|
|
148
|
+
# Start main daemon loop
|
|
149
|
+
logger.info("Starting main daemon loop")
|
|
150
|
+
self._main_loop_task = asyncio.create_task(self.run())
|
|
151
|
+
|
|
152
|
+
logger.info("Commander daemon started successfully")
|
|
153
|
+
|
|
154
|
+
async def stop(self) -> None:
|
|
155
|
+
"""Graceful shutdown with cleanup.
|
|
156
|
+
|
|
157
|
+
Stops all active sessions, persists state, and shuts down API server.
|
|
158
|
+
"""
|
|
159
|
+
if not self._running:
|
|
160
|
+
logger.warning("Daemon not running, nothing to stop")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
logger.info("Stopping Commander daemon...")
|
|
164
|
+
self._running = False
|
|
165
|
+
|
|
166
|
+
# Stop all project sessions
|
|
167
|
+
for project_id, session in list(self.sessions.items()):
|
|
168
|
+
try:
|
|
169
|
+
logger.info(f"Stopping session for project {project_id}")
|
|
170
|
+
await session.stop()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Error stopping session {project_id}: {e}")
|
|
173
|
+
|
|
174
|
+
# Cancel main loop task
|
|
175
|
+
if self._main_loop_task and not self._main_loop_task.done():
|
|
176
|
+
self._main_loop_task.cancel()
|
|
177
|
+
try:
|
|
178
|
+
await self._main_loop_task
|
|
179
|
+
except asyncio.CancelledError:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# Persist state to disk
|
|
183
|
+
await self._save_state()
|
|
184
|
+
|
|
185
|
+
# Stop API server
|
|
186
|
+
if self._server_task and not self._server_task.done():
|
|
187
|
+
self._server_task.cancel()
|
|
188
|
+
try:
|
|
189
|
+
await self._server_task
|
|
190
|
+
except asyncio.CancelledError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
logger.info("Commander daemon stopped")
|
|
194
|
+
|
|
195
|
+
async def run(self) -> None:
|
|
196
|
+
"""Main daemon loop.
|
|
197
|
+
|
|
198
|
+
Continuously polls for:
|
|
199
|
+
- Resolved events to resume paused sessions
|
|
200
|
+
- New work items to execute
|
|
201
|
+
- Project state changes
|
|
202
|
+
- Periodic state persistence
|
|
203
|
+
|
|
204
|
+
Runs until _running flag is set to False.
|
|
205
|
+
"""
|
|
206
|
+
logger.info("Main daemon loop starting")
|
|
207
|
+
|
|
208
|
+
# Track last save time for periodic persistence
|
|
209
|
+
last_save_time = asyncio.get_event_loop().time()
|
|
210
|
+
|
|
211
|
+
while self._running:
|
|
212
|
+
try:
|
|
213
|
+
# TODO: Check for resolved events and resume sessions (Phase 2 Sprint 3)
|
|
214
|
+
# TODO: Check each ProjectSession for runnable work (Phase 2 Sprint 2)
|
|
215
|
+
# TODO: Spawn RuntimeExecutors for new work items (Phase 2 Sprint 1)
|
|
216
|
+
|
|
217
|
+
# Periodic state persistence
|
|
218
|
+
current_time = asyncio.get_event_loop().time()
|
|
219
|
+
if current_time - last_save_time >= self.config.save_interval:
|
|
220
|
+
try:
|
|
221
|
+
await self._save_state()
|
|
222
|
+
last_save_time = current_time
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Error during periodic save: {e}", exc_info=True)
|
|
225
|
+
|
|
226
|
+
# Sleep to prevent tight loop
|
|
227
|
+
await asyncio.sleep(self.config.poll_interval)
|
|
228
|
+
|
|
229
|
+
except asyncio.CancelledError:
|
|
230
|
+
logger.info("Main loop cancelled")
|
|
231
|
+
break
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Error in main loop: {e}", exc_info=True)
|
|
234
|
+
# Continue running despite errors
|
|
235
|
+
await asyncio.sleep(self.config.poll_interval)
|
|
236
|
+
|
|
237
|
+
logger.info("Main daemon loop stopped")
|
|
238
|
+
|
|
239
|
+
def _setup_signal_handlers(self) -> None:
|
|
240
|
+
"""Set up signal handlers for graceful shutdown.
|
|
241
|
+
|
|
242
|
+
Registers handlers for SIGINT and SIGTERM that trigger
|
|
243
|
+
daemon shutdown via asyncio event loop.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def handle_signal(signum: int, frame) -> None:
|
|
247
|
+
"""Handle shutdown signal.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
signum: Signal number
|
|
251
|
+
frame: Current stack frame
|
|
252
|
+
"""
|
|
253
|
+
sig_name = signal.Signals(signum).name
|
|
254
|
+
logger.info(f"Received {sig_name}, initiating graceful shutdown...")
|
|
255
|
+
|
|
256
|
+
# Schedule shutdown in event loop
|
|
257
|
+
if self._running:
|
|
258
|
+
asyncio.create_task(self.stop())
|
|
259
|
+
|
|
260
|
+
# Register signal handlers
|
|
261
|
+
signal.signal(signal.SIGINT, handle_signal)
|
|
262
|
+
signal.signal(signal.SIGTERM, handle_signal)
|
|
263
|
+
|
|
264
|
+
logger.debug("Signal handlers configured (SIGINT, SIGTERM)")
|
|
265
|
+
|
|
266
|
+
def get_or_create_session(self, project_id: str) -> ProjectSession:
|
|
267
|
+
"""Get existing session or create new one for project.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
project_id: Project identifier
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
ProjectSession for the project
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If project not found in registry
|
|
277
|
+
"""
|
|
278
|
+
if project_id in self.sessions:
|
|
279
|
+
return self.sessions[project_id]
|
|
280
|
+
|
|
281
|
+
project = self.registry.get(project_id)
|
|
282
|
+
if project is None:
|
|
283
|
+
raise ValueError(f"Project not found: {project_id}")
|
|
284
|
+
|
|
285
|
+
session = ProjectSession(project, self.orchestrator)
|
|
286
|
+
self.sessions[project_id] = session
|
|
287
|
+
|
|
288
|
+
logger.info(f"Created new session for project {project_id}")
|
|
289
|
+
return session
|
|
290
|
+
|
|
291
|
+
async def _load_state(self) -> None:
|
|
292
|
+
"""Load state from disk (projects, sessions, events).
|
|
293
|
+
|
|
294
|
+
Called on daemon startup to restore previous state.
|
|
295
|
+
Handles missing or corrupt files gracefully.
|
|
296
|
+
"""
|
|
297
|
+
logger.info("Loading state from disk...")
|
|
298
|
+
|
|
299
|
+
# Load projects
|
|
300
|
+
try:
|
|
301
|
+
projects = await self.state_store.load_projects()
|
|
302
|
+
for project in projects:
|
|
303
|
+
# Re-register projects (bypassing validation for already-registered paths)
|
|
304
|
+
self.registry._projects[project.id] = project
|
|
305
|
+
self.registry._path_index[project.path] = project.id
|
|
306
|
+
logger.info(f"Restored {len(projects)} projects")
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.error(f"Failed to load projects: {e}", exc_info=True)
|
|
309
|
+
|
|
310
|
+
# Load sessions
|
|
311
|
+
try:
|
|
312
|
+
session_states = await self.state_store.load_sessions()
|
|
313
|
+
for project_id, state_dict in session_states.items():
|
|
314
|
+
# Only restore sessions for projects we have
|
|
315
|
+
if project_id in self.registry._projects:
|
|
316
|
+
project = self.registry.get(project_id)
|
|
317
|
+
session = ProjectSession(project, self.orchestrator)
|
|
318
|
+
|
|
319
|
+
# Restore session state (but don't restart runtime - manual resume)
|
|
320
|
+
try:
|
|
321
|
+
session._state = SessionState(state_dict.get("state", "idle"))
|
|
322
|
+
session.active_pane = state_dict.get("pane_target")
|
|
323
|
+
session.pause_reason = state_dict.get("paused_event_id")
|
|
324
|
+
self.sessions[project_id] = session
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.warning(
|
|
327
|
+
f"Failed to restore session for {project_id}: {e}"
|
|
328
|
+
)
|
|
329
|
+
logger.info(f"Restored {len(self.sessions)} sessions")
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Failed to load sessions: {e}", exc_info=True)
|
|
332
|
+
|
|
333
|
+
# Load events
|
|
334
|
+
try:
|
|
335
|
+
events = await self.event_store.load_events()
|
|
336
|
+
for event in events:
|
|
337
|
+
self.event_manager.add_event(event)
|
|
338
|
+
logger.info(f"Restored {len(events)} events")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Failed to load events: {e}", exc_info=True)
|
|
341
|
+
|
|
342
|
+
logger.info("State loading complete")
|
|
343
|
+
|
|
344
|
+
async def _save_state(self) -> None:
|
|
345
|
+
"""Save state to disk (projects, sessions, events).
|
|
346
|
+
|
|
347
|
+
Called on daemon shutdown and periodically during runtime.
|
|
348
|
+
Uses atomic writes to prevent corruption.
|
|
349
|
+
"""
|
|
350
|
+
logger.debug("Saving state to disk...")
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Save projects
|
|
354
|
+
await self.state_store.save_projects(self.registry)
|
|
355
|
+
|
|
356
|
+
# Save sessions
|
|
357
|
+
await self.state_store.save_sessions(self.sessions)
|
|
358
|
+
|
|
359
|
+
# Save events
|
|
360
|
+
await self.event_store.save_events(self.inbox)
|
|
361
|
+
|
|
362
|
+
logger.debug("State saved successfully")
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Failed to save state: {e}", exc_info=True)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
async def main(config: Optional[DaemonConfig] = None) -> None:
|
|
368
|
+
"""Main entry point for running the daemon.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
config: Optional daemon configuration (uses defaults if None)
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
>>> import asyncio
|
|
375
|
+
>>> asyncio.run(main())
|
|
376
|
+
"""
|
|
377
|
+
if config is None:
|
|
378
|
+
config = DaemonConfig()
|
|
379
|
+
|
|
380
|
+
daemon = CommanderDaemon(config)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
await daemon.start()
|
|
384
|
+
|
|
385
|
+
# Keep daemon running until stopped
|
|
386
|
+
while daemon.is_running:
|
|
387
|
+
await asyncio.sleep(1)
|
|
388
|
+
|
|
389
|
+
except KeyboardInterrupt:
|
|
390
|
+
logger.info("Received KeyboardInterrupt")
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Daemon error: {e}", exc_info=True)
|
|
393
|
+
finally:
|
|
394
|
+
await daemon.stop()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if __name__ == "__main__":
|
|
398
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Event management for MPM Commander.
|
|
2
|
+
|
|
3
|
+
Exports:
|
|
4
|
+
- Event model and enums from models.events
|
|
5
|
+
- EventManager for event lifecycle management
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..models.events import (
|
|
9
|
+
BLOCKING_EVENTS,
|
|
10
|
+
DEFAULT_PRIORITIES,
|
|
11
|
+
Event,
|
|
12
|
+
EventPriority,
|
|
13
|
+
EventStatus,
|
|
14
|
+
EventType,
|
|
15
|
+
)
|
|
16
|
+
from .manager import EventManager
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BLOCKING_EVENTS",
|
|
20
|
+
"DEFAULT_PRIORITIES",
|
|
21
|
+
"Event",
|
|
22
|
+
"EventManager",
|
|
23
|
+
"EventPriority",
|
|
24
|
+
"EventStatus",
|
|
25
|
+
"EventType",
|
|
26
|
+
]
|