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,486 @@
1
+ #!/usr/bin/env python3
2
+ """Auto-pause handler for Claude Code hooks.
3
+
4
+ WHY: Automatically pause Claude sessions when context usage reaches 90% to prevent
5
+ context window exhaustion. Integrates with existing hook infrastructure to monitor
6
+ token usage and trigger incremental pause capture.
7
+
8
+ DESIGN DECISIONS:
9
+ - Integrates with ContextUsageTracker for token tracking across hook invocations
10
+ - Uses IncrementalPauseManager for capturing actions during pause mode
11
+ - Thread-safe - handles hook calls from multiple processes via file-based state
12
+ - Emits warnings to stderr for visibility without breaking hook flow
13
+ - Only triggers auto-pause on NEW threshold crossings (prevents duplicate warnings)
14
+ - Graceful error handling - auto-pause failures don't break main hook processing
15
+
16
+ USAGE:
17
+ # Initialize handler in hook handler
18
+ auto_pause = AutoPauseHandler()
19
+
20
+ # Monitor token usage from API responses
21
+ if "usage" in metadata:
22
+ threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
23
+ if threshold_crossed:
24
+ warning = auto_pause.emit_threshold_warning(threshold_crossed)
25
+ print(f"\n⚠️ {warning}", file=sys.stderr)
26
+
27
+ # Record actions during pause mode
28
+ if auto_pause.is_pause_active():
29
+ auto_pause.on_tool_call(tool_name, tool_args)
30
+ auto_pause.on_assistant_response(response_summary)
31
+
32
+ # Finalize on session end
33
+ session_file = auto_pause.on_session_end()
34
+ """
35
+
36
+ import os
37
+ import sys
38
+ from datetime import datetime, timezone
39
+ from pathlib import Path
40
+ from typing import Any, Dict, Optional
41
+
42
+ from claude_mpm.core.logger import get_logger
43
+ from claude_mpm.services.cli.incremental_pause_manager import IncrementalPauseManager
44
+ from claude_mpm.services.infrastructure.context_usage_tracker import (
45
+ ContextUsageTracker,
46
+ )
47
+
48
+ logger = get_logger(__name__)
49
+
50
+ # Debug mode
51
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
52
+
53
+ # Warning messages for threshold crossings
54
+ THRESHOLD_WARNINGS = {
55
+ "caution": "Context usage at 70%. Consider wrapping up current work.",
56
+ "warning": "Context usage at 85%. Session nearing capacity.",
57
+ "auto_pause": "Context usage at 90%. Auto-pause activated. Actions are being recorded for session continuity.",
58
+ "critical": "Context usage at 95%. Session nearly exhausted. Wrapping up...",
59
+ }
60
+
61
+ # Maximum length for summaries to avoid storing full responses
62
+ MAX_SUMMARY_LENGTH = 500
63
+
64
+
65
+ class AutoPauseHandler:
66
+ """Handler for automatic session pausing based on context usage thresholds.
67
+
68
+ Integrates with Claude Code hooks to:
69
+ 1. Track cumulative token usage from API responses
70
+ 2. Trigger auto-pause when 90% context used
71
+ 3. Capture all subsequent actions during pause mode
72
+ 4. Emit warnings/notifications to user
73
+
74
+ Features:
75
+ - File-based state persistence (works across hook process restarts)
76
+ - Thread-safe through atomic file operations
77
+ - Graceful error handling (failures don't break main hook flow)
78
+ - Only emits warnings on NEW threshold crossings
79
+ - Summarizes long content to prevent memory bloat
80
+ """
81
+
82
+ def __init__(self, project_path: Optional[Path] = None):
83
+ """Initialize auto-pause handler.
84
+
85
+ Args:
86
+ project_path: Project root path (default: current directory)
87
+ """
88
+ self.project_path = (project_path or Path.cwd()).resolve()
89
+
90
+ # Initialize services
91
+ self.tracker = ContextUsageTracker(self.project_path)
92
+ self.pause_manager = IncrementalPauseManager(self.project_path)
93
+
94
+ # Track previous threshold to detect NEW crossings
95
+ self._previous_threshold: Optional[str] = None
96
+
97
+ # Load initial state
98
+ try:
99
+ current_state = self.tracker.get_current_state()
100
+ self._previous_threshold = current_state.threshold_reached
101
+
102
+ if DEBUG:
103
+ print(
104
+ f"AutoPauseHandler initialized: "
105
+ f"{current_state.percentage_used:.1f}% context used, "
106
+ f"threshold: {current_state.threshold_reached}",
107
+ file=sys.stderr,
108
+ )
109
+ except Exception as e:
110
+ logger.error(f"Failed to initialize AutoPauseHandler: {e}")
111
+ # Continue with None - will initialize on first update
112
+
113
+ def on_usage_update(self, usage: Dict[str, Any]) -> Optional[str]:
114
+ """Process token usage from a Claude API response.
115
+
116
+ Args:
117
+ usage: Dict with 'input_tokens', 'output_tokens',
118
+ 'cache_creation_input_tokens', 'cache_read_input_tokens'
119
+
120
+ Returns:
121
+ Threshold name if a NEW threshold was crossed ('caution', 'warning',
122
+ 'auto_pause', 'critical'), or None if no new threshold crossed.
123
+
124
+ Raises:
125
+ ValueError: If usage data is invalid
126
+ """
127
+ try:
128
+ # Extract token counts
129
+ input_tokens = usage.get("input_tokens", 0)
130
+ output_tokens = usage.get("output_tokens", 0)
131
+ cache_creation = usage.get("cache_creation_input_tokens", 0)
132
+ cache_read = usage.get("cache_read_input_tokens", 0)
133
+
134
+ # Validate token counts
135
+ if any(
136
+ t < 0 for t in [input_tokens, output_tokens, cache_creation, cache_read]
137
+ ):
138
+ raise ValueError("Token counts cannot be negative")
139
+
140
+ # Update cumulative usage
141
+ state = self.tracker.update_usage(
142
+ input_tokens=input_tokens,
143
+ output_tokens=output_tokens,
144
+ cache_creation=cache_creation,
145
+ cache_read=cache_read,
146
+ )
147
+
148
+ # Check if we crossed a NEW threshold
149
+ current_threshold = state.threshold_reached
150
+ new_threshold_crossed = None
151
+
152
+ if current_threshold != self._previous_threshold:
153
+ # Determine if this is a higher threshold
154
+ threshold_order = ["caution", "warning", "auto_pause", "critical"]
155
+
156
+ prev_idx = (
157
+ threshold_order.index(self._previous_threshold)
158
+ if self._previous_threshold in threshold_order
159
+ else -1
160
+ )
161
+ curr_idx = (
162
+ threshold_order.index(current_threshold)
163
+ if current_threshold in threshold_order
164
+ else -1
165
+ )
166
+
167
+ if curr_idx > prev_idx:
168
+ new_threshold_crossed = current_threshold
169
+ self._previous_threshold = current_threshold
170
+
171
+ if DEBUG:
172
+ print(
173
+ f"Context threshold crossed: {current_threshold} "
174
+ f"({state.percentage_used:.1f}%)",
175
+ file=sys.stderr,
176
+ )
177
+
178
+ # Trigger auto-pause if threshold reached
179
+ if current_threshold in ["auto_pause", "critical"]:
180
+ self._trigger_auto_pause(state)
181
+
182
+ return new_threshold_crossed
183
+
184
+ except Exception as e:
185
+ logger.error(f"Failed to update usage: {e}")
186
+ if DEBUG:
187
+ print(f"❌ Usage update failed: {e}", file=sys.stderr)
188
+ # Don't propagate error - auto-pause is optional
189
+ return None
190
+
191
+ def on_tool_call(self, tool_name: str, tool_args: Dict[str, Any]) -> None:
192
+ """Record a tool call if auto-pause is active.
193
+
194
+ Args:
195
+ tool_name: Name of the tool being called
196
+ tool_args: Tool arguments dictionary
197
+
198
+ Raises:
199
+ RuntimeError: If append operation fails (optional, logged only)
200
+ """
201
+ if not self.is_pause_active():
202
+ return
203
+
204
+ try:
205
+ # Summarize tool args to avoid storing large data
206
+ args_summary = self._summarize_dict(tool_args)
207
+
208
+ # Get current context percentage
209
+ state = self.tracker.get_current_state()
210
+
211
+ # Record action
212
+ self.pause_manager.append_action(
213
+ action_type="tool_call",
214
+ action_data={
215
+ "tool": tool_name,
216
+ "args_summary": args_summary,
217
+ "timestamp": datetime.now(timezone.utc).isoformat(),
218
+ },
219
+ context_percentage=state.percentage_used / 100,
220
+ )
221
+
222
+ if DEBUG:
223
+ print(f"Recorded tool call during pause: {tool_name}", file=sys.stderr)
224
+
225
+ except Exception as e:
226
+ logger.error(f"Failed to record tool call: {e}")
227
+ if DEBUG:
228
+ print(f"❌ Failed to record tool call: {e}", file=sys.stderr)
229
+
230
+ def on_assistant_response(self, response_summary: str) -> None:
231
+ """Record an assistant response if auto-pause is active.
232
+
233
+ Args:
234
+ response_summary: Summary of assistant response (will be truncated)
235
+
236
+ Raises:
237
+ RuntimeError: If append operation fails (optional, logged only)
238
+ """
239
+ if not self.is_pause_active():
240
+ return
241
+
242
+ try:
243
+ # Truncate long responses
244
+ summary = self._truncate_text(response_summary, MAX_SUMMARY_LENGTH)
245
+
246
+ # Get current context percentage
247
+ state = self.tracker.get_current_state()
248
+
249
+ # Record action
250
+ self.pause_manager.append_action(
251
+ action_type="assistant_response",
252
+ action_data={
253
+ "summary": summary,
254
+ "timestamp": datetime.now(timezone.utc).isoformat(),
255
+ },
256
+ context_percentage=state.percentage_used / 100,
257
+ )
258
+
259
+ if DEBUG:
260
+ print(
261
+ f"Recorded assistant response during pause (length: {len(summary)})",
262
+ file=sys.stderr,
263
+ )
264
+
265
+ except Exception as e:
266
+ logger.error(f"Failed to record assistant response: {e}")
267
+ if DEBUG:
268
+ print(f"❌ Failed to record assistant response: {e}", file=sys.stderr)
269
+
270
+ def on_user_message(self, message_summary: str) -> None:
271
+ """Record a user message if auto-pause is active.
272
+
273
+ Args:
274
+ message_summary: Summary of user message (will be truncated)
275
+
276
+ Raises:
277
+ RuntimeError: If append operation fails (optional, logged only)
278
+ """
279
+ if not self.is_pause_active():
280
+ return
281
+
282
+ try:
283
+ # Truncate long messages
284
+ summary = self._truncate_text(message_summary, MAX_SUMMARY_LENGTH)
285
+
286
+ # Get current context percentage
287
+ state = self.tracker.get_current_state()
288
+
289
+ # Record action
290
+ self.pause_manager.append_action(
291
+ action_type="user_message",
292
+ action_data={
293
+ "summary": summary,
294
+ "timestamp": datetime.now(timezone.utc).isoformat(),
295
+ },
296
+ context_percentage=state.percentage_used / 100,
297
+ )
298
+
299
+ if DEBUG:
300
+ print(
301
+ f"Recorded user message during pause (length: {len(summary)})",
302
+ file=sys.stderr,
303
+ )
304
+
305
+ except Exception as e:
306
+ logger.error(f"Failed to record user message: {e}")
307
+ if DEBUG:
308
+ print(f"❌ Failed to record user message: {e}", file=sys.stderr)
309
+
310
+ def on_session_end(self) -> Optional[Path]:
311
+ """Called when session ends. Finalizes any active pause.
312
+
313
+ Returns:
314
+ Path to finalized session file, or None if no pause was active.
315
+
316
+ Raises:
317
+ RuntimeError: If finalization fails
318
+ """
319
+ if not self.is_pause_active():
320
+ if DEBUG:
321
+ print("No active pause to finalize", file=sys.stderr)
322
+ return None
323
+
324
+ try:
325
+ # Finalize the pause session
326
+ session_path = self.pause_manager.finalize_pause(create_full_snapshot=True)
327
+
328
+ if session_path and DEBUG:
329
+ print(f"✅ Session finalized: {session_path.name}", file=sys.stderr)
330
+
331
+ return session_path
332
+
333
+ except Exception as e:
334
+ logger.error(f"Failed to finalize pause session: {e}")
335
+ if DEBUG:
336
+ print(f"❌ Failed to finalize pause: {e}", file=sys.stderr)
337
+ raise
338
+
339
+ def is_pause_active(self) -> bool:
340
+ """Check if auto-pause mode is currently active.
341
+
342
+ Returns:
343
+ True if auto-pause has been triggered and is capturing actions
344
+ """
345
+ return self.pause_manager.is_pause_active()
346
+
347
+ def get_status(self) -> Dict[str, Any]:
348
+ """Get current status for display/logging.
349
+
350
+ Returns:
351
+ Dict with: context_percentage, threshold_reached,
352
+ pause_active, actions_recorded, etc.
353
+ """
354
+ try:
355
+ state = self.tracker.get_current_state()
356
+ pause_summary = self.pause_manager.get_pause_summary()
357
+
358
+ status = {
359
+ "context_percentage": round(state.percentage_used, 2),
360
+ "threshold_reached": state.threshold_reached,
361
+ "auto_pause_active": state.auto_pause_active,
362
+ "pause_active": self.is_pause_active(),
363
+ "session_id": state.session_id,
364
+ "total_tokens": (
365
+ state.cumulative_input_tokens + state.cumulative_output_tokens
366
+ ),
367
+ "budget": ContextUsageTracker.CONTEXT_BUDGET,
368
+ }
369
+
370
+ # Add pause details if active
371
+ if pause_summary:
372
+ status["pause_details"] = {
373
+ "action_count": pause_summary["action_count"],
374
+ "duration_seconds": pause_summary["duration_seconds"],
375
+ "context_range": pause_summary["context_range"],
376
+ "last_action_type": pause_summary["last_action_type"],
377
+ }
378
+
379
+ return status
380
+
381
+ except Exception as e:
382
+ logger.error(f"Failed to get status: {e}")
383
+ return {"error": str(e)}
384
+
385
+ def emit_threshold_warning(self, threshold: str) -> str:
386
+ """Generate a warning message for threshold crossing.
387
+
388
+ Args:
389
+ threshold: Threshold name ('caution', 'warning', 'auto_pause', 'critical')
390
+
391
+ Returns:
392
+ User-friendly warning message string
393
+ """
394
+ warning = THRESHOLD_WARNINGS.get(
395
+ threshold, f"Context usage threshold reached: {threshold}"
396
+ )
397
+
398
+ # Add context percentage to warning
399
+ try:
400
+ state = self.tracker.get_current_state()
401
+ warning = f"{warning} ({state.percentage_used:.1f}%)"
402
+ except Exception:
403
+ pass # nosec B110 - Intentionally ignore formatting errors, warning is already constructed
404
+
405
+ return warning
406
+
407
+ def _trigger_auto_pause(self, state) -> None:
408
+ """Trigger auto-pause and start recording actions.
409
+
410
+ Args:
411
+ state: Current context usage state
412
+
413
+ Raises:
414
+ RuntimeError: If pause cannot be started
415
+ """
416
+ try:
417
+ # Check if pause is already active
418
+ if self.is_pause_active():
419
+ if DEBUG:
420
+ print(
421
+ "Auto-pause already active, skipping trigger", file=sys.stderr
422
+ )
423
+ return
424
+
425
+ # Start incremental pause
426
+ session_id = self.pause_manager.start_incremental_pause(
427
+ context_percentage=state.percentage_used / 100,
428
+ initial_state=state.__dict__,
429
+ )
430
+
431
+ if DEBUG:
432
+ print(
433
+ f"✅ Auto-pause triggered: {session_id} "
434
+ f"({state.percentage_used:.1f}% context used)",
435
+ file=sys.stderr,
436
+ )
437
+
438
+ except Exception as e:
439
+ logger.error(f"Failed to trigger auto-pause: {e}")
440
+ if DEBUG:
441
+ print(f"❌ Failed to trigger auto-pause: {e}", file=sys.stderr)
442
+ # Don't propagate - auto-pause is optional
443
+
444
+ def _summarize_dict(
445
+ self, data: Dict[str, Any], max_items: int = 10
446
+ ) -> Dict[str, Any]:
447
+ """Create a summary of a dictionary by limiting items and truncating values.
448
+
449
+ Args:
450
+ data: Dictionary to summarize
451
+ max_items: Maximum number of items to include
452
+
453
+ Returns:
454
+ Summarized dictionary
455
+ """
456
+ summary = {}
457
+
458
+ for i, (key, value) in enumerate(data.items()):
459
+ if i >= max_items:
460
+ summary["..."] = f"({len(data) - max_items} more items)"
461
+ break
462
+
463
+ # Truncate string values
464
+ if isinstance(value, str):
465
+ summary[key] = self._truncate_text(value, 100)
466
+ elif isinstance(value, (list, dict)):
467
+ summary[key] = f"<{type(value).__name__} with {len(value)} items>"
468
+ else:
469
+ summary[key] = str(value)[:100]
470
+
471
+ return summary
472
+
473
+ def _truncate_text(self, text: str, max_length: int) -> str:
474
+ """Truncate text to maximum length with ellipsis.
475
+
476
+ Args:
477
+ text: Text to truncate
478
+ max_length: Maximum length
479
+
480
+ Returns:
481
+ Truncated text with "..." suffix if truncated
482
+ """
483
+ if len(text) <= max_length:
484
+ return text
485
+
486
+ return text[: max_length - 3] + "..."