claude-mpm 5.4.65__py3-none-any.whl → 5.6.10__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_OUTPUT_STYLE.md +66 -241
- claude_mpm/agents/CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md +413 -0
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +107 -1928
- claude_mpm/agents/PM_INSTRUCTIONS.md +119 -689
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- 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 +566 -0
- claude_mpm/cli/commands/commander.py +46 -0
- claude_mpm/cli/commands/configure.py +620 -21
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init/core.py +2 -2
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +171 -17
- claude_mpm/cli/executor.py +120 -16
- 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 +76 -1
- claude_mpm/cli/parsers/commander_parser.py +83 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +203 -359
- claude_mpm/cli/startup_display.py +72 -5
- claude_mpm/cli/startup_logging.py +2 -2
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +72 -0
- claude_mpm/commander/adapters/__init__.py +31 -0
- claude_mpm/commander/adapters/base.py +191 -0
- claude_mpm/commander/adapters/claude_code.py +361 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +105 -0
- claude_mpm/commander/api/errors.py +133 -0
- claude_mpm/commander/api/routes/__init__.py +8 -0
- claude_mpm/commander/api/routes/events.py +184 -0
- claude_mpm/commander/api/routes/inbox.py +171 -0
- claude_mpm/commander/api/routes/messages.py +148 -0
- claude_mpm/commander/api/routes/projects.py +271 -0
- claude_mpm/commander/api/routes/sessions.py +228 -0
- claude_mpm/commander/api/routes/work.py +260 -0
- claude_mpm/commander/api/schemas.py +182 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +107 -0
- claude_mpm/commander/chat/commands.py +96 -0
- claude_mpm/commander/chat/repl.py +310 -0
- claude_mpm/commander/config.py +49 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/daemon.py +398 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +332 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +143 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +62 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +337 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +121 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +309 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +404 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +316 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +361 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +189 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +219 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +9 -1
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -2
- claude_mpm/core/config.py +32 -19
- claude_mpm/core/hook_manager.py +51 -3
- claude_mpm/core/interactive_session.py +7 -7
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +35 -11
- claude_mpm/core/output_style_manager.py +31 -13
- claude_mpm/core/unified_config.py +54 -8
- claude_mpm/core/unified_paths.py +95 -90
- 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/experimental/cli_enhancements.py +2 -1
- 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__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +485 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +283 -87
- claude_mpm/hooks/claude_hooks/hook_handler.py +106 -89
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +116 -8
- claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
- claude_mpm/hooks/claude_hooks/response_tracking.py +42 -59
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +39 -24
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
- claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +73 -75
- claude_mpm/hooks/session_resume_hook.py +89 -1
- claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
- claude_mpm/init.py +1 -1
- claude_mpm/scripts/claude-hook-handler.sh +43 -16
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- 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/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- 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 +325 -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 +259 -87
- claude_mpm/services/skills/git_skill_source_manager.py +135 -11
- claude_mpm/services/skills/selective_skill_deployer.py +142 -26
- claude_mpm/services/skills/skill_discovery_service.py +74 -4
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/services/socketio/handlers/hook.py +14 -7
- claude_mpm/services/socketio/server/main.py +12 -4
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- 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-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- claude_mpm/skills/skill_manager.py +4 -4
- claude_mpm/utils/agent_dependency_loader.py +4 -2
- claude_mpm/utils/robust_installer.py +10 -6
- claude_mpm-5.6.10.dist-info/METADATA +391 -0
- {claude_mpm-5.4.65.dist-info → claude_mpm-5.6.10.dist-info}/RECORD +303 -181
- 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.65.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.65.dist-info → claude_mpm-5.6.10.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.65.dist-info → claude_mpm-5.6.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.65.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.65.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.65.dist-info → claude_mpm-5.6.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""State persistence for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module handles atomic persistence and recovery of project registry
|
|
4
|
+
and session states to disk.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import tempfile
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
from ..models import Project, ProjectState, ToolSession
|
|
16
|
+
from ..registry import ProjectRegistry
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StateStore:
|
|
22
|
+
"""Persists and recovers project registry state.
|
|
23
|
+
|
|
24
|
+
Provides atomic writes to prevent corruption and handles graceful
|
|
25
|
+
recovery from missing or corrupted files.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
state_dir: Directory for state files
|
|
29
|
+
projects_path: Path to projects.json
|
|
30
|
+
sessions_path: Path to sessions.json
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> store = StateStore(Path("~/.claude-mpm/commander"))
|
|
34
|
+
>>> await store.save_projects(registry)
|
|
35
|
+
>>> projects = await store.load_projects()
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
VERSION = "1.0"
|
|
39
|
+
|
|
40
|
+
def __init__(self, state_dir: Path):
|
|
41
|
+
"""Initialize state store.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
state_dir: Directory for state files (created if needed)
|
|
45
|
+
"""
|
|
46
|
+
self.state_dir = state_dir.expanduser()
|
|
47
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
self.projects_path = self.state_dir / "projects.json"
|
|
50
|
+
self.sessions_path = self.state_dir / "sessions.json"
|
|
51
|
+
|
|
52
|
+
logger.info(f"Initialized StateStore at {self.state_dir}")
|
|
53
|
+
|
|
54
|
+
async def save_projects(self, registry: ProjectRegistry) -> None:
|
|
55
|
+
"""Save all projects to disk (atomic write).
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
registry: ProjectRegistry to persist
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
IOError: If write fails
|
|
62
|
+
"""
|
|
63
|
+
projects = registry.list_all()
|
|
64
|
+
|
|
65
|
+
data = {
|
|
66
|
+
"version": self.VERSION,
|
|
67
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
68
|
+
"projects": [self._serialize_project(p) for p in projects],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Run sync I/O in executor to avoid blocking
|
|
72
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
73
|
+
None, self._atomic_write, self.projects_path, data
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
logger.info(f"Saved {len(projects)} projects to {self.projects_path}")
|
|
77
|
+
|
|
78
|
+
async def load_projects(self) -> List[Project]:
|
|
79
|
+
"""Load projects from disk.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of Project instances (empty if file missing or corrupt)
|
|
83
|
+
"""
|
|
84
|
+
if not self.projects_path.exists():
|
|
85
|
+
logger.info("No projects file found, returning empty list")
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Run sync I/O in executor
|
|
90
|
+
data = await asyncio.get_event_loop().run_in_executor(
|
|
91
|
+
None, self._read_json, self.projects_path
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if data.get("version") != self.VERSION:
|
|
95
|
+
logger.warning(
|
|
96
|
+
f"Version mismatch: expected {self.VERSION}, "
|
|
97
|
+
f"got {data.get('version')}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
projects = [self._deserialize_project(p) for p in data.get("projects", [])]
|
|
101
|
+
|
|
102
|
+
logger.info(f"Loaded {len(projects)} projects from {self.projects_path}")
|
|
103
|
+
return projects
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to load projects: {e}", exc_info=True)
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
async def save_sessions(self, sessions: Dict[str, Any]) -> None:
|
|
110
|
+
"""Save session states (for recovery).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
sessions: Active ProjectSession instances by project_id
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
IOError: If write fails
|
|
117
|
+
"""
|
|
118
|
+
data = {
|
|
119
|
+
"version": self.VERSION,
|
|
120
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
121
|
+
"sessions": {
|
|
122
|
+
project_id: {
|
|
123
|
+
"state": session.state.value,
|
|
124
|
+
"pane_target": session.active_pane,
|
|
125
|
+
"paused_event_id": session.pause_reason,
|
|
126
|
+
}
|
|
127
|
+
for project_id, session in sessions.items()
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Run sync I/O in executor
|
|
132
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
133
|
+
None, self._atomic_write, self.sessions_path, data
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
logger.info(f"Saved {len(sessions)} sessions to {self.sessions_path}")
|
|
137
|
+
|
|
138
|
+
async def load_sessions(self) -> Dict[str, Dict[str, Any]]:
|
|
139
|
+
"""Load session states.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict mapping project_id to session state dict
|
|
143
|
+
(empty if file missing or corrupt)
|
|
144
|
+
"""
|
|
145
|
+
if not self.sessions_path.exists():
|
|
146
|
+
logger.info("No sessions file found, returning empty dict")
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Run sync I/O in executor
|
|
151
|
+
data = await asyncio.get_event_loop().run_in_executor(
|
|
152
|
+
None, self._read_json, self.sessions_path
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if data.get("version") != self.VERSION:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Version mismatch: expected {self.VERSION}, "
|
|
158
|
+
f"got {data.get('version')}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
sessions = data.get("sessions", {})
|
|
162
|
+
logger.info(f"Loaded {len(sessions)} sessions from {self.sessions_path}")
|
|
163
|
+
return sessions
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to load sessions: {e}", exc_info=True)
|
|
167
|
+
return {}
|
|
168
|
+
|
|
169
|
+
def _atomic_write(self, path: Path, data: Dict) -> None:
|
|
170
|
+
"""Write atomically (write to temp, then rename).
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
path: Target file path
|
|
174
|
+
data: Data to serialize as JSON
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
IOError: If write fails
|
|
178
|
+
"""
|
|
179
|
+
# Write to temporary file in same directory
|
|
180
|
+
# (ensures atomic rename works across filesystems)
|
|
181
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
182
|
+
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
with open(fd, "w") as f:
|
|
187
|
+
json.dump(data, f, indent=2)
|
|
188
|
+
|
|
189
|
+
# Atomic rename (POSIX guarantees atomicity)
|
|
190
|
+
Path(tmp_path).rename(path)
|
|
191
|
+
|
|
192
|
+
logger.debug(f"Atomically wrote to {path}")
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
# Clean up temp file on error
|
|
196
|
+
try:
|
|
197
|
+
Path(tmp_path).unlink()
|
|
198
|
+
except Exception: # nosec B110
|
|
199
|
+
pass # Ignore errors during cleanup
|
|
200
|
+
raise OSError(f"Failed to write {path}: {e}") from e
|
|
201
|
+
|
|
202
|
+
def _read_json(self, path: Path) -> Dict:
|
|
203
|
+
"""Read JSON file.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
path: File to read
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Parsed JSON data
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
IOError: If read fails
|
|
213
|
+
"""
|
|
214
|
+
with open(path) as f:
|
|
215
|
+
return json.load(f)
|
|
216
|
+
|
|
217
|
+
def _serialize_project(self, project: Project) -> Dict[str, Any]:
|
|
218
|
+
"""Serialize Project to JSON-compatible dict.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
project: Project instance
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
JSON-serializable dict
|
|
225
|
+
"""
|
|
226
|
+
return {
|
|
227
|
+
"id": project.id,
|
|
228
|
+
"path": project.path,
|
|
229
|
+
"name": project.name,
|
|
230
|
+
"state": project.state.value,
|
|
231
|
+
"state_reason": project.state_reason,
|
|
232
|
+
"config_loaded": project.config_loaded,
|
|
233
|
+
"config": project.config,
|
|
234
|
+
"sessions": {
|
|
235
|
+
sid: self._serialize_session(session)
|
|
236
|
+
for sid, session in project.sessions.items()
|
|
237
|
+
},
|
|
238
|
+
"created_at": project.created_at.isoformat(),
|
|
239
|
+
"last_activity": project.last_activity.isoformat(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def _serialize_session(self, session: ToolSession) -> Dict[str, Any]:
|
|
243
|
+
"""Serialize ToolSession to JSON-compatible dict.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
session: ToolSession instance
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
JSON-serializable dict
|
|
250
|
+
"""
|
|
251
|
+
return {
|
|
252
|
+
"id": session.id,
|
|
253
|
+
"project_id": session.project_id,
|
|
254
|
+
"runtime": session.runtime,
|
|
255
|
+
"tmux_target": session.tmux_target,
|
|
256
|
+
"status": session.status,
|
|
257
|
+
"created_at": session.created_at.isoformat(),
|
|
258
|
+
"last_output_at": (
|
|
259
|
+
session.last_output_at.isoformat() if session.last_output_at else None
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def _deserialize_project(self, data: Dict[str, Any]) -> Project:
|
|
264
|
+
"""Deserialize Project from JSON dict.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
data: Serialized project data
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Project instance
|
|
271
|
+
"""
|
|
272
|
+
return Project(
|
|
273
|
+
id=data["id"],
|
|
274
|
+
path=data["path"],
|
|
275
|
+
name=data["name"],
|
|
276
|
+
state=ProjectState(data["state"]),
|
|
277
|
+
state_reason=data.get("state_reason"),
|
|
278
|
+
config_loaded=data.get("config_loaded", False),
|
|
279
|
+
config=data.get("config"),
|
|
280
|
+
sessions={
|
|
281
|
+
sid: self._deserialize_session(sess)
|
|
282
|
+
for sid, sess in data.get("sessions", {}).items()
|
|
283
|
+
},
|
|
284
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
285
|
+
last_activity=datetime.fromisoformat(data["last_activity"]),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def _deserialize_session(self, data: Dict[str, Any]) -> ToolSession:
|
|
289
|
+
"""Deserialize ToolSession from JSON dict.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
data: Serialized session data
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
ToolSession instance
|
|
296
|
+
"""
|
|
297
|
+
return ToolSession(
|
|
298
|
+
id=data["id"],
|
|
299
|
+
project_id=data["project_id"],
|
|
300
|
+
runtime=data["runtime"],
|
|
301
|
+
tmux_target=data["tmux_target"],
|
|
302
|
+
status=data.get("status", "initializing"),
|
|
303
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
304
|
+
last_output_at=(
|
|
305
|
+
datetime.fromisoformat(data["last_output_at"])
|
|
306
|
+
if data.get("last_output_at")
|
|
307
|
+
else None
|
|
308
|
+
),
|
|
309
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Work item persistence for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module handles persistence and recovery of work queues,
|
|
4
|
+
including all work items across all projects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
from ..models.work import WorkItem
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkStore:
|
|
20
|
+
"""Persists and recovers work items.
|
|
21
|
+
|
|
22
|
+
Provides efficient work item persistence with:
|
|
23
|
+
- Batch save of all work items across all projects
|
|
24
|
+
- Atomic writes to prevent corruption
|
|
25
|
+
- Schema validation on load
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
state_dir: Directory for state files
|
|
29
|
+
work_path: Path to work.json
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> store = WorkStore(Path("~/.claude-mpm/commander"))
|
|
33
|
+
>>> await store.save_work(work_queues)
|
|
34
|
+
>>> work_items = await store.load_work()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
VERSION = "1.0"
|
|
38
|
+
|
|
39
|
+
def __init__(self, state_dir: Path):
|
|
40
|
+
"""Initialize work store.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
state_dir: Directory for state files (created if needed)
|
|
44
|
+
"""
|
|
45
|
+
self.state_dir = state_dir.expanduser()
|
|
46
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
self.work_path = self.state_dir / "work.json"
|
|
49
|
+
|
|
50
|
+
logger.info(f"Initialized WorkStore at {self.state_dir}")
|
|
51
|
+
|
|
52
|
+
async def save_work(self, work_queues: Dict[str, "WorkQueue"]) -> None: # noqa: F821
|
|
53
|
+
"""Save work items from all queues to disk.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
work_queues: Dict of project_id -> WorkQueue
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
IOError: If write fails
|
|
60
|
+
"""
|
|
61
|
+
# Collect all work items from all queues
|
|
62
|
+
all_items: List[WorkItem] = []
|
|
63
|
+
for queue in work_queues.values():
|
|
64
|
+
all_items.extend(queue.list())
|
|
65
|
+
|
|
66
|
+
data = {
|
|
67
|
+
"version": self.VERSION,
|
|
68
|
+
"saved_at": self._get_timestamp(),
|
|
69
|
+
"work_items": [item.to_dict() for item in all_items],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Run sync I/O in executor
|
|
73
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
74
|
+
None, self._atomic_write, self.work_path, data
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
f"Saved {len(all_items)} work items from {len(work_queues)} "
|
|
79
|
+
f"queues to {self.work_path}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def load_work(self) -> List[WorkItem]:
|
|
83
|
+
"""Load work items from disk.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of WorkItem instances (empty if file missing or corrupt)
|
|
87
|
+
"""
|
|
88
|
+
if not self.work_path.exists():
|
|
89
|
+
logger.info("No work file found, returning empty list")
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Run sync I/O in executor
|
|
94
|
+
data = await asyncio.get_event_loop().run_in_executor(
|
|
95
|
+
None, self._read_json, self.work_path
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if data.get("version") != self.VERSION:
|
|
99
|
+
logger.warning(
|
|
100
|
+
f"Version mismatch: expected {self.VERSION}, "
|
|
101
|
+
f"got {data.get('version')}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Deserialize work items
|
|
105
|
+
items = []
|
|
106
|
+
for item_data in data.get("work_items", []):
|
|
107
|
+
try:
|
|
108
|
+
item = WorkItem.from_dict(item_data)
|
|
109
|
+
items.append(item)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"Failed to deserialize work item: {e}")
|
|
112
|
+
logger.debug(f"Item data: {item_data}")
|
|
113
|
+
|
|
114
|
+
logger.info(f"Loaded {len(items)} work items from {self.work_path}")
|
|
115
|
+
return items
|
|
116
|
+
|
|
117
|
+
except json.JSONDecodeError as e:
|
|
118
|
+
logger.error(f"Failed to parse work file: {e}")
|
|
119
|
+
return []
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Failed to load work items: {e}")
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
def _atomic_write(self, path: Path, data: Dict[str, Any]) -> None:
|
|
125
|
+
"""Write data to file atomically.
|
|
126
|
+
|
|
127
|
+
Uses temp file + rename to ensure atomic write.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path: Target file path
|
|
131
|
+
data: Dictionary to write as JSON
|
|
132
|
+
"""
|
|
133
|
+
# Write to temp file
|
|
134
|
+
with tempfile.NamedTemporaryFile(
|
|
135
|
+
mode="w",
|
|
136
|
+
dir=path.parent,
|
|
137
|
+
prefix=".work-",
|
|
138
|
+
suffix=".tmp",
|
|
139
|
+
delete=False,
|
|
140
|
+
) as tmp:
|
|
141
|
+
json.dump(data, tmp, indent=2, default=str)
|
|
142
|
+
tmp_path = Path(tmp.name)
|
|
143
|
+
|
|
144
|
+
# Atomic rename
|
|
145
|
+
tmp_path.replace(path)
|
|
146
|
+
logger.debug(f"Wrote work file atomically: {path}")
|
|
147
|
+
|
|
148
|
+
def _read_json(self, path: Path) -> Dict[str, Any]:
|
|
149
|
+
"""Read JSON file.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
path: File path
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Parsed JSON data
|
|
156
|
+
"""
|
|
157
|
+
with open(path) as f:
|
|
158
|
+
return json.load(f)
|
|
159
|
+
|
|
160
|
+
def _get_timestamp(self) -> str:
|
|
161
|
+
"""Get current timestamp as ISO string."""
|
|
162
|
+
from datetime import datetime, timezone
|
|
163
|
+
|
|
164
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Output polling and event detection for MPM Commander."""
|
|
2
|
+
|
|
3
|
+
from .event_detector import BasicEventDetector, DetectedEvent, EventType
|
|
4
|
+
from .output_buffer import OutputBuffer
|
|
5
|
+
from .output_poller import OutputPoller
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BasicEventDetector",
|
|
9
|
+
"DetectedEvent",
|
|
10
|
+
"EventType",
|
|
11
|
+
"OutputBuffer",
|
|
12
|
+
"OutputPoller",
|
|
13
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Event detection for MPM Commander output."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventType(Enum):
|
|
10
|
+
"""Types of events that can be detected in output."""
|
|
11
|
+
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
IDLE = "idle"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DetectedEvent:
|
|
18
|
+
"""An event detected in session output."""
|
|
19
|
+
|
|
20
|
+
event_type: EventType
|
|
21
|
+
content: str
|
|
22
|
+
context: str # Surrounding lines for context
|
|
23
|
+
line_number: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BasicEventDetector:
|
|
27
|
+
"""Phase 1: Detect errors and idle states only."""
|
|
28
|
+
|
|
29
|
+
# ANSI escape code pattern
|
|
30
|
+
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
31
|
+
|
|
32
|
+
ERROR_PATTERNS = [
|
|
33
|
+
r"Error:",
|
|
34
|
+
r"Failed:",
|
|
35
|
+
r"Exception:",
|
|
36
|
+
r"Traceback \(most recent call last\):",
|
|
37
|
+
r"Permission denied",
|
|
38
|
+
r"command not found",
|
|
39
|
+
r"FATAL:",
|
|
40
|
+
r"✗", # Claude Code error indicator
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
IDLE_PATTERNS = [
|
|
44
|
+
r"^>\s*$", # Claude Code prompt
|
|
45
|
+
r"^claude>\s*$", # Alternative prompt
|
|
46
|
+
r"^\$\s*$", # Shell prompt
|
|
47
|
+
r"What would you like",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def strip_ansi(self, text: str) -> str:
|
|
51
|
+
"""Remove ANSI escape codes from text.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
text: Text potentially containing ANSI codes
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Clean text with ANSI codes removed
|
|
58
|
+
"""
|
|
59
|
+
return self.ANSI_ESCAPE.sub("", text)
|
|
60
|
+
|
|
61
|
+
def detect_error(self, content: str) -> Optional[DetectedEvent]:
|
|
62
|
+
"""Check for error patterns in output.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: Output content to check
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
DetectedEvent if error found, None otherwise
|
|
69
|
+
"""
|
|
70
|
+
clean = self.strip_ansi(content)
|
|
71
|
+
lines = clean.split("\n")
|
|
72
|
+
|
|
73
|
+
for i, line in enumerate(lines):
|
|
74
|
+
for pattern in self.ERROR_PATTERNS:
|
|
75
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
76
|
+
# Extract context (3 lines before/after)
|
|
77
|
+
start = max(0, i - 3)
|
|
78
|
+
end = min(len(lines), i + 4)
|
|
79
|
+
context = "\n".join(lines[start:end])
|
|
80
|
+
|
|
81
|
+
return DetectedEvent(
|
|
82
|
+
event_type=EventType.ERROR,
|
|
83
|
+
content=line.strip(),
|
|
84
|
+
context=context,
|
|
85
|
+
line_number=i + 1,
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def detect_idle(self, content: str) -> bool:
|
|
90
|
+
"""Check if tool is waiting for input (last line is a prompt).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
content: Output content to check
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if session appears to be idle (waiting for input)
|
|
97
|
+
"""
|
|
98
|
+
clean = self.strip_ansi(content)
|
|
99
|
+
lines = clean.strip().split("\n")
|
|
100
|
+
if not lines:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
last_line = lines[-1]
|
|
104
|
+
return any(re.search(p, last_line) for p in self.IDLE_PATTERNS)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Output buffer for tracking session output changes."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class OutputBuffer:
|
|
11
|
+
"""Tracks output from a session and detects changes."""
|
|
12
|
+
|
|
13
|
+
session_id: str
|
|
14
|
+
content: str = ""
|
|
15
|
+
last_hash: str = ""
|
|
16
|
+
last_update: Optional[datetime] = None
|
|
17
|
+
lines_captured: int = 0
|
|
18
|
+
|
|
19
|
+
def update(self, new_content: str) -> tuple[bool, str]:
|
|
20
|
+
"""Update buffer with new content.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
new_content: The new output content to check
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (has_changed, new_lines_only)
|
|
27
|
+
- has_changed: True if content changed since last update
|
|
28
|
+
- new_lines_only: Only the new lines added (diff)
|
|
29
|
+
"""
|
|
30
|
+
new_hash = hashlib.md5(new_content.encode(), usedforsecurity=False).hexdigest()
|
|
31
|
+
if new_hash == self.last_hash:
|
|
32
|
+
return False, ""
|
|
33
|
+
|
|
34
|
+
# Find new lines (diff)
|
|
35
|
+
old_lines = self.content.split("\n") if self.content else []
|
|
36
|
+
new_lines = new_content.split("\n")
|
|
37
|
+
|
|
38
|
+
# Simple diff: new lines at the end
|
|
39
|
+
if len(new_lines) > len(old_lines):
|
|
40
|
+
diff = "\n".join(new_lines[len(old_lines) :])
|
|
41
|
+
else:
|
|
42
|
+
diff = new_content # Content replaced entirely
|
|
43
|
+
|
|
44
|
+
self.content = new_content
|
|
45
|
+
self.last_hash = new_hash
|
|
46
|
+
self.last_update = datetime.now(timezone.utc)
|
|
47
|
+
self.lines_captured = len(new_lines)
|
|
48
|
+
|
|
49
|
+
return True, diff
|