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,404 @@
1
+ """Project registry for MPM Commander.
2
+
3
+ This module provides thread-safe registration and management of projects,
4
+ including state tracking, session management, and path indexing.
5
+ """
6
+
7
+ import logging
8
+ import threading
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional
13
+
14
+ from .models import Project, ProjectState, ToolSession
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ProjectRegistry:
20
+ """Thread-safe registry for managing projects.
21
+
22
+ Maintains an in-memory registry of all active projects with:
23
+ - Unique project IDs (UUIDs)
24
+ - Path-based indexing for fast lookup
25
+ - Thread-safe access with RLock
26
+ - State management and session tracking
27
+
28
+ Example:
29
+ >>> registry = ProjectRegistry()
30
+ >>> project = registry.register("/Users/masa/Projects/my-app")
31
+ >>> project.id
32
+ 'a3f2c1d4-...'
33
+ >>> registry.update_state(project.id, ProjectState.WORKING)
34
+ >>> registry.get(project.id).state
35
+ <ProjectState.WORKING: 'working'>
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize empty registry with thread-safe lock."""
40
+ self._projects: Dict[str, Project] = {}
41
+ self._path_index: Dict[str, str] = {} # path -> project_id
42
+ self._lock = threading.RLock()
43
+ logger.info("Initialized ProjectRegistry")
44
+
45
+ def register(self, path: str, name: Optional[str] = None) -> Project:
46
+ """Register a new project.
47
+
48
+ Creates a new project with unique UUID and adds it to the registry.
49
+ Path must be a valid directory and cannot already be registered.
50
+
51
+ Args:
52
+ path: Absolute filesystem path to project directory
53
+ name: Optional human-readable name (defaults to directory name)
54
+
55
+ Returns:
56
+ Newly created Project instance
57
+
58
+ Raises:
59
+ ValueError: If path is invalid, not a directory, or already registered
60
+
61
+ Example:
62
+ >>> registry = ProjectRegistry()
63
+ >>> project = registry.register("/Users/masa/Projects/my-app")
64
+ >>> project.name
65
+ 'my-app'
66
+ >>> project.state
67
+ <ProjectState.IDLE: 'idle'>
68
+ """
69
+ with self._lock:
70
+ # Validate path exists and is directory
71
+ path_obj = Path(path)
72
+ try:
73
+ if not path_obj.exists():
74
+ raise ValueError(f"Path does not exist: {path}")
75
+ if not path_obj.is_dir():
76
+ raise ValueError(f"Path is not a directory: {path}")
77
+ except (OSError, PermissionError) as e:
78
+ raise ValueError(f"Cannot access path: {path}") from e
79
+
80
+ # Resolve to absolute path for consistency
81
+ abs_path = str(path_obj.resolve())
82
+
83
+ # Check for duplicate registration
84
+ if abs_path in self._path_index:
85
+ existing_id = self._path_index[abs_path]
86
+ raise ValueError(
87
+ f"Project already registered at path: {abs_path} "
88
+ f"(project_id: {existing_id})"
89
+ )
90
+
91
+ # Derive name from directory if not provided
92
+ if name is None:
93
+ name = path_obj.name
94
+
95
+ # Generate unique project ID
96
+ project_id = str(uuid.uuid4())
97
+
98
+ # Create project instance
99
+ project = Project(
100
+ id=project_id,
101
+ path=abs_path,
102
+ name=name,
103
+ state=ProjectState.IDLE,
104
+ )
105
+
106
+ # Register in both indexes
107
+ self._projects[project_id] = project
108
+ self._path_index[abs_path] = project_id
109
+
110
+ logger.info(
111
+ "Registered project: id=%s, path=%s, name=%s",
112
+ project_id,
113
+ abs_path,
114
+ name,
115
+ )
116
+
117
+ return project
118
+
119
+ def unregister(self, project_id: str) -> None:
120
+ """Remove project from registry.
121
+
122
+ Removes project from both ID and path indexes.
123
+
124
+ Args:
125
+ project_id: Unique project identifier
126
+
127
+ Raises:
128
+ KeyError: If project_id not found
129
+
130
+ Example:
131
+ >>> registry = ProjectRegistry()
132
+ >>> project = registry.register("/tmp/test-project")
133
+ >>> registry.unregister(project.id)
134
+ >>> registry.get(project.id) is None
135
+ True
136
+ """
137
+ with self._lock:
138
+ if project_id not in self._projects:
139
+ raise KeyError(f"Project not found: {project_id}")
140
+
141
+ project = self._projects[project_id]
142
+
143
+ # Remove from both indexes
144
+ del self._projects[project_id]
145
+ del self._path_index[project.path]
146
+
147
+ logger.info(
148
+ "Unregistered project: id=%s, path=%s",
149
+ project_id,
150
+ project.path,
151
+ )
152
+
153
+ def get(self, project_id: str) -> Optional[Project]:
154
+ """Get project by ID.
155
+
156
+ Args:
157
+ project_id: Unique project identifier
158
+
159
+ Returns:
160
+ Project instance or None if not found
161
+
162
+ Example:
163
+ >>> registry = ProjectRegistry()
164
+ >>> project = registry.register("/tmp/test")
165
+ >>> registry.get(project.id).name
166
+ 'test'
167
+ >>> registry.get("invalid-id") is None
168
+ True
169
+ """
170
+ with self._lock:
171
+ return self._projects.get(project_id)
172
+
173
+ def get_by_path(self, path: str) -> Optional[Project]:
174
+ """Get project by filesystem path.
175
+
176
+ Resolves path to absolute before lookup.
177
+
178
+ Args:
179
+ path: Filesystem path to project
180
+
181
+ Returns:
182
+ Project instance or None if not found
183
+
184
+ Example:
185
+ >>> registry = ProjectRegistry()
186
+ >>> project = registry.register("/tmp/test")
187
+ >>> found = registry.get_by_path("/tmp/test")
188
+ >>> found.id == project.id
189
+ True
190
+ """
191
+ with self._lock:
192
+ # Resolve to absolute path for consistent lookup
193
+ try:
194
+ abs_path = str(Path(path).resolve())
195
+ except (OSError, ValueError):
196
+ # Invalid path
197
+ return None
198
+
199
+ project_id = self._path_index.get(abs_path)
200
+ if project_id is None:
201
+ return None
202
+
203
+ return self._projects.get(project_id)
204
+
205
+ def list_all(self) -> List[Project]:
206
+ """List all registered projects.
207
+
208
+ Returns:
209
+ List of all Project instances (may be empty)
210
+
211
+ Example:
212
+ >>> registry = ProjectRegistry()
213
+ >>> registry.register("/tmp/proj1")
214
+ >>> registry.register("/tmp/proj2")
215
+ >>> len(registry.list_all())
216
+ 2
217
+ """
218
+ with self._lock:
219
+ return list(self._projects.values())
220
+
221
+ def list_by_state(self, state: ProjectState) -> List[Project]:
222
+ """List projects in specific state.
223
+
224
+ Args:
225
+ state: ProjectState to filter by
226
+
227
+ Returns:
228
+ List of projects in given state (may be empty)
229
+
230
+ Example:
231
+ >>> registry = ProjectRegistry()
232
+ >>> p1 = registry.register("/tmp/proj1")
233
+ >>> p2 = registry.register("/tmp/proj2")
234
+ >>> registry.update_state(p1.id, ProjectState.WORKING)
235
+ >>> working = registry.list_by_state(ProjectState.WORKING)
236
+ >>> len(working)
237
+ 1
238
+ >>> working[0].id == p1.id
239
+ True
240
+ """
241
+ with self._lock:
242
+ return [p for p in self._projects.values() if p.state == state]
243
+
244
+ def update_state(
245
+ self,
246
+ project_id: str,
247
+ state: ProjectState,
248
+ reason: Optional[str] = None,
249
+ ) -> None:
250
+ """Update project state.
251
+
252
+ Updates both state and optional reason, and touches last_activity.
253
+
254
+ Args:
255
+ project_id: Unique project identifier
256
+ state: New ProjectState
257
+ reason: Optional state reason (e.g., error message)
258
+
259
+ Raises:
260
+ KeyError: If project_id not found
261
+
262
+ Example:
263
+ >>> registry = ProjectRegistry()
264
+ >>> project = registry.register("/tmp/test")
265
+ >>> registry.update_state(
266
+ ... project.id,
267
+ ... ProjectState.ERROR,
268
+ ... reason="Connection timeout"
269
+ ... )
270
+ >>> project.state
271
+ <ProjectState.ERROR: 'error'>
272
+ >>> project.state_reason
273
+ 'Connection timeout'
274
+ """
275
+ with self._lock:
276
+ if project_id not in self._projects:
277
+ raise KeyError(f"Project not found: {project_id}")
278
+
279
+ project = self._projects[project_id]
280
+ old_state = project.state
281
+
282
+ project.state = state
283
+ project.state_reason = reason
284
+ project.last_activity = datetime.now(timezone.utc)
285
+
286
+ logger.info(
287
+ "State change: project=%s, %s -> %s, reason=%s",
288
+ project_id,
289
+ old_state.value,
290
+ state.value,
291
+ reason,
292
+ )
293
+
294
+ def add_session(self, project_id: str, session: ToolSession) -> None:
295
+ """Add session to project.
296
+
297
+ Adds tool session to project's session dict and updates last_activity.
298
+
299
+ Args:
300
+ project_id: Unique project identifier
301
+ session: ToolSession to add
302
+
303
+ Raises:
304
+ KeyError: If project_id not found
305
+
306
+ Example:
307
+ >>> registry = ProjectRegistry()
308
+ >>> project = registry.register("/tmp/test")
309
+ >>> session = ToolSession(
310
+ ... id="sess-123",
311
+ ... project_id=project.id,
312
+ ... runtime="claude-code",
313
+ ... tmux_target="commander:test-cc"
314
+ ... )
315
+ >>> registry.add_session(project.id, session)
316
+ >>> len(project.sessions)
317
+ 1
318
+ """
319
+ with self._lock:
320
+ if project_id not in self._projects:
321
+ raise KeyError(f"Project not found: {project_id}")
322
+
323
+ project = self._projects[project_id]
324
+ project.sessions[session.id] = session
325
+ project.last_activity = datetime.now(timezone.utc)
326
+
327
+ logger.info(
328
+ "Added session: project=%s, session=%s, runtime=%s",
329
+ project_id,
330
+ session.id,
331
+ session.runtime,
332
+ )
333
+
334
+ def remove_session(self, project_id: str, session_id: str) -> None:
335
+ """Remove session from project.
336
+
337
+ Removes tool session from project's session dict and updates last_activity.
338
+
339
+ Args:
340
+ project_id: Unique project identifier
341
+ session_id: Session ID to remove
342
+
343
+ Raises:
344
+ KeyError: If project_id not found or session_id not in project
345
+
346
+ Example:
347
+ >>> registry = ProjectRegistry()
348
+ >>> project = registry.register("/tmp/test")
349
+ >>> session = ToolSession(
350
+ ... id="sess-123",
351
+ ... project_id=project.id,
352
+ ... runtime="claude-code",
353
+ ... tmux_target="commander:test-cc"
354
+ ... )
355
+ >>> registry.add_session(project.id, session)
356
+ >>> registry.remove_session(project.id, session.id)
357
+ >>> len(project.sessions)
358
+ 0
359
+ """
360
+ with self._lock:
361
+ if project_id not in self._projects:
362
+ raise KeyError(f"Project not found: {project_id}")
363
+
364
+ project = self._projects[project_id]
365
+
366
+ if session_id not in project.sessions:
367
+ raise KeyError(f"Session not found in project: {session_id}")
368
+
369
+ del project.sessions[session_id]
370
+ project.last_activity = datetime.now(timezone.utc)
371
+
372
+ logger.info(
373
+ "Removed session: project=%s, session=%s",
374
+ project_id,
375
+ session_id,
376
+ )
377
+
378
+ def touch(self, project_id: str) -> None:
379
+ """Update last_activity timestamp.
380
+
381
+ Args:
382
+ project_id: Unique project identifier
383
+
384
+ Raises:
385
+ KeyError: If project_id not found
386
+
387
+ Example:
388
+ >>> import time
389
+ >>> registry = ProjectRegistry()
390
+ >>> project = registry.register("/tmp/test")
391
+ >>> old_time = project.last_activity
392
+ >>> time.sleep(0.01)
393
+ >>> registry.touch(project.id)
394
+ >>> project.last_activity > old_time
395
+ True
396
+ """
397
+ with self._lock:
398
+ if project_id not in self._projects:
399
+ raise KeyError(f"Project not found: {project_id}")
400
+
401
+ project = self._projects[project_id]
402
+ project.last_activity = datetime.now(timezone.utc)
403
+
404
+ logger.debug("Touched project: %s", project_id)
@@ -0,0 +1,10 @@
1
+ """Runtime integration for MPM Commander.
2
+
3
+ This module provides components for spawning and monitoring Claude Code instances
4
+ in tmux panes, enabling autonomous task execution with event detection.
5
+ """
6
+
7
+ from .executor import RuntimeExecutor
8
+ from .monitor import RuntimeMonitor
9
+
10
+ __all__ = ["RuntimeExecutor", "RuntimeMonitor"]
@@ -0,0 +1,191 @@
1
+ """Runtime executor for spawning and managing Claude Code instances in tmux.
2
+
3
+ This module provides RuntimeExecutor which spawns Claude Code processes in tmux
4
+ panes and manages their lifecycle, including sending messages and terminating.
5
+ """
6
+
7
+ import logging
8
+
9
+ from ..models.project import Project
10
+ from ..tmux_orchestrator import TmuxOrchestrator
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class RuntimeExecutor:
16
+ """Spawns and manages Claude Code processes in tmux panes.
17
+
18
+ This class handles the lifecycle of Claude Code instances running in tmux panes,
19
+ providing capabilities to spawn new instances, send messages, check status,
20
+ and terminate processes.
21
+
22
+ Attributes:
23
+ orchestrator: TmuxOrchestrator instance for tmux operations
24
+
25
+ Example:
26
+ >>> orchestrator = TmuxOrchestrator()
27
+ >>> executor = RuntimeExecutor(orchestrator)
28
+ >>> pane_target = await executor.spawn(project, "claude")
29
+ >>> await executor.send_message(pane_target, "Implement user authentication")
30
+ >>> if executor.is_running(pane_target):
31
+ ... await executor.terminate(pane_target)
32
+ """
33
+
34
+ def __init__(self, orchestrator: TmuxOrchestrator):
35
+ """Initialize runtime executor.
36
+
37
+ Args:
38
+ orchestrator: TmuxOrchestrator for tmux operations
39
+
40
+ Raises:
41
+ ValueError: If orchestrator is None
42
+ """
43
+ if orchestrator is None:
44
+ raise ValueError("Orchestrator cannot be None")
45
+
46
+ self.orchestrator = orchestrator
47
+ logger.debug("RuntimeExecutor initialized")
48
+
49
+ async def spawn(self, project: Project, command: str = "claude") -> str:
50
+ """Spawn Claude Code in a new tmux pane for the project.
51
+
52
+ Creates a new tmux pane in the project's working directory and spawns
53
+ the specified command (typically "claude" for Claude Code).
54
+
55
+ Args:
56
+ project: Project instance with working directory
57
+ command: Command to run (default: "claude")
58
+
59
+ Returns:
60
+ The pane target (e.g., '%5') that can be used for subsequent operations
61
+
62
+ Raises:
63
+ RuntimeError: If pane creation fails
64
+ ValueError: If project or project.path is None
65
+
66
+ Example:
67
+ >>> project = Project(id="proj1", path="/path/to/project")
68
+ >>> pane_target = await executor.spawn(project)
69
+ >>> print(f"Spawned in pane: {pane_target}")
70
+ Spawned in pane: %5
71
+ """
72
+ if project is None:
73
+ raise ValueError("Project cannot be None")
74
+ if not project.path:
75
+ raise ValueError("Project path cannot be None or empty")
76
+
77
+ logger.info(
78
+ "Spawning %s for project %s in %s", command, project.id, project.path
79
+ )
80
+
81
+ try:
82
+ # Create tmux session if it doesn't exist
83
+ if not self.orchestrator.session_exists():
84
+ self.orchestrator.create_session()
85
+ logger.debug("Created tmux session")
86
+
87
+ # Create pane with project working directory
88
+ pane_target = self.orchestrator.create_pane(project.id, project.path)
89
+ logger.debug("Created pane: %s", pane_target)
90
+
91
+ # Send command to pane
92
+ self.orchestrator.send_keys(pane_target, command, enter=True)
93
+ logger.info("Spawned %s in pane %s", command, pane_target)
94
+
95
+ return pane_target
96
+
97
+ except Exception as e:
98
+ logger.error(
99
+ "Failed to spawn %s for project %s: %s", command, project.id, e
100
+ )
101
+ raise RuntimeError(f"Failed to spawn {command}: {e}") from e
102
+
103
+ async def send_message(self, pane_target: str, message: str) -> None:
104
+ """Send a message/command to a running Claude instance.
105
+
106
+ Sends the message to the specified tmux pane, followed by Enter.
107
+
108
+ Args:
109
+ pane_target: Pane target from spawn()
110
+ message: Message to send to Claude Code
111
+
112
+ Raises:
113
+ ValueError: If pane_target or message is None/empty
114
+ RuntimeError: If sending message fails
115
+
116
+ Example:
117
+ >>> await executor.send_message("%5", "Fix the authentication bug")
118
+ """
119
+ if not pane_target:
120
+ raise ValueError("Pane target cannot be None or empty")
121
+ if not message:
122
+ raise ValueError("Message cannot be None or empty")
123
+
124
+ logger.debug("Sending message to pane %s: %s", pane_target, message[:50])
125
+
126
+ try:
127
+ self.orchestrator.send_keys(pane_target, message, enter=True)
128
+ logger.info("Sent message to pane %s", pane_target)
129
+
130
+ except Exception as e:
131
+ logger.error("Failed to send message to pane %s: %s", pane_target, e)
132
+ raise RuntimeError(
133
+ f"Failed to send message to pane {pane_target}: {e}"
134
+ ) from e
135
+
136
+ async def terminate(self, pane_target: str) -> None:
137
+ """Terminate a Claude Code instance.
138
+
139
+ Kills the specified tmux pane, terminating the Claude Code process.
140
+
141
+ Args:
142
+ pane_target: Pane target from spawn()
143
+
144
+ Raises:
145
+ ValueError: If pane_target is None/empty
146
+ RuntimeError: If termination fails
147
+
148
+ Example:
149
+ >>> await executor.terminate("%5")
150
+ """
151
+ if not pane_target:
152
+ raise ValueError("Pane target cannot be None or empty")
153
+
154
+ logger.info("Terminating pane %s", pane_target)
155
+
156
+ try:
157
+ self.orchestrator.kill_pane(pane_target)
158
+ logger.info("Terminated pane %s", pane_target)
159
+
160
+ except Exception as e:
161
+ logger.error("Failed to terminate pane %s: %s", pane_target, e)
162
+ raise RuntimeError(f"Failed to terminate pane {pane_target}: {e}") from e
163
+
164
+ def is_running(self, pane_target: str) -> bool:
165
+ """Check if a pane is still active.
166
+
167
+ Args:
168
+ pane_target: Pane target from spawn()
169
+
170
+ Returns:
171
+ True if pane exists and is running, False otherwise
172
+
173
+ Example:
174
+ >>> if executor.is_running("%5"):
175
+ ... print("Pane is still running")
176
+ """
177
+ if not pane_target:
178
+ return False
179
+
180
+ try:
181
+ # List all panes and check if target exists
182
+ panes = self.orchestrator.list_panes()
183
+ pane_ids = [pane["id"] for pane in panes]
184
+ is_active = pane_target in pane_ids
185
+
186
+ logger.debug("Pane %s running: %s", pane_target, is_active)
187
+ return is_active
188
+
189
+ except Exception as e:
190
+ logger.warning("Error checking if pane %s is running: %s", pane_target, e)
191
+ return False