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,191 @@
1
+ """Handles output from Claude Code instances."""
2
+
3
+ import hashlib
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+
8
+ from claude_mpm.commander.llm.summarizer import OutputSummarizer
9
+ from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class OutputChunk:
16
+ """Represents a chunk of output from an instance."""
17
+
18
+ instance_name: str
19
+ raw_output: str
20
+ summary: str | None = None
21
+ timestamp: datetime = field(default_factory=datetime.utcnow)
22
+ is_complete: bool = False
23
+
24
+
25
+ class OutputHandler:
26
+ """Captures and summarizes output from Claude Code instances."""
27
+
28
+ # Patterns that indicate Claude Code has finished responding
29
+ COMPLETION_PATTERNS = [
30
+ "> ", # Default prompt
31
+ "$ ", # Shell prompt
32
+ "\n> ", # Prompt on new line
33
+ ]
34
+
35
+ def __init__(
36
+ self,
37
+ orchestrator: TmuxOrchestrator,
38
+ summarizer: OutputSummarizer | None = None,
39
+ ):
40
+ """Initialize OutputHandler.
41
+
42
+ Args:
43
+ orchestrator: TmuxOrchestrator for capturing output from panes.
44
+ summarizer: Optional OutputSummarizer for long output.
45
+ """
46
+ self.orchestrator = orchestrator
47
+ self.summarizer = summarizer
48
+ self._output_buffers: dict[str, str] = {}
49
+ self._last_output_hash: dict[str, str] = {}
50
+
51
+ async def capture_output(self, pane_target: str, lines: int = 100) -> str:
52
+ """Capture raw output from a pane.
53
+
54
+ Args:
55
+ pane_target: Tmux pane target.
56
+ lines: Number of lines to capture from history.
57
+
58
+ Returns:
59
+ Captured output as string.
60
+ """
61
+ logger.debug(f"Capturing {lines} lines from pane {pane_target}")
62
+ return self.orchestrator.capture_output(pane_target, lines=lines)
63
+
64
+ def _hash_output(self, output: str) -> str:
65
+ """Generate hash of output for change detection.
66
+
67
+ Args:
68
+ output: Output text to hash.
69
+
70
+ Returns:
71
+ MD5 hash of the output.
72
+ """
73
+ # MD5 is sufficient for non-security change detection
74
+ return hashlib.md5(output.encode(), usedforsecurity=False).hexdigest() # nosec B324
75
+
76
+ async def get_new_output(self, instance_name: str, pane_target: str) -> str | None:
77
+ """Get only new output since last capture (diff-based).
78
+
79
+ Args:
80
+ instance_name: Name of the instance.
81
+ pane_target: Tmux pane target.
82
+
83
+ Returns:
84
+ New output since last capture, or None if no change.
85
+ """
86
+ current_output = await self.capture_output(pane_target)
87
+ current_hash = self._hash_output(current_output)
88
+
89
+ # Check if output has changed
90
+ last_hash = self._last_output_hash.get(instance_name)
91
+ if last_hash == current_hash:
92
+ logger.debug(f"No new output for {instance_name}")
93
+ return None
94
+
95
+ # Update hash
96
+ self._last_output_hash[instance_name] = current_hash
97
+
98
+ # For first capture, return full output
99
+ if last_hash is None:
100
+ logger.debug(f"First capture for {instance_name}")
101
+ return current_output
102
+
103
+ # Extract new lines by comparing with buffered output
104
+ last_output = self._output_buffers.get(instance_name, "")
105
+
106
+ # Simple diff: if current output contains last output as prefix,
107
+ # return the difference
108
+ if current_output.startswith(last_output):
109
+ new_output = current_output[len(last_output) :]
110
+ logger.debug(f"New output for {instance_name}: {len(new_output)} chars")
111
+ return new_output
112
+
113
+ # If not a simple append, return full current output
114
+ # (pane may have been cleared or output wrapped)
115
+ logger.debug(f"Output changed significantly for {instance_name}")
116
+ return current_output
117
+
118
+ async def process_output(
119
+ self, instance_name: str, pane_target: str, context: str | None = None
120
+ ) -> OutputChunk | None:
121
+ """Capture new output and optionally summarize.
122
+
123
+ Args:
124
+ instance_name: Name of the instance.
125
+ pane_target: Tmux pane target.
126
+ context: Optional context (e.g., what command was sent).
127
+
128
+ Returns:
129
+ OutputChunk with raw and summarized output, or None if no new output.
130
+ """
131
+ # Get new output
132
+ new_output = await self.get_new_output(instance_name, pane_target)
133
+ if new_output is None:
134
+ return None
135
+
136
+ # Update buffer with full current output
137
+ current_output = await self.capture_output(pane_target)
138
+ self._output_buffers[instance_name] = current_output
139
+
140
+ # Create chunk
141
+ chunk = OutputChunk(
142
+ instance_name=instance_name,
143
+ raw_output=new_output,
144
+ is_complete=self.detect_completion(new_output),
145
+ )
146
+
147
+ # Summarize if needed and summarizer is available
148
+ if self.summarizer and self.summarizer.needs_summarization(new_output):
149
+ try:
150
+ logger.debug(f"Summarizing output for {instance_name}")
151
+ chunk.summary = await self.summarizer.summarize(new_output, context)
152
+ except Exception as e:
153
+ logger.error(f"Failed to summarize output: {e}")
154
+ # Continue without summary
155
+
156
+ return chunk
157
+
158
+ def detect_completion(self, output: str) -> bool:
159
+ """Detect if Claude Code has finished responding (prompt returned).
160
+
161
+ Args:
162
+ output: Output text to check.
163
+
164
+ Returns:
165
+ True if output ends with a completion pattern.
166
+ """
167
+ if not output:
168
+ return False
169
+
170
+ # Strip only trailing newlines/tabs/carriage returns, keep spaces
171
+ output_check = output.rstrip("\n\t\r")
172
+
173
+ for pattern in self.COMPLETION_PATTERNS:
174
+ # Check if output ends with the exact pattern
175
+ if output_check.endswith(pattern):
176
+ return True
177
+
178
+ # Special handling for trailing whitespace:
179
+ # If pattern has trailing space and output has extra trailing spaces,
180
+ # still match (e.g., "> " should match "> " pattern)
181
+ if pattern.endswith(" "):
182
+ pattern_base = pattern.rstrip(" ")
183
+ # Check if after stripping spaces, we have pattern_base followed by at least one space
184
+ output_stripped_spaces = output_check.rstrip(" ")
185
+ # Check: output must end with pattern_base + at least one space
186
+ if output_stripped_spaces.endswith(pattern_base) and len(
187
+ output_check
188
+ ) > len(output_stripped_spaces):
189
+ return True
190
+
191
+ return False
@@ -0,0 +1,155 @@
1
+ """Relays output from instances to user."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Awaitable, Callable
6
+
7
+ from .formatter import OutputFormatter
8
+ from .output_handler import OutputHandler
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class OutputRelay:
14
+ """Relays instance output to user with summarization."""
15
+
16
+ def __init__(
17
+ self,
18
+ handler: OutputHandler,
19
+ formatter: OutputFormatter,
20
+ on_output: Callable[[str], Awaitable[None]] | None = None,
21
+ ):
22
+ """Initialize OutputRelay.
23
+
24
+ Args:
25
+ handler: OutputHandler for capturing output.
26
+ formatter: OutputFormatter for display formatting.
27
+ on_output: Optional callback to display output.
28
+ """
29
+ self.handler = handler
30
+ self.formatter = formatter
31
+ self.on_output = on_output
32
+ self._monitoring: dict[str, asyncio.Task] = {}
33
+
34
+ async def _monitor_output(
35
+ self, instance_name: str, pane_target: str, poll_interval: float
36
+ ) -> None:
37
+ """Monitor output from an instance continuously.
38
+
39
+ Args:
40
+ instance_name: Name of the instance.
41
+ pane_target: Tmux pane target.
42
+ poll_interval: Seconds between polls.
43
+ """
44
+ logger.info(f"Starting output relay for {instance_name}")
45
+
46
+ try:
47
+ while True:
48
+ # Process output
49
+ chunk = await self.handler.process_output(instance_name, pane_target)
50
+
51
+ # If we got new output, format and display
52
+ if chunk and self.on_output:
53
+ # Use summary format if available, otherwise raw
54
+ if chunk.summary:
55
+ output = self.formatter.format_summary(chunk)
56
+ else:
57
+ output = self.formatter.format_raw(chunk)
58
+
59
+ await self.on_output(output)
60
+
61
+ # Wait before next poll
62
+ await asyncio.sleep(poll_interval)
63
+
64
+ except asyncio.CancelledError:
65
+ logger.info(f"Output relay cancelled for {instance_name}")
66
+ raise
67
+ except Exception as e:
68
+ logger.error(f"Error in output relay for {instance_name}: {e}")
69
+ if self.on_output:
70
+ error_msg = self.formatter.format_error(instance_name, str(e))
71
+ try:
72
+ await self.on_output(error_msg)
73
+ except Exception: # nosec B110
74
+ # Intentionally ignore errors in error reporting to avoid cascading failures
75
+ pass
76
+ raise
77
+
78
+ async def start_relay(
79
+ self, instance_name: str, pane_target: str, poll_interval: float = 0.5
80
+ ) -> None:
81
+ """Start relaying output from an instance.
82
+
83
+ Args:
84
+ instance_name: Name of the instance.
85
+ pane_target: Tmux pane target.
86
+ poll_interval: Seconds between polls (default: 0.5).
87
+ """
88
+ # Stop existing relay if any
89
+ if instance_name in self._monitoring:
90
+ await self.stop_relay(instance_name)
91
+
92
+ # Start monitoring task
93
+ task = asyncio.create_task(
94
+ self._monitor_output(instance_name, pane_target, poll_interval)
95
+ )
96
+ self._monitoring[instance_name] = task
97
+
98
+ logger.info(f"Started output relay for {instance_name}")
99
+
100
+ async def stop_relay(self, instance_name: str) -> None:
101
+ """Stop relaying output from an instance.
102
+
103
+ Args:
104
+ instance_name: Name of the instance.
105
+ """
106
+ task = self._monitoring.pop(instance_name, None)
107
+ if task:
108
+ task.cancel()
109
+ try:
110
+ await task
111
+ except asyncio.CancelledError:
112
+ pass
113
+
114
+ logger.info(f"Stopped output relay for {instance_name}")
115
+
116
+ async def stop_all(self) -> None:
117
+ """Stop all output relays."""
118
+ logger.info("Stopping all output relays")
119
+
120
+ # Cancel all tasks
121
+ tasks = list(self._monitoring.values())
122
+ for task in tasks:
123
+ task.cancel()
124
+
125
+ # Wait for cancellation
126
+ await asyncio.gather(*tasks, return_exceptions=True)
127
+
128
+ self._monitoring.clear()
129
+
130
+ async def get_latest_output(
131
+ self, instance_name: str, pane_target: str, context: str | None = None
132
+ ) -> str:
133
+ """Get and format latest output from instance.
134
+
135
+ Args:
136
+ instance_name: Name of the instance.
137
+ pane_target: Tmux pane target.
138
+ context: Optional context for summarization.
139
+
140
+ Returns:
141
+ Formatted output string.
142
+ """
143
+ # Process output once
144
+ chunk = await self.handler.process_output(
145
+ instance_name, pane_target, context=context
146
+ )
147
+
148
+ if chunk is None:
149
+ return self.formatter.format_status(instance_name, "No new output")
150
+
151
+ # Format with summary if available
152
+ if chunk.summary:
153
+ return self.formatter.format_summary(chunk)
154
+
155
+ return self.formatter.format_raw(chunk)