claude-mpm 5.4.85__py3-none-any.whl → 5.6.1__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.
Files changed (254) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +8 -5
  3. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +101 -703
  5. claude_mpm/agents/WORKFLOW.md +2 -0
  6. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  7. claude_mpm/cli/commands/autotodos.py +566 -0
  8. claude_mpm/cli/commands/commander.py +46 -0
  9. claude_mpm/cli/commands/hook_errors.py +60 -60
  10. claude_mpm/cli/commands/monitor.py +2 -2
  11. claude_mpm/cli/commands/mpm_init/core.py +2 -2
  12. claude_mpm/cli/commands/run.py +35 -3
  13. claude_mpm/cli/executor.py +119 -16
  14. claude_mpm/cli/parsers/base_parser.py +71 -1
  15. claude_mpm/cli/parsers/commander_parser.py +83 -0
  16. claude_mpm/cli/parsers/run_parser.py +10 -0
  17. claude_mpm/cli/startup.py +54 -16
  18. claude_mpm/cli/startup_display.py +72 -5
  19. claude_mpm/cli/startup_logging.py +2 -2
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +72 -0
  22. claude_mpm/commander/adapters/__init__.py +31 -0
  23. claude_mpm/commander/adapters/base.py +191 -0
  24. claude_mpm/commander/adapters/claude_code.py +361 -0
  25. claude_mpm/commander/adapters/communication.py +366 -0
  26. claude_mpm/commander/api/__init__.py +16 -0
  27. claude_mpm/commander/api/app.py +105 -0
  28. claude_mpm/commander/api/errors.py +112 -0
  29. claude_mpm/commander/api/routes/__init__.py +8 -0
  30. claude_mpm/commander/api/routes/events.py +184 -0
  31. claude_mpm/commander/api/routes/inbox.py +171 -0
  32. claude_mpm/commander/api/routes/messages.py +148 -0
  33. claude_mpm/commander/api/routes/projects.py +271 -0
  34. claude_mpm/commander/api/routes/sessions.py +215 -0
  35. claude_mpm/commander/api/routes/work.py +260 -0
  36. claude_mpm/commander/api/schemas.py +182 -0
  37. claude_mpm/commander/chat/__init__.py +7 -0
  38. claude_mpm/commander/chat/cli.py +107 -0
  39. claude_mpm/commander/chat/commands.py +96 -0
  40. claude_mpm/commander/chat/repl.py +310 -0
  41. claude_mpm/commander/config.py +49 -0
  42. claude_mpm/commander/config_loader.py +115 -0
  43. claude_mpm/commander/daemon.py +398 -0
  44. claude_mpm/commander/events/__init__.py +26 -0
  45. claude_mpm/commander/events/manager.py +332 -0
  46. claude_mpm/commander/frameworks/__init__.py +12 -0
  47. claude_mpm/commander/frameworks/base.py +143 -0
  48. claude_mpm/commander/frameworks/claude_code.py +58 -0
  49. claude_mpm/commander/frameworks/mpm.py +62 -0
  50. claude_mpm/commander/inbox/__init__.py +16 -0
  51. claude_mpm/commander/inbox/dedup.py +128 -0
  52. claude_mpm/commander/inbox/inbox.py +224 -0
  53. claude_mpm/commander/inbox/models.py +70 -0
  54. claude_mpm/commander/instance_manager.py +337 -0
  55. claude_mpm/commander/llm/__init__.py +6 -0
  56. claude_mpm/commander/llm/openrouter_client.py +167 -0
  57. claude_mpm/commander/llm/summarizer.py +70 -0
  58. claude_mpm/commander/models/__init__.py +18 -0
  59. claude_mpm/commander/models/events.py +121 -0
  60. claude_mpm/commander/models/project.py +162 -0
  61. claude_mpm/commander/models/work.py +214 -0
  62. claude_mpm/commander/parsing/__init__.py +20 -0
  63. claude_mpm/commander/parsing/extractor.py +132 -0
  64. claude_mpm/commander/parsing/output_parser.py +270 -0
  65. claude_mpm/commander/parsing/patterns.py +100 -0
  66. claude_mpm/commander/persistence/__init__.py +11 -0
  67. claude_mpm/commander/persistence/event_store.py +274 -0
  68. claude_mpm/commander/persistence/state_store.py +309 -0
  69. claude_mpm/commander/persistence/work_store.py +164 -0
  70. claude_mpm/commander/polling/__init__.py +13 -0
  71. claude_mpm/commander/polling/event_detector.py +104 -0
  72. claude_mpm/commander/polling/output_buffer.py +49 -0
  73. claude_mpm/commander/polling/output_poller.py +153 -0
  74. claude_mpm/commander/project_session.py +268 -0
  75. claude_mpm/commander/proxy/__init__.py +12 -0
  76. claude_mpm/commander/proxy/formatter.py +89 -0
  77. claude_mpm/commander/proxy/output_handler.py +191 -0
  78. claude_mpm/commander/proxy/relay.py +155 -0
  79. claude_mpm/commander/registry.py +404 -0
  80. claude_mpm/commander/runtime/__init__.py +10 -0
  81. claude_mpm/commander/runtime/executor.py +191 -0
  82. claude_mpm/commander/runtime/monitor.py +316 -0
  83. claude_mpm/commander/session/__init__.py +6 -0
  84. claude_mpm/commander/session/context.py +81 -0
  85. claude_mpm/commander/session/manager.py +59 -0
  86. claude_mpm/commander/tmux_orchestrator.py +361 -0
  87. claude_mpm/commander/web/__init__.py +1 -0
  88. claude_mpm/commander/work/__init__.py +30 -0
  89. claude_mpm/commander/work/executor.py +189 -0
  90. claude_mpm/commander/work/queue.py +405 -0
  91. claude_mpm/commander/workflow/__init__.py +27 -0
  92. claude_mpm/commander/workflow/event_handler.py +219 -0
  93. claude_mpm/commander/workflow/notifier.py +146 -0
  94. claude_mpm/commands/mpm-config.md +8 -0
  95. claude_mpm/commands/mpm-doctor.md +8 -0
  96. claude_mpm/commands/mpm-help.md +8 -0
  97. claude_mpm/commands/mpm-init.md +8 -0
  98. claude_mpm/commands/mpm-monitor.md +8 -0
  99. claude_mpm/commands/mpm-organize.md +8 -0
  100. claude_mpm/commands/mpm-postmortem.md +8 -0
  101. claude_mpm/commands/mpm-session-resume.md +9 -1
  102. claude_mpm/commands/mpm-status.md +8 -0
  103. claude_mpm/commands/mpm-ticket-view.md +8 -0
  104. claude_mpm/commands/mpm-version.md +8 -0
  105. claude_mpm/commands/mpm.md +8 -0
  106. claude_mpm/config/agent_presets.py +8 -7
  107. claude_mpm/core/config.py +5 -0
  108. claude_mpm/core/hook_manager.py +51 -3
  109. claude_mpm/core/logger.py +10 -7
  110. claude_mpm/core/logging_utils.py +4 -2
  111. claude_mpm/core/output_style_manager.py +15 -5
  112. claude_mpm/core/unified_config.py +10 -6
  113. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  114. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  115. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
  116. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
  117. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
  118. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
  119. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
  120. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
  121. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
  122. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
  123. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
  124. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
  125. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
  126. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
  127. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
  128. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  129. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  130. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
  131. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
  132. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
  133. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
  134. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
  135. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
  136. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
  137. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
  138. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  139. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
  140. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
  141. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
  142. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
  143. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
  144. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
  145. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
  146. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
  147. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
  148. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
  149. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
  150. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
  151. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
  152. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
  153. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
  154. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
  155. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
  156. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
  157. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  158. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
  159. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
  160. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
  161. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  162. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
  163. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
  164. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
  165. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
  166. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  167. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
  168. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
  169. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  170. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  171. claude_mpm/dashboard/static/svelte-build/index.html +9 -9
  172. claude_mpm/experimental/cli_enhancements.py +2 -1
  173. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  174. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  175. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
  176. claude_mpm/hooks/claude_hooks/event_handlers.py +250 -11
  177. claude_mpm/hooks/claude_hooks/hook_handler.py +106 -89
  178. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  179. claude_mpm/hooks/claude_hooks/installer.py +69 -5
  180. claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
  181. claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
  182. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
  183. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
  184. claude_mpm/hooks/session_resume_hook.py +85 -1
  185. claude_mpm/init.py +1 -1
  186. claude_mpm/scripts/claude-hook-handler.sh +36 -10
  187. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  188. claude_mpm/services/agents/cache_git_manager.py +1 -1
  189. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
  190. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  191. claude_mpm/services/cli/__init__.py +3 -0
  192. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  193. claude_mpm/services/cli/session_resume_helper.py +10 -2
  194. claude_mpm/services/delegation_detector.py +175 -0
  195. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  196. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  197. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  198. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  199. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  200. claude_mpm/services/diagnostics/models.py +14 -1
  201. claude_mpm/services/event_log.py +325 -0
  202. claude_mpm/services/infrastructure/__init__.py +4 -0
  203. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  204. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  205. claude_mpm/services/monitor/daemon_manager.py +15 -4
  206. claude_mpm/services/monitor/management/lifecycle.py +8 -2
  207. claude_mpm/services/monitor/server.py +106 -16
  208. claude_mpm/services/pm_skills_deployer.py +259 -87
  209. claude_mpm/services/skills/git_skill_source_manager.py +51 -2
  210. claude_mpm/services/skills/selective_skill_deployer.py +114 -16
  211. claude_mpm/services/skills/skill_discovery_service.py +57 -3
  212. claude_mpm/services/socketio/handlers/hook.py +14 -7
  213. claude_mpm/services/socketio/server/main.py +12 -4
  214. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  215. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  216. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  217. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  218. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  219. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  220. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  221. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  222. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  223. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  224. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  225. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  226. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  227. claude_mpm/skills/bundled/pm/{pm-teaching-mode → mpm-teaching-mode}/SKILL.md +2 -2
  228. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  229. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  230. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  231. claude_mpm/skills/skill_manager.py +4 -4
  232. claude_mpm-5.6.1.dist-info/METADATA +391 -0
  233. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/RECORD +244 -145
  234. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
  235. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
  236. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
  237. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
  238. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
  239. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
  240. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
  241. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
  242. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
  243. claude_mpm-5.4.85.dist-info/METADATA +0 -1023
  244. /claude_mpm/skills/bundled/pm/{pm-bug-reporting/pm-bug-reporting.md → mpm-bug-reporting/SKILL.md} +0 -0
  245. /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
  246. /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
  247. /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
  248. /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
  249. /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
  250. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/WHEEL +0 -0
  251. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/entry_points.txt +0 -0
  252. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE +0 -0
  253. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  254. {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.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
+ ]