claude-mpm 5.4.65__py3-none-any.whl → 5.4.96__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (174) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md +405 -0
  3. claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +66 -241
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +107 -1928
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +82 -686
  6. claude_mpm/cli/__init__.py +5 -1
  7. claude_mpm/cli/commands/agents.py +2 -4
  8. claude_mpm/cli/commands/agents_reconcile.py +197 -0
  9. claude_mpm/cli/commands/autotodos.py +526 -0
  10. claude_mpm/cli/commands/configure.py +620 -21
  11. claude_mpm/cli/commands/monitor.py +2 -2
  12. claude_mpm/cli/commands/mpm_init/core.py +2 -2
  13. claude_mpm/cli/commands/skills.py +166 -14
  14. claude_mpm/cli/executor.py +89 -0
  15. claude_mpm/cli/interactive/__init__.py +10 -0
  16. claude_mpm/cli/interactive/agent_wizard.py +30 -50
  17. claude_mpm/cli/interactive/questionary_styles.py +65 -0
  18. claude_mpm/cli/interactive/skill_selector.py +481 -0
  19. claude_mpm/cli/parsers/base_parser.py +59 -1
  20. claude_mpm/cli/startup.py +184 -358
  21. claude_mpm/cli/startup_display.py +72 -5
  22. claude_mpm/cli/startup_logging.py +2 -2
  23. claude_mpm/commands/mpm-session-resume.md +1 -1
  24. claude_mpm/constants.py +1 -0
  25. claude_mpm/core/claude_runner.py +2 -2
  26. claude_mpm/core/hook_manager.py +51 -3
  27. claude_mpm/core/interactive_session.py +7 -7
  28. claude_mpm/core/output_style_manager.py +21 -13
  29. claude_mpm/core/unified_config.py +50 -8
  30. claude_mpm/core/unified_paths.py +30 -13
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  89. claude_mpm/dashboard/static/svelte-build/index.html +9 -9
  90. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  91. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  92. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
  97. claude_mpm/hooks/claude_hooks/event_handlers.py +216 -11
  98. claude_mpm/hooks/claude_hooks/hook_handler.py +28 -4
  99. claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
  100. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  101. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  102. claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
  103. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
  104. claude_mpm/hooks/session_resume_hook.py +85 -1
  105. claude_mpm/init.py +1 -1
  106. claude_mpm/services/agents/cache_git_manager.py +1 -1
  107. claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
  108. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
  109. claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
  110. claude_mpm/services/agents/startup_sync.py +5 -2
  111. claude_mpm/services/cli/__init__.py +3 -0
  112. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  113. claude_mpm/services/cli/session_resume_helper.py +10 -2
  114. claude_mpm/services/delegation_detector.py +175 -0
  115. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  116. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  117. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  118. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  119. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  120. claude_mpm/services/diagnostics/models.py +14 -1
  121. claude_mpm/services/event_log.py +317 -0
  122. claude_mpm/services/infrastructure/__init__.py +4 -0
  123. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  124. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  125. claude_mpm/services/monitor/daemon_manager.py +15 -4
  126. claude_mpm/services/monitor/management/lifecycle.py +8 -2
  127. claude_mpm/services/monitor/server.py +106 -16
  128. claude_mpm/services/pm_skills_deployer.py +177 -83
  129. claude_mpm/services/skills/git_skill_source_manager.py +5 -1
  130. claude_mpm/services/skills/selective_skill_deployer.py +114 -26
  131. claude_mpm/services/socketio/handlers/hook.py +14 -7
  132. claude_mpm/services/socketio/server/main.py +12 -4
  133. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  134. claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
  135. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  136. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  137. claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
  138. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  139. claude_mpm/skills/skill_manager.py +4 -4
  140. claude_mpm/utils/agent_dependency_loader.py +4 -2
  141. claude_mpm/utils/robust_installer.py +10 -6
  142. claude_mpm-5.4.96.dist-info/METADATA +377 -0
  143. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/RECORD +153 -142
  144. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
  145. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
  146. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
  147. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
  148. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
  149. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
  150. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
  151. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
  152. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
  153. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  155. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  156. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  157. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  158. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  159. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  160. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  164. claude_mpm-5.4.65.dist-info/METADATA +0 -999
  165. /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
  166. /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
  167. /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
  168. /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
  169. /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
  170. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/WHEEL +0 -0
  171. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/entry_points.txt +0 -0
  172. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE +0 -0
  173. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  174. {claude_mpm-5.4.65.dist-info → claude_mpm-5.4.96.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,317 @@
1
+ """Event Log Service for persistent event storage.
2
+
3
+ WHY this is needed:
4
+ - Decouple event producers from consumers
5
+ - Persist events for later processing (e.g., autotodos CLI)
6
+ - Enable event-driven architecture patterns
7
+ - Provide audit trail of system events
8
+
9
+ DESIGN DECISION: Simple JSON file storage because:
10
+ - Human-readable and inspectable
11
+ - No additional database dependencies
12
+ - Fast for small event volumes
13
+ - Easy to clear and manage
14
+ - Follows existing pattern (hook_error_memory)
15
+ """
16
+
17
+ import json
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Literal, Optional
21
+
22
+ from ..core.logger import get_logger
23
+
24
+ # Event status types
25
+ EventStatus = Literal["pending", "resolved", "archived"]
26
+
27
+ # Max message length to prevent file bloat
28
+ MAX_MESSAGE_LENGTH = 2000
29
+
30
+
31
+ class EventLog:
32
+ """Persistent event log with simple JSON storage.
33
+
34
+ WHY this design:
35
+ - Store events with timestamp, type, payload, status
36
+ - Support filtering by status and event type
37
+ - Prevent file bloat with message truncation
38
+ - Enable mark-as-resolved workflow
39
+ - Keep it simple - no complex queries needed
40
+ """
41
+
42
+ def __init__(self, log_file: Optional[Path] = None):
43
+ """Initialize event log.
44
+
45
+ Args:
46
+ log_file: Path to event log file (default: .claude-mpm/event_log.json)
47
+ """
48
+ self.logger = get_logger("event_log")
49
+
50
+ # Use default location if not specified
51
+ if log_file is None:
52
+ log_file = Path.cwd() / ".claude-mpm" / "event_log.json"
53
+
54
+ self.log_file = log_file
55
+ self.events: List[Dict[str, Any]] = self._load_events()
56
+
57
+ def _load_events(self) -> List[Dict[str, Any]]:
58
+ """Load events from disk.
59
+
60
+ Returns:
61
+ List of event records
62
+ """
63
+ if not self.log_file.exists():
64
+ return []
65
+
66
+ try:
67
+ content = self.log_file.read_text()
68
+ if not content.strip():
69
+ return []
70
+ data = json.loads(content)
71
+
72
+ # Validate structure
73
+ if not isinstance(data, list):
74
+ self.logger.warning("Event log is not a list, resetting")
75
+ return []
76
+
77
+ return data
78
+ except json.JSONDecodeError as e:
79
+ self.logger.warning(f"Failed to parse event log: {e}, resetting")
80
+ return []
81
+ except Exception as e:
82
+ self.logger.error(f"Error loading event log: {e}")
83
+ return []
84
+
85
+ def _save_events(self):
86
+ """Persist events to disk."""
87
+ try:
88
+ # Ensure directory exists
89
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Write with pretty formatting for human readability
92
+ self.log_file.write_text(json.dumps(self.events, indent=2))
93
+ except Exception as e:
94
+ self.logger.error(f"Failed to save event log: {e}")
95
+
96
+ def _truncate_message(self, message: str) -> str:
97
+ """Truncate message to prevent file bloat.
98
+
99
+ Args:
100
+ message: Message to truncate
101
+
102
+ Returns:
103
+ Truncated message with ellipsis if needed
104
+ """
105
+ if len(message) <= MAX_MESSAGE_LENGTH:
106
+ return message
107
+
108
+ return message[:MAX_MESSAGE_LENGTH] + "... (truncated)"
109
+
110
+ def append_event(
111
+ self,
112
+ event_type: str,
113
+ payload: Dict[str, Any],
114
+ status: EventStatus = "pending",
115
+ ) -> str:
116
+ """Append a new event to the log.
117
+
118
+ Args:
119
+ event_type: Type of event (e.g., "autotodo.error", "hook.error")
120
+ payload: Event data (will be truncated if too large)
121
+ status: Event status (default: "pending")
122
+
123
+ Returns:
124
+ Event ID (timestamp-based for simplicity)
125
+ """
126
+ # Truncate any message fields in payload
127
+ truncated_payload = payload.copy()
128
+ if "message" in truncated_payload:
129
+ truncated_payload["message"] = self._truncate_message(
130
+ str(truncated_payload["message"])
131
+ )
132
+ if "full_message" in truncated_payload:
133
+ truncated_payload["full_message"] = self._truncate_message(
134
+ str(truncated_payload["full_message"])
135
+ )
136
+
137
+ # Create event record
138
+ timestamp = datetime.now(timezone.utc).isoformat()
139
+ event = {
140
+ "id": timestamp, # Use timestamp as ID for simplicity
141
+ "timestamp": timestamp,
142
+ "event_type": event_type,
143
+ "payload": truncated_payload,
144
+ "status": status,
145
+ }
146
+
147
+ # Append and save
148
+ self.events.append(event)
149
+ self._save_events()
150
+
151
+ self.logger.debug(f"Appended event: {event_type} (status: {status})")
152
+ return timestamp
153
+
154
+ def list_events(
155
+ self,
156
+ event_type: Optional[str] = None,
157
+ status: Optional[EventStatus] = None,
158
+ limit: Optional[int] = None,
159
+ ) -> List[Dict[str, Any]]:
160
+ """List events with optional filtering.
161
+
162
+ Args:
163
+ event_type: Filter by event type (e.g., "autotodo.error")
164
+ status: Filter by status (e.g., "pending")
165
+ limit: Maximum number of events to return (most recent first)
166
+
167
+ Returns:
168
+ List of matching events
169
+ """
170
+ # Filter events
171
+ filtered = self.events
172
+
173
+ if event_type:
174
+ filtered = [e for e in filtered if e["event_type"] == event_type]
175
+
176
+ if status:
177
+ filtered = [e for e in filtered if e["status"] == status]
178
+
179
+ # Sort by timestamp (most recent first)
180
+ filtered = sorted(filtered, key=lambda e: e["timestamp"], reverse=True)
181
+
182
+ # Apply limit
183
+ if limit:
184
+ filtered = filtered[:limit]
185
+
186
+ return filtered
187
+
188
+ def mark_resolved(self, event_id: str) -> bool:
189
+ """Mark an event as resolved.
190
+
191
+ Args:
192
+ event_id: Event ID (timestamp)
193
+
194
+ Returns:
195
+ True if event was found and updated
196
+ """
197
+ for event in self.events:
198
+ if event["id"] == event_id:
199
+ event["status"] = "resolved"
200
+ event["resolved_at"] = datetime.now(timezone.utc).isoformat()
201
+ self._save_events()
202
+ self.logger.debug(f"Marked event resolved: {event_id}")
203
+ return True
204
+
205
+ return False
206
+
207
+ def mark_all_resolved(
208
+ self, event_type: Optional[str] = None, status: EventStatus = "pending"
209
+ ) -> int:
210
+ """Mark multiple events as resolved.
211
+
212
+ Args:
213
+ event_type: Optional filter by event type
214
+ status: Filter by current status (default: "pending")
215
+
216
+ Returns:
217
+ Number of events marked as resolved
218
+ """
219
+ count = 0
220
+ now = datetime.now(timezone.utc).isoformat()
221
+
222
+ for event in self.events:
223
+ # Check filters
224
+ if event["status"] != status:
225
+ continue
226
+ if event_type and event["event_type"] != event_type:
227
+ continue
228
+
229
+ # Mark resolved
230
+ event["status"] = "resolved"
231
+ event["resolved_at"] = now
232
+ count += 1
233
+
234
+ if count > 0:
235
+ self._save_events()
236
+ self.logger.debug(f"Marked {count} events as resolved")
237
+
238
+ return count
239
+
240
+ def clear_resolved(self, older_than_days: Optional[int] = None) -> int:
241
+ """Remove resolved events from the log.
242
+
243
+ Args:
244
+ older_than_days: Only clear events older than N days
245
+
246
+ Returns:
247
+ Number of events removed
248
+ """
249
+ if older_than_days:
250
+ # Calculate cutoff timestamp
251
+ from datetime import timedelta
252
+
253
+ cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
254
+ cutoff_iso = cutoff.isoformat()
255
+
256
+ # Keep events that are NOT resolved OR are newer than cutoff
257
+ before_count = len(self.events)
258
+ self.events = [
259
+ e
260
+ for e in self.events
261
+ if e["status"] != "resolved" or e.get("resolved_at", "") > cutoff_iso
262
+ ]
263
+ removed = before_count - len(self.events)
264
+ else:
265
+ # Remove all resolved events
266
+ before_count = len(self.events)
267
+ self.events = [e for e in self.events if e["status"] != "resolved"]
268
+ removed = before_count - len(self.events)
269
+
270
+ if removed > 0:
271
+ self._save_events()
272
+ self.logger.debug(f"Cleared {removed} resolved events")
273
+
274
+ return removed
275
+
276
+ def get_stats(self) -> Dict[str, Any]:
277
+ """Get event log statistics.
278
+
279
+ Returns:
280
+ Dictionary with event counts by status and type
281
+ """
282
+ stats = {
283
+ "total_events": len(self.events),
284
+ "by_status": {"pending": 0, "resolved": 0, "archived": 0},
285
+ "by_type": {},
286
+ "log_file": str(self.log_file),
287
+ }
288
+
289
+ for event in self.events:
290
+ # Count by status
291
+ status = event["status"]
292
+ stats["by_status"][status] = stats["by_status"].get(status, 0) + 1
293
+
294
+ # Count by type
295
+ event_type = event["event_type"]
296
+ stats["by_type"][event_type] = stats["by_type"].get(event_type, 0) + 1
297
+
298
+ return stats
299
+
300
+
301
+ # Global instance
302
+ _event_log: Optional[EventLog] = None
303
+
304
+
305
+ def get_event_log(log_file: Optional[Path] = None) -> EventLog:
306
+ """Get the global event log instance.
307
+
308
+ Args:
309
+ log_file: Optional custom log file path
310
+
311
+ Returns:
312
+ EventLog instance
313
+ """
314
+ global _event_log
315
+ if _event_log is None:
316
+ _event_log = EventLog(log_file)
317
+ return _event_log
@@ -11,8 +11,10 @@ Services:
11
11
  - LoggingService: Centralized logging with structured output
12
12
  - HealthMonitor: System health monitoring and alerting
13
13
  - MemoryGuardian: Memory monitoring and process restart management
14
+ - ContextUsageTracker: Token usage tracking across hook invocations
14
15
  """
15
16
 
17
+ from .context_usage_tracker import ContextUsageState, ContextUsageTracker
16
18
  from .logging import LoggingService
17
19
  from .monitoring import (
18
20
  AdvancedHealthMonitor,
@@ -36,6 +38,8 @@ except ImportError:
36
38
 
37
39
  __all__ = [
38
40
  "AdvancedHealthMonitor", # For SocketIO server monitoring
41
+ "ContextUsageState",
42
+ "ContextUsageTracker",
39
43
  "LoggingService",
40
44
  # New service-based monitoring API
41
45
  "MonitoringAggregatorService",
@@ -0,0 +1,291 @@
1
+ """Context Usage Tracker Service.
2
+
3
+ WHY: Track cumulative token usage across Claude Code hook invocations to prevent
4
+ context window exhaustion and enable intelligent auto-pause behavior.
5
+
6
+ DESIGN DECISIONS:
7
+ - File-based persistence (hooks run in separate processes)
8
+ - Atomic file operations using StateStorage
9
+ - Threshold detection at 70% (caution), 85% (warning), 90% (auto_pause), 95% (critical)
10
+ - Session-scoped tracking with reset capability
11
+ - Compatible with 200k context budget for Claude Sonnet 4.5
12
+
13
+ USAGE:
14
+ tracker = ContextUsageTracker()
15
+ state = tracker.update_usage(input_tokens=15000, output_tokens=2000)
16
+ if tracker.should_auto_pause():
17
+ # Trigger auto-pause workflow
18
+ pass
19
+ """
20
+
21
+ from dataclasses import asdict, dataclass, field
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Optional
25
+
26
+ from claude_mpm.core.logger import get_logger
27
+ from claude_mpm.storage.state_storage import StateStorage
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ @dataclass
33
+ class ContextUsageState:
34
+ """State tracking for cumulative context/token usage.
35
+
36
+ Attributes:
37
+ session_id: Unique session identifier
38
+ cumulative_input_tokens: Total input tokens across all hook invocations
39
+ cumulative_output_tokens: Total output tokens across all hook invocations
40
+ cache_creation_tokens: Total tokens spent creating prompt cache
41
+ cache_read_tokens: Total tokens read from prompt cache
42
+ percentage_used: Percentage of 200k context budget used (0.0-100.0)
43
+ threshold_reached: Highest threshold crossed ('caution', 'warning', 'auto_pause', 'critical')
44
+ auto_pause_active: Whether auto-pause has been triggered
45
+ last_updated: ISO timestamp of last update
46
+ """
47
+
48
+ session_id: str
49
+ cumulative_input_tokens: int = 0
50
+ cumulative_output_tokens: int = 0
51
+ cache_creation_tokens: int = 0
52
+ cache_read_tokens: int = 0
53
+ percentage_used: float = 0.0
54
+ threshold_reached: Optional[str] = None
55
+ auto_pause_active: bool = False
56
+ last_updated: str = field(
57
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
58
+ )
59
+
60
+
61
+ class ContextUsageTracker:
62
+ """Track cumulative context/token usage across hook invocations.
63
+
64
+ Features:
65
+ - Cumulative tracking across multiple API calls
66
+ - File-based persistence for cross-process state sharing
67
+ - Atomic file operations for concurrent safety
68
+ - Threshold detection (70%, 85%, 90%, 95%)
69
+ - Auto-pause triggering at 90%+ usage
70
+ - Session management with reset capability
71
+ """
72
+
73
+ # Claude Sonnet 4.5 context window
74
+ CONTEXT_BUDGET = 200000
75
+
76
+ # Threshold levels for warnings and auto-pause
77
+ THRESHOLDS = {
78
+ "caution": 0.70, # Yellow warning
79
+ "warning": 0.85, # Orange warning
80
+ "auto_pause": 0.90, # Trigger auto-pause
81
+ "critical": 0.95, # Red critical alert
82
+ }
83
+
84
+ def __init__(self, project_path: Optional[Path] = None):
85
+ """Initialize context usage tracker.
86
+
87
+ Args:
88
+ project_path: Project root path (default: current directory)
89
+ """
90
+ self.project_path = (project_path or Path.cwd()).resolve()
91
+ self.state_dir = self.project_path / ".claude-mpm" / "state"
92
+ self.state_dir.mkdir(parents=True, exist_ok=True)
93
+ self.state_file = self.state_dir / "context-usage.json"
94
+
95
+ # Use StateStorage for atomic operations
96
+ self.storage = StateStorage(self.state_dir)
97
+
98
+ logger.debug(f"ContextUsageTracker initialized: {self.state_file}")
99
+
100
+ def update_usage(
101
+ self,
102
+ input_tokens: int,
103
+ output_tokens: int,
104
+ cache_creation: int = 0,
105
+ cache_read: int = 0,
106
+ ) -> ContextUsageState:
107
+ """Update cumulative usage from API response.
108
+
109
+ Args:
110
+ input_tokens: Input tokens from this API call
111
+ output_tokens: Output tokens from this API call
112
+ cache_creation: Cache creation tokens (optional)
113
+ cache_read: Cache read tokens (optional)
114
+
115
+ Returns:
116
+ Updated context usage state
117
+
118
+ Raises:
119
+ ValueError: If token counts are negative
120
+ """
121
+ if any(
122
+ t < 0 for t in [input_tokens, output_tokens, cache_creation, cache_read]
123
+ ):
124
+ raise ValueError("Token counts cannot be negative")
125
+
126
+ # Load current state
127
+ state = self._load_state()
128
+
129
+ # Update cumulative counters
130
+ state.cumulative_input_tokens += input_tokens
131
+ state.cumulative_output_tokens += output_tokens
132
+ state.cache_creation_tokens += cache_creation
133
+ state.cache_read_tokens += cache_read
134
+
135
+ # Calculate total effective tokens (input + output, cache read is "free")
136
+ total_tokens = state.cumulative_input_tokens + state.cumulative_output_tokens
137
+ state.percentage_used = (total_tokens / self.CONTEXT_BUDGET) * 100
138
+
139
+ # Check thresholds
140
+ state.threshold_reached = self.check_thresholds(state)
141
+
142
+ # Activate auto-pause if threshold reached
143
+ if state.threshold_reached in {"auto_pause", "critical"}:
144
+ state.auto_pause_active = True
145
+
146
+ # Update timestamp
147
+ state.last_updated = datetime.now(timezone.utc).isoformat()
148
+
149
+ # Persist state atomically
150
+ self._save_state(state)
151
+
152
+ logger.debug(
153
+ f"Usage updated: {total_tokens}/{self.CONTEXT_BUDGET} tokens "
154
+ f"({state.percentage_used:.1f}%), threshold: {state.threshold_reached}"
155
+ )
156
+
157
+ return state
158
+
159
+ def check_thresholds(
160
+ self, state: Optional[ContextUsageState] = None
161
+ ) -> Optional[str]:
162
+ """Check which threshold (if any) has been exceeded.
163
+
164
+ Args:
165
+ state: Optional state to check (uses current state if None)
166
+
167
+ Returns:
168
+ Highest threshold exceeded ('caution', 'warning', 'auto_pause', 'critical')
169
+ or None if no thresholds exceeded
170
+ """
171
+ if state is None:
172
+ state = self.get_current_state()
173
+
174
+ percentage = state.percentage_used / 100 # Convert to 0.0-1.0
175
+
176
+ # Check thresholds in descending order (highest first)
177
+ for threshold_name in ["critical", "auto_pause", "warning", "caution"]:
178
+ if percentage >= self.THRESHOLDS[threshold_name]:
179
+ return threshold_name
180
+
181
+ return None
182
+
183
+ def should_auto_pause(self) -> bool:
184
+ """Check if auto-pause should be triggered.
185
+
186
+ Returns:
187
+ True if 90%+ context budget used
188
+ """
189
+ state = self.get_current_state()
190
+ return state.percentage_used >= (self.THRESHOLDS["auto_pause"] * 100)
191
+
192
+ def get_current_state(self) -> ContextUsageState:
193
+ """Get current usage state without modifying.
194
+
195
+ Returns:
196
+ Current context usage state
197
+ """
198
+ return self._load_state()
199
+
200
+ def reset_session(self, new_session_id: str) -> None:
201
+ """Reset tracking for a new session.
202
+
203
+ Args:
204
+ new_session_id: New session identifier
205
+ """
206
+ state = ContextUsageState(session_id=new_session_id)
207
+ self._save_state(state)
208
+ logger.info(f"Context usage reset for new session: {new_session_id}")
209
+
210
+ def _load_state(self) -> ContextUsageState:
211
+ """Load state from persistence file.
212
+
213
+ Returns:
214
+ Loaded state or default state if file doesn't exist/is corrupted
215
+ """
216
+ try:
217
+ if not self.state_file.exists():
218
+ # Generate initial session ID
219
+ session_id = (
220
+ f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
221
+ )
222
+ logger.debug("No state file found, creating default state")
223
+ return ContextUsageState(session_id=session_id)
224
+
225
+ # Load JSON state
226
+ data = self.storage.read_json(self.state_file)
227
+
228
+ if data is None:
229
+ logger.warning("Failed to read state file, using default state")
230
+ session_id = (
231
+ f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
232
+ )
233
+ return ContextUsageState(session_id=session_id)
234
+
235
+ # Reconstruct ContextUsageState from dict
236
+ return ContextUsageState(**data)
237
+
238
+ except Exception as e:
239
+ logger.error(f"Error loading state, using default: {e}")
240
+ session_id = (
241
+ f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
242
+ )
243
+ return ContextUsageState(session_id=session_id)
244
+
245
+ def _save_state(self, state: ContextUsageState) -> None:
246
+ """Save state to persistence file atomically.
247
+
248
+ Args:
249
+ state: Context usage state to persist
250
+
251
+ Raises:
252
+ RuntimeError: If atomic write fails
253
+ """
254
+ try:
255
+ # Convert dataclass to dict
256
+ state_dict = asdict(state)
257
+
258
+ # Atomic write using StateStorage
259
+ if not self.storage.write_json(state_dict, self.state_file, atomic=True):
260
+ raise RuntimeError(f"Failed to write state to {self.state_file}")
261
+
262
+ logger.debug(f"State saved: {self.state_file}")
263
+
264
+ except Exception as e:
265
+ logger.error(f"Error saving state: {e}")
266
+ raise RuntimeError(f"Failed to persist context usage state: {e}") from e
267
+
268
+ def get_usage_summary(self) -> dict:
269
+ """Get human-readable usage summary.
270
+
271
+ Returns:
272
+ Dictionary with usage statistics
273
+ """
274
+ state = self.get_current_state()
275
+ total_tokens = state.cumulative_input_tokens + state.cumulative_output_tokens
276
+
277
+ return {
278
+ "session_id": state.session_id,
279
+ "total_tokens": total_tokens,
280
+ "budget": self.CONTEXT_BUDGET,
281
+ "percentage_used": round(state.percentage_used, 2),
282
+ "threshold_reached": state.threshold_reached,
283
+ "auto_pause_active": state.auto_pause_active,
284
+ "breakdown": {
285
+ "input_tokens": state.cumulative_input_tokens,
286
+ "output_tokens": state.cumulative_output_tokens,
287
+ "cache_creation_tokens": state.cache_creation_tokens,
288
+ "cache_read_tokens": state.cache_read_tokens,
289
+ },
290
+ "last_updated": state.last_updated,
291
+ }
@@ -6,7 +6,8 @@ Integrates with session management and response tracking infrastructure.
6
6
  Triggers:
7
7
  - model_context_window_exceeded (stop_reason)
8
8
  - Manual pause command
9
- - 95% token threshold reached
9
+ - 95% token threshold reached (critical)
10
+ - 90% token threshold reached (auto-pause)
10
11
  - Session end with high token usage (>85%)
11
12
 
12
13
  Design Principles:
@@ -71,6 +72,7 @@ class ResumeLogGenerator:
71
72
  thresholds = self.config.get("context_management", {}).get("thresholds", {})
72
73
  self.threshold_caution = thresholds.get("caution", 0.70)
73
74
  self.threshold_warning = thresholds.get("warning", 0.85)
75
+ self.threshold_auto_pause = thresholds.get("auto_pause", 0.90)
74
76
  self.threshold_critical = thresholds.get("critical", 0.95)
75
77
 
76
78
  logger.info(
@@ -96,14 +98,14 @@ class ResumeLogGenerator:
96
98
  if not self.enabled or not self.auto_generate:
97
99
  return manual_trigger # Only generate on manual trigger if auto is disabled
98
100
 
99
- # Trigger conditions
101
+ # Trigger conditions (ordered by severity)
100
102
  triggers = [
101
103
  stop_reason == "max_tokens",
102
104
  stop_reason == "model_context_window_exceeded",
103
105
  manual_trigger,
104
- token_usage_pct and token_usage_pct >= self.threshold_critical,
105
- token_usage_pct
106
- and token_usage_pct >= self.threshold_warning, # Generate at 85% too
106
+ token_usage_pct and token_usage_pct >= self.threshold_critical, # 95%
107
+ token_usage_pct and token_usage_pct >= self.threshold_auto_pause, # 90%
108
+ token_usage_pct and token_usage_pct >= self.threshold_warning, # 85%
107
109
  ]
108
110
 
109
111
  should_gen = any(triggers)
@@ -121,6 +123,22 @@ class ResumeLogGenerator:
121
123
 
122
124
  return should_gen
123
125
 
126
+ def should_auto_pause(self, token_usage_pct: Optional[float]) -> bool:
127
+ """Check if auto-pause threshold (90%) has been reached.
128
+
129
+ This is a convenience method to check specifically for the 90% threshold
130
+ which triggers automatic session pausing.
131
+
132
+ Args:
133
+ token_usage_pct: Current token usage percentage (0.0-1.0)
134
+
135
+ Returns:
136
+ True if auto-pause threshold has been reached
137
+ """
138
+ if token_usage_pct is None:
139
+ return False
140
+ return token_usage_pct >= self.threshold_auto_pause
141
+
124
142
  def generate_from_session_state(
125
143
  self,
126
144
  session_id: str,
@@ -427,6 +445,7 @@ class ResumeLogGenerator:
427
445
  "thresholds": {
428
446
  "caution": f"{self.threshold_caution:.0%}",
429
447
  "warning": f"{self.threshold_warning:.0%}",
448
+ "auto_pause": f"{self.threshold_auto_pause:.0%}",
430
449
  "critical": f"{self.threshold_critical:.0%}",
431
450
  },
432
451
  }