claude-mpm 5.4.64__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md +405 -0
- claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +66 -241
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +107 -1928
- claude_mpm/agents/PM_INSTRUCTIONS.md +82 -686
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +2 -4
- claude_mpm/cli/commands/agents_reconcile.py +197 -0
- claude_mpm/cli/commands/autotodos.py +526 -0
- claude_mpm/cli/commands/configure.py +620 -21
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init/core.py +2 -2
- claude_mpm/cli/commands/skills.py +166 -14
- claude_mpm/cli/executor.py +89 -0
- claude_mpm/cli/interactive/__init__.py +10 -0
- claude_mpm/cli/interactive/agent_wizard.py +30 -50
- claude_mpm/cli/interactive/questionary_styles.py +65 -0
- claude_mpm/cli/interactive/skill_selector.py +481 -0
- claude_mpm/cli/parsers/base_parser.py +59 -1
- claude_mpm/cli/startup.py +202 -367
- claude_mpm/cli/startup_display.py +72 -5
- claude_mpm/cli/startup_logging.py +2 -2
- claude_mpm/commands/mpm-session-resume.md +1 -1
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -2
- claude_mpm/core/hook_manager.py +51 -3
- claude_mpm/core/interactive_session.py +7 -7
- claude_mpm/core/output_style_manager.py +21 -13
- claude_mpm/core/unified_config.py +50 -8
- claude_mpm/core/unified_paths.py +30 -13
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
- claude_mpm/dashboard/static/svelte-build/index.html +9 -9
- claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
- claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +216 -11
- claude_mpm/hooks/claude_hooks/hook_handler.py +28 -4
- claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
- claude_mpm/hooks/session_resume_hook.py +85 -1
- claude_mpm/init.py +1 -1
- claude_mpm/services/agents/cache_git_manager.py +1 -1
- claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
- claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
- claude_mpm/services/agents/startup_sync.py +5 -2
- claude_mpm/services/cli/__init__.py +3 -0
- claude_mpm/services/cli/incremental_pause_manager.py +561 -0
- claude_mpm/services/cli/session_resume_helper.py +10 -2
- claude_mpm/services/delegation_detector.py +175 -0
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
- claude_mpm/services/diagnostics/models.py +14 -1
- claude_mpm/services/event_log.py +317 -0
- claude_mpm/services/infrastructure/__init__.py +4 -0
- claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
- claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
- claude_mpm/services/monitor/daemon_manager.py +15 -4
- claude_mpm/services/monitor/management/lifecycle.py +8 -2
- claude_mpm/services/monitor/server.py +106 -16
- claude_mpm/services/pm_skills_deployer.py +177 -83
- claude_mpm/services/skills/git_skill_source_manager.py +5 -1
- claude_mpm/services/skills/selective_skill_deployer.py +114 -26
- claude_mpm/services/socketio/handlers/hook.py +14 -7
- claude_mpm/services/socketio/server/main.py +12 -4
- claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
- claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
- claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
- claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
- claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
- claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
- claude_mpm/skills/skill_manager.py +4 -4
- claude_mpm/utils/agent_dependency_loader.py +103 -4
- claude_mpm/utils/robust_installer.py +45 -24
- claude_mpm-5.4.96.dist-info/METADATA +377 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/RECORD +153 -131
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
- claude_mpm-5.4.64.dist-info/METADATA +0 -999
- /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ Claude Code hook events.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
-
import subprocess
|
|
10
|
+
import subprocess # nosec B404 - subprocess used for safe claude CLI version checking only
|
|
11
11
|
import sys
|
|
12
12
|
import uuid
|
|
13
13
|
from datetime import datetime, timezone
|
|
@@ -115,7 +115,7 @@ class EventHandlers:
|
|
|
115
115
|
f"Stored prompt for comprehensive tracking: session {session_id[:8]}...",
|
|
116
116
|
file=sys.stderr,
|
|
117
117
|
)
|
|
118
|
-
except Exception:
|
|
118
|
+
except Exception: # nosec B110
|
|
119
119
|
# Response tracking is optional - silently continue if it fails
|
|
120
120
|
pass
|
|
121
121
|
|
|
@@ -189,8 +189,34 @@ class EventHandlers:
|
|
|
189
189
|
if tool_name == "Task" and isinstance(tool_input, dict):
|
|
190
190
|
self._handle_task_delegation(tool_input, pre_tool_data, session_id)
|
|
191
191
|
|
|
192
|
+
# Record tool call for auto-pause if active
|
|
193
|
+
auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
|
|
194
|
+
if auto_pause and auto_pause.is_pause_active():
|
|
195
|
+
try:
|
|
196
|
+
auto_pause.on_tool_call(tool_name, tool_input)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
if DEBUG:
|
|
199
|
+
print(f"Auto-pause tool recording error: {e}", file=sys.stderr)
|
|
200
|
+
|
|
192
201
|
self.hook_handler._emit_socketio_event("", "pre_tool", pre_tool_data)
|
|
193
202
|
|
|
203
|
+
# Handle TodoWrite specially - emit dedicated todo_updated event
|
|
204
|
+
# WHY: Frontend expects todo_updated events for dashboard display
|
|
205
|
+
# The broadcaster.todo_updated() method exists but was never called
|
|
206
|
+
if tool_name == "TodoWrite" and tool_params.get("todos"):
|
|
207
|
+
todo_data = {
|
|
208
|
+
"todos": tool_params["todos"],
|
|
209
|
+
"total_count": len(tool_params["todos"]),
|
|
210
|
+
"session_id": session_id,
|
|
211
|
+
"timestamp": timestamp,
|
|
212
|
+
}
|
|
213
|
+
self.hook_handler._emit_socketio_event("", "todo_updated", todo_data)
|
|
214
|
+
if DEBUG:
|
|
215
|
+
print(
|
|
216
|
+
f" - Emitted todo_updated event with {len(tool_params['todos'])} todos for session {session_id[:8]}...",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
|
|
194
220
|
def _handle_task_delegation(
|
|
195
221
|
self, tool_input: dict, pre_tool_data: dict, session_id: str
|
|
196
222
|
):
|
|
@@ -271,7 +297,7 @@ class EventHandlers:
|
|
|
271
297
|
mhm = getattr(self.hook_handler, "memory_hook_manager", None)
|
|
272
298
|
if mhm and hasattr(mhm, "trigger_pre_delegation_hook"):
|
|
273
299
|
mhm.trigger_pre_delegation_hook(agent_type, tool_input, session_id)
|
|
274
|
-
except Exception:
|
|
300
|
+
except Exception: # nosec B110
|
|
275
301
|
# Memory hooks are optional
|
|
276
302
|
pass
|
|
277
303
|
|
|
@@ -364,7 +390,7 @@ class EventHandlers:
|
|
|
364
390
|
os.chdir(working_dir)
|
|
365
391
|
|
|
366
392
|
# Run git command to get current branch
|
|
367
|
-
result = subprocess.run(
|
|
393
|
+
result = subprocess.run( # nosec B603 B607
|
|
368
394
|
["git", "branch", "--show-current"],
|
|
369
395
|
capture_output=True,
|
|
370
396
|
text=True,
|
|
@@ -474,7 +500,7 @@ class EventHandlers:
|
|
|
474
500
|
mhm = getattr(self.hook_handler, "memory_hook_manager", None)
|
|
475
501
|
if mhm and hasattr(mhm, "trigger_post_delegation_hook"):
|
|
476
502
|
mhm.trigger_post_delegation_hook(agent_type, event, session_id)
|
|
477
|
-
except Exception:
|
|
503
|
+
except Exception: # nosec B110
|
|
478
504
|
# Memory hooks are optional
|
|
479
505
|
pass
|
|
480
506
|
|
|
@@ -488,7 +514,7 @@ class EventHandlers:
|
|
|
488
514
|
rtm.track_agent_response(
|
|
489
515
|
session_id, agent_type, event, delegation_requests
|
|
490
516
|
)
|
|
491
|
-
except Exception:
|
|
517
|
+
except Exception: # nosec B110
|
|
492
518
|
# Response tracking is optional
|
|
493
519
|
pass
|
|
494
520
|
|
|
@@ -550,13 +576,49 @@ class EventHandlers:
|
|
|
550
576
|
if DEBUG:
|
|
551
577
|
self._log_stop_event_debug(event, session_id, metadata)
|
|
552
578
|
|
|
579
|
+
# Auto-pause integration (independent of response tracking)
|
|
580
|
+
# WHY HERE: Auto-pause must work even when response_tracking is disabled
|
|
581
|
+
# Extract usage data directly from event and trigger auto-pause if thresholds crossed
|
|
582
|
+
if "usage" in event:
|
|
583
|
+
auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
|
|
584
|
+
if auto_pause:
|
|
585
|
+
try:
|
|
586
|
+
usage_data = event["usage"]
|
|
587
|
+
metadata["usage"] = {
|
|
588
|
+
"input_tokens": usage_data.get("input_tokens", 0),
|
|
589
|
+
"output_tokens": usage_data.get("output_tokens", 0),
|
|
590
|
+
"cache_creation_input_tokens": usage_data.get(
|
|
591
|
+
"cache_creation_input_tokens", 0
|
|
592
|
+
),
|
|
593
|
+
"cache_read_input_tokens": usage_data.get(
|
|
594
|
+
"cache_read_input_tokens", 0
|
|
595
|
+
),
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
|
|
599
|
+
if threshold_crossed:
|
|
600
|
+
warning = auto_pause.emit_threshold_warning(threshold_crossed)
|
|
601
|
+
print(f"\n⚠️ {warning}", file=sys.stderr)
|
|
602
|
+
|
|
603
|
+
if DEBUG:
|
|
604
|
+
print(
|
|
605
|
+
f" - Auto-pause threshold crossed: {threshold_crossed}",
|
|
606
|
+
file=sys.stderr,
|
|
607
|
+
)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
if DEBUG:
|
|
610
|
+
print(
|
|
611
|
+
f"Auto-pause error in handle_stop_fast: {e}",
|
|
612
|
+
file=sys.stderr,
|
|
613
|
+
)
|
|
614
|
+
|
|
553
615
|
# Track response if enabled
|
|
554
616
|
try:
|
|
555
617
|
rtm = getattr(self.hook_handler, "response_tracking_manager", None)
|
|
556
618
|
if rtm and hasattr(rtm, "track_stop_response"):
|
|
557
619
|
pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
|
|
558
620
|
rtm.track_stop_response(event, session_id, metadata, pending_prompts)
|
|
559
|
-
except Exception:
|
|
621
|
+
except Exception: # nosec B110
|
|
560
622
|
# Response tracking is optional
|
|
561
623
|
pass
|
|
562
624
|
|
|
@@ -598,7 +660,7 @@ class EventHandlers:
|
|
|
598
660
|
f" - response_tracker exists: {tracker_exists}",
|
|
599
661
|
file=sys.stderr,
|
|
600
662
|
)
|
|
601
|
-
except Exception:
|
|
663
|
+
except Exception: # nosec B110
|
|
602
664
|
# If debug logging fails, just skip it
|
|
603
665
|
pass
|
|
604
666
|
|
|
@@ -664,7 +726,7 @@ class EventHandlers:
|
|
|
664
726
|
try:
|
|
665
727
|
# Get the original request data (with fuzzy matching fallback)
|
|
666
728
|
delegation_requests = getattr(self.hook_handler, "delegation_requests", {})
|
|
667
|
-
request_info = delegation_requests.get(session_id)
|
|
729
|
+
request_info = delegation_requests.get(session_id) # nosec B113
|
|
668
730
|
|
|
669
731
|
# If exact match fails, try partial matching
|
|
670
732
|
if not request_info and session_id:
|
|
@@ -689,7 +751,7 @@ class EventHandlers:
|
|
|
689
751
|
f" - ✅ Fuzzy match found: {stored_sid[:16]}...",
|
|
690
752
|
file=sys.stderr,
|
|
691
753
|
)
|
|
692
|
-
request_info = delegation_requests.get(stored_sid)
|
|
754
|
+
request_info = delegation_requests.get(stored_sid) # nosec B113
|
|
693
755
|
# Update the key to use the current session_id for consistency
|
|
694
756
|
if request_info:
|
|
695
757
|
delegation_requests[session_id] = request_info
|
|
@@ -789,6 +851,7 @@ class EventHandlers:
|
|
|
789
851
|
- Captures response content and metadata for analysis
|
|
790
852
|
- Enables tracking of conversation flow and response patterns
|
|
791
853
|
- Essential for comprehensive monitoring of Claude interactions
|
|
854
|
+
- Scans for delegation anti-patterns and creates autotodos
|
|
792
855
|
"""
|
|
793
856
|
# Track the response for logging
|
|
794
857
|
try:
|
|
@@ -796,10 +859,17 @@ class EventHandlers:
|
|
|
796
859
|
if rtm and hasattr(rtm, "track_assistant_response"):
|
|
797
860
|
pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
|
|
798
861
|
rtm.track_assistant_response(event, pending_prompts)
|
|
799
|
-
except Exception:
|
|
862
|
+
except Exception: # nosec B110
|
|
800
863
|
# Response tracking is optional
|
|
801
864
|
pass
|
|
802
865
|
|
|
866
|
+
# Scan response for delegation anti-patterns and create autotodos
|
|
867
|
+
try:
|
|
868
|
+
self._scan_for_delegation_patterns(event)
|
|
869
|
+
except Exception as e: # nosec B110
|
|
870
|
+
if DEBUG:
|
|
871
|
+
print(f"Delegation scanning error: {e}", file=sys.stderr)
|
|
872
|
+
|
|
803
873
|
# Get working directory and git branch
|
|
804
874
|
working_dir = event.get("cwd", "")
|
|
805
875
|
git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
|
|
@@ -852,6 +922,21 @@ class EventHandlers:
|
|
|
852
922
|
file=sys.stderr,
|
|
853
923
|
)
|
|
854
924
|
|
|
925
|
+
# Record assistant response for auto-pause if active
|
|
926
|
+
auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
|
|
927
|
+
if auto_pause and auto_pause.is_pause_active():
|
|
928
|
+
try:
|
|
929
|
+
# Summarize response to first 200 chars
|
|
930
|
+
summary = (
|
|
931
|
+
response_text[:200] + "..."
|
|
932
|
+
if len(response_text) > 200
|
|
933
|
+
else response_text
|
|
934
|
+
)
|
|
935
|
+
auto_pause.on_assistant_response(summary)
|
|
936
|
+
except Exception as e:
|
|
937
|
+
if DEBUG:
|
|
938
|
+
print(f"Auto-pause response recording error: {e}", file=sys.stderr)
|
|
939
|
+
|
|
855
940
|
# Emit normalized event
|
|
856
941
|
self.hook_handler._emit_socketio_event(
|
|
857
942
|
"", "assistant_response", assistant_response_data
|
|
@@ -886,3 +971,123 @@ class EventHandlers:
|
|
|
886
971
|
|
|
887
972
|
# Emit normalized event
|
|
888
973
|
self.hook_handler._emit_socketio_event("", "session_start", session_start_data)
|
|
974
|
+
|
|
975
|
+
def handle_subagent_start_fast(self, event):
|
|
976
|
+
"""Handle SubagentStart events with proper agent type extraction.
|
|
977
|
+
|
|
978
|
+
WHY separate from SessionStart:
|
|
979
|
+
- SubagentStart contains agent-specific information
|
|
980
|
+
- Frontend needs agent_type to create distinct agent nodes
|
|
981
|
+
- Multiple engineers should show as separate nodes in hierarchy
|
|
982
|
+
- Research agents must appear in the agent hierarchy
|
|
983
|
+
|
|
984
|
+
Unlike SessionStart, SubagentStart events contain agent-specific
|
|
985
|
+
information that must be preserved and emitted to the dashboard.
|
|
986
|
+
"""
|
|
987
|
+
session_id = event.get("session_id", "")
|
|
988
|
+
|
|
989
|
+
# Extract agent type from event - Claude provides this in SubagentStart
|
|
990
|
+
# Try multiple possible field names for compatibility
|
|
991
|
+
agent_type = event.get("agent_type") or event.get("subagent_type") or "unknown"
|
|
992
|
+
|
|
993
|
+
# Generate unique agent ID combining type and session
|
|
994
|
+
agent_id = event.get("agent_id", f"{agent_type}_{session_id[:8]}")
|
|
995
|
+
|
|
996
|
+
# Get working directory and git branch
|
|
997
|
+
working_dir = event.get("cwd", "")
|
|
998
|
+
git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
|
|
999
|
+
|
|
1000
|
+
# Build subagent start data with all required fields
|
|
1001
|
+
subagent_start_data = {
|
|
1002
|
+
"session_id": session_id,
|
|
1003
|
+
"agent_type": agent_type,
|
|
1004
|
+
"agent_id": agent_id,
|
|
1005
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1006
|
+
"hook_event_name": "SubagentStart", # Preserve correct hook name
|
|
1007
|
+
"working_directory": working_dir,
|
|
1008
|
+
"git_branch": git_branch,
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
# Debug logging
|
|
1012
|
+
if DEBUG:
|
|
1013
|
+
print(
|
|
1014
|
+
f"Hook handler: SubagentStart - agent_type='{agent_type}', "
|
|
1015
|
+
f"agent_id='{agent_id}', session_id='{session_id[:16]}...'",
|
|
1016
|
+
file=sys.stderr,
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
# Emit to /hook namespace as subagent_start (NOT session_start!)
|
|
1020
|
+
self.hook_handler._emit_socketio_event(
|
|
1021
|
+
"", "subagent_start", subagent_start_data
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
def _scan_for_delegation_patterns(self, event):
|
|
1025
|
+
"""Scan assistant response for delegation anti-patterns.
|
|
1026
|
+
|
|
1027
|
+
WHY this is needed:
|
|
1028
|
+
- Detect when PM asks user to do something manually instead of delegating
|
|
1029
|
+
- Flag PM behavior violations for immediate correction
|
|
1030
|
+
- Enforce delegation principle in PM workflow
|
|
1031
|
+
- Help PM recognize delegation opportunities
|
|
1032
|
+
|
|
1033
|
+
This method scans the assistant's response text for patterns like:
|
|
1034
|
+
- "Make sure .env.local is in your .gitignore"
|
|
1035
|
+
- "You'll need to run npm install"
|
|
1036
|
+
- "Please run the tests manually"
|
|
1037
|
+
|
|
1038
|
+
When patterns are detected, PM violations are logged as errors/warnings
|
|
1039
|
+
that should be corrected immediately, NOT as todos to delegate.
|
|
1040
|
+
|
|
1041
|
+
DESIGN DECISION: pm.violation vs autotodo.delegation
|
|
1042
|
+
- Delegation patterns = PM doing something WRONG → pm.violation (error)
|
|
1043
|
+
- Script failures = Something BROKEN → autotodo.error (todo)
|
|
1044
|
+
"""
|
|
1045
|
+
# Only scan if delegation detector is available
|
|
1046
|
+
try:
|
|
1047
|
+
from claude_mpm.services.delegation_detector import get_delegation_detector
|
|
1048
|
+
from claude_mpm.services.event_log import get_event_log
|
|
1049
|
+
except ImportError:
|
|
1050
|
+
if DEBUG:
|
|
1051
|
+
print("Delegation detector or event log not available", file=sys.stderr)
|
|
1052
|
+
return
|
|
1053
|
+
|
|
1054
|
+
response_text = event.get("response", "")
|
|
1055
|
+
if not response_text:
|
|
1056
|
+
return
|
|
1057
|
+
|
|
1058
|
+
# Get the delegation detector
|
|
1059
|
+
detector = get_delegation_detector()
|
|
1060
|
+
|
|
1061
|
+
# Detect delegation patterns
|
|
1062
|
+
detections = detector.detect_user_delegation(response_text)
|
|
1063
|
+
|
|
1064
|
+
if not detections:
|
|
1065
|
+
return # No patterns detected
|
|
1066
|
+
|
|
1067
|
+
# Get event log for violation recording
|
|
1068
|
+
event_log = get_event_log()
|
|
1069
|
+
|
|
1070
|
+
# Create PM violation events (NOT autotodos)
|
|
1071
|
+
for detection in detections:
|
|
1072
|
+
# Create event log entry as pm.violation
|
|
1073
|
+
event_log.append_event(
|
|
1074
|
+
event_type="pm.violation",
|
|
1075
|
+
payload={
|
|
1076
|
+
"violation_type": "delegation_anti_pattern",
|
|
1077
|
+
"pattern_type": detection["pattern_type"],
|
|
1078
|
+
"original_text": detection["original_text"],
|
|
1079
|
+
"suggested_action": detection["suggested_todo"],
|
|
1080
|
+
"action": detection["action"],
|
|
1081
|
+
"session_id": event.get("session_id", ""),
|
|
1082
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1083
|
+
"severity": "warning", # Not critical, but should be fixed
|
|
1084
|
+
"message": f"PM asked user to do something manually: {detection['original_text'][:80]}...",
|
|
1085
|
+
},
|
|
1086
|
+
status="pending",
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
if DEBUG:
|
|
1090
|
+
print(
|
|
1091
|
+
f"⚠️ PM violation detected: {detection['original_text'][:60]}...",
|
|
1092
|
+
file=sys.stderr,
|
|
1093
|
+
)
|
|
@@ -31,6 +31,7 @@ from typing import Optional, Tuple
|
|
|
31
31
|
# Import extracted modules with fallback for direct execution
|
|
32
32
|
try:
|
|
33
33
|
# Try relative imports first (when imported as module)
|
|
34
|
+
from .auto_pause_handler import AutoPauseHandler
|
|
34
35
|
from .event_handlers import EventHandlers
|
|
35
36
|
from .memory_integration import MemoryHookManager
|
|
36
37
|
from .response_tracking import ResponseTrackingManager
|
|
@@ -47,6 +48,7 @@ except ImportError:
|
|
|
47
48
|
# Add parent directory to path
|
|
48
49
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
49
50
|
|
|
51
|
+
from auto_pause_handler import AutoPauseHandler
|
|
50
52
|
from event_handlers import EventHandlers
|
|
51
53
|
from memory_integration import MemoryHookManager
|
|
52
54
|
from response_tracking import ResponseTrackingManager
|
|
@@ -153,7 +155,7 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
|
|
|
153
155
|
"""
|
|
154
156
|
try:
|
|
155
157
|
# Try to detect Claude Code version
|
|
156
|
-
result = subprocess.run(
|
|
158
|
+
result = subprocess.run( # nosec B603 - Safe: hardcoded claude CLI with --version flag, no user input
|
|
157
159
|
["claude", "--version"],
|
|
158
160
|
capture_output=True,
|
|
159
161
|
text=True,
|
|
@@ -230,6 +232,19 @@ class ClaudeHookHandler:
|
|
|
230
232
|
self.state_manager, self.response_tracking_manager, self.connection_manager
|
|
231
233
|
)
|
|
232
234
|
|
|
235
|
+
# Initialize auto-pause handler
|
|
236
|
+
try:
|
|
237
|
+
self.auto_pause_handler = AutoPauseHandler()
|
|
238
|
+
# Pass reference to ResponseTrackingManager so it can call auto_pause
|
|
239
|
+
if hasattr(self, "response_tracking_manager"):
|
|
240
|
+
self.response_tracking_manager.auto_pause_handler = (
|
|
241
|
+
self.auto_pause_handler
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
self.auto_pause_handler = None
|
|
245
|
+
if DEBUG:
|
|
246
|
+
print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
|
|
247
|
+
|
|
233
248
|
# Backward compatibility properties for tests
|
|
234
249
|
self.connection_pool = self.connection_manager.connection_pool
|
|
235
250
|
|
|
@@ -419,7 +434,7 @@ class ClaudeHookHandler:
|
|
|
419
434
|
"Notification": self.event_handlers.handle_notification_fast,
|
|
420
435
|
"Stop": self.event_handlers.handle_stop_fast,
|
|
421
436
|
"SubagentStop": self.event_handlers.handle_subagent_stop_fast,
|
|
422
|
-
"SubagentStart": self.event_handlers.
|
|
437
|
+
"SubagentStart": self.event_handlers.handle_subagent_start_fast,
|
|
423
438
|
"SessionStart": self.event_handlers.handle_session_start_fast,
|
|
424
439
|
"AssistantResponse": self.event_handlers.handle_assistant_response,
|
|
425
440
|
}
|
|
@@ -535,13 +550,15 @@ class ClaudeHookHandler:
|
|
|
535
550
|
# Build hook execution data
|
|
536
551
|
hook_data = {
|
|
537
552
|
"hook_name": hook_type,
|
|
538
|
-
"hook_type": hook_type,
|
|
553
|
+
"hook_type": hook_type, # Actual hook type (PreToolUse, UserPromptSubmit, etc.)
|
|
554
|
+
"hook_event_type": hook_type, # Additional field for clarity
|
|
539
555
|
"session_id": session_id,
|
|
540
556
|
"working_directory": working_dir,
|
|
541
557
|
"success": success,
|
|
542
558
|
"duration_ms": duration_ms,
|
|
543
559
|
"result_summary": summary,
|
|
544
560
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
561
|
+
"source": "claude_hook_handler", # Explicit source identification
|
|
545
562
|
}
|
|
546
563
|
|
|
547
564
|
# Add error information if present
|
|
@@ -628,12 +645,19 @@ class ClaudeHookHandler:
|
|
|
628
645
|
|
|
629
646
|
def __del__(self):
|
|
630
647
|
"""Cleanup on handler destruction."""
|
|
648
|
+
# Finalize any active auto-pause session
|
|
649
|
+
if hasattr(self, "auto_pause_handler") and self.auto_pause_handler:
|
|
650
|
+
try:
|
|
651
|
+
self.auto_pause_handler.on_session_end()
|
|
652
|
+
except Exception:
|
|
653
|
+
pass # nosec B110 - Intentionally ignore cleanup errors during handler destruction
|
|
654
|
+
|
|
631
655
|
# Clean up connection manager if it exists
|
|
632
656
|
if hasattr(self, "connection_manager") and self.connection_manager:
|
|
633
657
|
try:
|
|
634
658
|
self.connection_manager.cleanup()
|
|
635
659
|
except Exception:
|
|
636
|
-
pass #
|
|
660
|
+
pass # nosec B110 - Intentionally ignore cleanup errors during handler destruction
|
|
637
661
|
|
|
638
662
|
|
|
639
663
|
def main():
|
|
@@ -130,7 +130,7 @@ class ResponseTrackingManager:
|
|
|
130
130
|
|
|
131
131
|
try:
|
|
132
132
|
# Get the original request data stored during pre-tool
|
|
133
|
-
request_info = delegation_requests.get(session_id)
|
|
133
|
+
request_info = delegation_requests.get(session_id) # nosec B113 - False positive: dict.get(), not requests library
|
|
134
134
|
if not request_info:
|
|
135
135
|
if DEBUG:
|
|
136
136
|
print(
|
|
@@ -306,6 +306,8 @@ class ResponseTrackingManager:
|
|
|
306
306
|
)
|
|
307
307
|
|
|
308
308
|
# Capture Claude API usage data if available
|
|
309
|
+
# NOTE: Usage data is already captured in metadata by handle_stop_fast()
|
|
310
|
+
# which also handles auto-pause triggering (even when response tracking disabled)
|
|
309
311
|
if "usage" in event:
|
|
310
312
|
usage_data = event["usage"]
|
|
311
313
|
metadata["usage"] = {
|
|
Binary file
|
|
Binary file
|
|
@@ -134,6 +134,26 @@ class ConnectionManagerService:
|
|
|
134
134
|
# Otherwise use "hook" as the type
|
|
135
135
|
if event == "hook_execution":
|
|
136
136
|
hook_type = data.get("hook_type", "unknown")
|
|
137
|
+
|
|
138
|
+
# BUGFIX: Validate hook_type is meaningful (not generic/invalid values)
|
|
139
|
+
# Problem: Dashboard shows "hook hook" instead of "PreToolUse", "UserPromptSubmit", etc.
|
|
140
|
+
# Root cause: hook_type defaults to "hook" or "unknown", providing no useful information
|
|
141
|
+
# Solution: Fallback to hook_name, then to descriptive "hook_execution_untyped"
|
|
142
|
+
if hook_type in ("hook", "unknown", "", None):
|
|
143
|
+
# Try fallback to hook_name field (set by _emit_hook_execution_event)
|
|
144
|
+
hook_type = data.get("hook_name", "unknown_hook")
|
|
145
|
+
|
|
146
|
+
# Final fallback if still generic - use descriptive name
|
|
147
|
+
if hook_type in ("hook", "unknown", "", None):
|
|
148
|
+
hook_type = "hook_execution_untyped"
|
|
149
|
+
|
|
150
|
+
# Debug log when we detect invalid hook_type for troubleshooting
|
|
151
|
+
if DEBUG:
|
|
152
|
+
print(
|
|
153
|
+
f"⚠️ Invalid hook_type detected, using fallback: {hook_type}",
|
|
154
|
+
file=sys.stderr,
|
|
155
|
+
)
|
|
156
|
+
|
|
137
157
|
event_type = hook_type
|
|
138
158
|
else:
|
|
139
159
|
event_type = "hook"
|
|
@@ -67,7 +67,9 @@ class SubagentResponseProcessor:
|
|
|
67
67
|
print(" - No stored sessions in delegation_requests!", file=sys.stderr)
|
|
68
68
|
|
|
69
69
|
# Get agent type and other basic info
|
|
70
|
-
agent_type, agent_id, reason = self._extract_basic_info(
|
|
70
|
+
agent_type, agent_id, reason, agent_type_inferred = self._extract_basic_info(
|
|
71
|
+
event, session_id
|
|
72
|
+
)
|
|
71
73
|
|
|
72
74
|
# Always log SubagentStop events for debugging
|
|
73
75
|
if DEBUG or agent_type != "unknown":
|
|
@@ -108,6 +110,7 @@ class SubagentResponseProcessor:
|
|
|
108
110
|
working_dir,
|
|
109
111
|
git_branch,
|
|
110
112
|
structured_response,
|
|
113
|
+
agent_type_inferred,
|
|
111
114
|
)
|
|
112
115
|
|
|
113
116
|
# Debug log the processed data
|
|
@@ -117,11 +120,20 @@ class SubagentResponseProcessor:
|
|
|
117
120
|
file=sys.stderr,
|
|
118
121
|
)
|
|
119
122
|
|
|
120
|
-
# Emit to
|
|
121
|
-
self.connection_manager.emit_event("
|
|
123
|
+
# Emit to default namespace (consistent with subagent_start)
|
|
124
|
+
self.connection_manager.emit_event("", "subagent_stop", subagent_stop_data)
|
|
125
|
+
|
|
126
|
+
def _extract_basic_info(
|
|
127
|
+
self, event: dict, session_id: str
|
|
128
|
+
) -> Tuple[str, str, str, bool]:
|
|
129
|
+
"""Extract basic info from the event.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Tuple of (agent_type, agent_id, reason, agent_type_inferred)
|
|
133
|
+
- agent_type_inferred is True when defaulted to "pm"
|
|
134
|
+
"""
|
|
135
|
+
agent_type_inferred = False
|
|
122
136
|
|
|
123
|
-
def _extract_basic_info(self, event: dict, session_id: str) -> Tuple[str, str, str]:
|
|
124
|
-
"""Extract basic info from the event."""
|
|
125
137
|
# First try to get agent type from our tracking
|
|
126
138
|
agent_type = (
|
|
127
139
|
self.state_manager.get_delegation_agent_type(session_id)
|
|
@@ -146,7 +158,17 @@ class SubagentResponseProcessor:
|
|
|
146
158
|
elif "pm" in task_desc or "project" in task_desc:
|
|
147
159
|
agent_type = "pm"
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
# Default to "pm" if still unknown (main conversation doesn't use Task tool)
|
|
162
|
+
if agent_type == "unknown":
|
|
163
|
+
agent_type = "pm"
|
|
164
|
+
agent_type_inferred = True
|
|
165
|
+
if DEBUG:
|
|
166
|
+
print(
|
|
167
|
+
" - Inferred agent_type='pm' (no explicit type found)",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return agent_type, agent_id, reason, agent_type_inferred
|
|
150
172
|
|
|
151
173
|
def _extract_structured_response(
|
|
152
174
|
self, output: str, agent_type: str
|
|
@@ -338,10 +360,12 @@ class SubagentResponseProcessor:
|
|
|
338
360
|
working_dir: str,
|
|
339
361
|
git_branch: str,
|
|
340
362
|
structured_response: Optional[dict],
|
|
363
|
+
agent_type_inferred: bool,
|
|
341
364
|
) -> dict:
|
|
342
365
|
"""Build the subagent stop data for event emission."""
|
|
343
366
|
subagent_stop_data = {
|
|
344
367
|
"agent_type": agent_type,
|
|
368
|
+
"agent_type_inferred": agent_type_inferred,
|
|
345
369
|
"agent_id": agent_id,
|
|
346
370
|
"reason": reason,
|
|
347
371
|
"session_id": session_id,
|
|
@@ -8,8 +8,11 @@ DESIGN DECISIONS:
|
|
|
8
8
|
- Non-blocking: doesn't prevent PM from starting if check fails
|
|
9
9
|
- Displays context to stdout for user visibility
|
|
10
10
|
- Integrates with existing session pause/resume infrastructure
|
|
11
|
+
- Checks for ACTIVE-PAUSE.jsonl (incremental auto-pause) before regular paused sessions
|
|
11
12
|
"""
|
|
12
13
|
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
from typing import Any, Dict, Optional
|
|
15
18
|
|
|
@@ -31,10 +34,83 @@ class SessionResumeStartupHook:
|
|
|
31
34
|
self.project_path = project_path or Path.cwd()
|
|
32
35
|
self.resume_helper = SessionResumeHelper(self.project_path)
|
|
33
36
|
self._session_displayed = False
|
|
37
|
+
self.sessions_dir = self.project_path / ".claude-mpm" / "sessions"
|
|
38
|
+
|
|
39
|
+
def check_for_active_pause(self) -> Optional[Dict[str, Any]]:
|
|
40
|
+
"""Check for an active incremental pause session.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Pause session metadata if ACTIVE-PAUSE.jsonl exists, None otherwise
|
|
44
|
+
"""
|
|
45
|
+
active_pause_path = self.sessions_dir / "ACTIVE-PAUSE.jsonl"
|
|
46
|
+
|
|
47
|
+
if not active_pause_path.exists():
|
|
48
|
+
logger.debug("No ACTIVE-PAUSE.jsonl found")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Read JSONL file to get first and last actions
|
|
53
|
+
with active_pause_path.open("r") as f:
|
|
54
|
+
lines = f.readlines()
|
|
55
|
+
|
|
56
|
+
if not lines:
|
|
57
|
+
logger.warning("ACTIVE-PAUSE.jsonl is empty")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Parse first action (session start)
|
|
61
|
+
first_action = json.loads(lines[0])
|
|
62
|
+
|
|
63
|
+
# Parse last action (most recent)
|
|
64
|
+
last_action = json.loads(lines[-1]) if len(lines) > 1 else first_action
|
|
65
|
+
|
|
66
|
+
# Extract metadata
|
|
67
|
+
return {
|
|
68
|
+
"is_incremental": True,
|
|
69
|
+
"session_id": first_action.get("session_id"),
|
|
70
|
+
"started_at": first_action.get("timestamp"),
|
|
71
|
+
"context_at_start": first_action.get("data", {}).get(
|
|
72
|
+
"context_percentage", 0
|
|
73
|
+
),
|
|
74
|
+
"current_context": last_action.get("context_percentage", 0),
|
|
75
|
+
"action_count": len(lines),
|
|
76
|
+
"file_path": str(active_pause_path),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
except (json.JSONDecodeError, OSError, KeyError) as e:
|
|
80
|
+
logger.error(f"Failed to parse ACTIVE-PAUSE.jsonl: {e}", exc_info=True)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def display_active_pause_warning(self, pause_info: Dict[str, Any]) -> None:
|
|
84
|
+
"""Display warning about active incremental pause session.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
pause_info: Pause session metadata from check_for_active_pause()
|
|
88
|
+
"""
|
|
89
|
+
print("\n" + "=" * 60, file=sys.stderr)
|
|
90
|
+
print("⚠️ ACTIVE AUTO-PAUSE SESSION DETECTED", file=sys.stderr)
|
|
91
|
+
print("=" * 60, file=sys.stderr)
|
|
92
|
+
print(f"Session ID: {pause_info['session_id']}", file=sys.stderr)
|
|
93
|
+
print(f"Started at: {pause_info['started_at']}", file=sys.stderr)
|
|
94
|
+
print(
|
|
95
|
+
f"Context at pause: {pause_info['context_at_start']:.1%}", file=sys.stderr
|
|
96
|
+
)
|
|
97
|
+
print(f"Actions recorded: {pause_info['action_count']}", file=sys.stderr)
|
|
98
|
+
print(
|
|
99
|
+
"\nThis session was auto-paused due to high context usage.", file=sys.stderr
|
|
100
|
+
)
|
|
101
|
+
print("Options:", file=sys.stderr)
|
|
102
|
+
print(" 1. Continue (actions will be appended)", file=sys.stderr)
|
|
103
|
+
print(" 2. Use /mpm-init pause --finalize to create snapshot", file=sys.stderr)
|
|
104
|
+
print(" 3. Use /mpm-init pause --discard to abandon", file=sys.stderr)
|
|
105
|
+
print("=" * 60 + "\n", file=sys.stderr)
|
|
34
106
|
|
|
35
107
|
def on_pm_startup(self) -> Optional[Dict[str, Any]]:
|
|
36
108
|
"""Execute on PM startup to check for paused sessions.
|
|
37
109
|
|
|
110
|
+
Checks in priority order:
|
|
111
|
+
1. ACTIVE-PAUSE.jsonl (incremental auto-pause)
|
|
112
|
+
2. Regular paused sessions (session-*.json)
|
|
113
|
+
|
|
38
114
|
Returns:
|
|
39
115
|
Session data if paused session found, None otherwise
|
|
40
116
|
"""
|
|
@@ -44,7 +120,15 @@ class SessionResumeStartupHook:
|
|
|
44
120
|
logger.debug("Session already displayed, skipping")
|
|
45
121
|
return None
|
|
46
122
|
|
|
47
|
-
# Check for
|
|
123
|
+
# PRIORITY 1: Check for active incremental pause FIRST
|
|
124
|
+
active_pause_info = self.check_for_active_pause()
|
|
125
|
+
if active_pause_info:
|
|
126
|
+
self.display_active_pause_warning(active_pause_info)
|
|
127
|
+
self._session_displayed = True
|
|
128
|
+
logger.info("Active pause session detected and displayed")
|
|
129
|
+
return active_pause_info
|
|
130
|
+
|
|
131
|
+
# PRIORITY 2: Fall back to regular paused sessions
|
|
48
132
|
session_data = self.resume_helper.check_and_display_resume_prompt()
|
|
49
133
|
|
|
50
134
|
if session_data:
|
claude_mpm/init.py
CHANGED