claude-mpm 5.4.85__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +8 -5
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +109 -706
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/autotodos.py +566 -0
- claude_mpm/cli/commands/commander.py +216 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init/core.py +2 -2
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +128 -16
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +84 -1
- claude_mpm/cli/parsers/commander_parser.py +116 -0
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -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 +345 -40
- claude_mpm/cli/startup_display.py +76 -7
- claude_mpm/cli/startup_logging.py +2 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +78 -0
- claude_mpm/commander/adapters/__init__.py +60 -0
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +288 -0
- claude_mpm/commander/adapters/claude_code.py +392 -0
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +121 -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 +226 -0
- claude_mpm/commander/api/routes/work.py +296 -0
- claude_mpm/commander/api/schemas.py +186 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +149 -0
- claude_mpm/commander/chat/commands.py +124 -0
- claude_mpm/commander/chat/repl.py +1957 -0
- claude_mpm/commander/config.py +51 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +603 -0
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +392 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +233 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +57 -0
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -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 +868 -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/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +127 -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 +403 -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 +410 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +346 -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 +362 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +207 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +241 -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 +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +35 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +53 -4
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +39 -13
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +52 -12
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- 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/auto_pause_handler.py +485 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +466 -136
- claude_mpm/hooks/claude_hooks/hook_handler.py +204 -104
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +291 -59
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +43 -60
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +41 -26
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +38 -105
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +75 -77
- claude_mpm/hooks/session_resume_hook.py +89 -1
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +22 -15
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +46 -19
- 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/remote_agent_discovery_service.py +3 -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/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/command_deployment_service.py +44 -26
- 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/hook_installer_service.py +77 -8
- 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/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/daemon_manager.py +15 -4
- claude_mpm/services/monitor/management/lifecycle.py +8 -2
- claude_mpm/services/monitor/server.py +111 -16
- claude_mpm/services/pm_skills_deployer.py +261 -87
- claude_mpm/services/skills/git_skill_source_manager.py +130 -10
- claude_mpm/services/skills/selective_skill_deployer.py +142 -16
- 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/__init__.py +2 -1
- 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-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-pause/SKILL.md +170 -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/{pm-teaching-mode → mpm-teaching-mode}/SKILL.md +2 -2
- 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/registry.py +295 -90
- claude_mpm/skills/skill_manager.py +4 -4
- claude_mpm-5.6.76.dist-info/METADATA +416 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +312 -175
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- 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.85.dist-info/METADATA +0 -1023
- /claude_mpm/skills/bundled/pm/{pm-bug-reporting/pm-bug-reporting.md → mpm-bug-reporting/SKILL.md} +0 -0
- /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.85.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Output polling loop for MPM Commander."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from ..models.project import ProjectState, ToolSession
|
|
9
|
+
from ..registry import ProjectRegistry
|
|
10
|
+
from ..tmux_orchestrator import TmuxOrchestrator
|
|
11
|
+
from .event_detector import BasicEventDetector
|
|
12
|
+
from .output_buffer import OutputBuffer
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .event_detector import DetectedEvent
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OutputPoller:
|
|
21
|
+
"""Polls tmux sessions for new output and detects events."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
tmux: TmuxOrchestrator,
|
|
26
|
+
registry: ProjectRegistry,
|
|
27
|
+
poll_interval: float = 0.5,
|
|
28
|
+
capture_lines: int = 1000,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize output poller.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
tmux: TmuxOrchestrator instance for capturing output
|
|
34
|
+
registry: ProjectRegistry for state management
|
|
35
|
+
poll_interval: Seconds between polls (default: 0.5)
|
|
36
|
+
capture_lines: Number of lines to capture from tmux (default: 1000)
|
|
37
|
+
"""
|
|
38
|
+
self.tmux = tmux
|
|
39
|
+
self.registry = registry
|
|
40
|
+
self.poll_interval = poll_interval
|
|
41
|
+
self.capture_lines = capture_lines
|
|
42
|
+
self.detector = BasicEventDetector()
|
|
43
|
+
self.buffers: Dict[str, OutputBuffer] = {}
|
|
44
|
+
self._running = False
|
|
45
|
+
self._task: Optional[asyncio.Task[None]] = None
|
|
46
|
+
self.on_error: Optional[Callable[[str, DetectedEvent], None]] = None
|
|
47
|
+
self.on_idle: Optional[Callable[[str], None]] = None
|
|
48
|
+
|
|
49
|
+
async def start(self) -> None:
|
|
50
|
+
"""Start the polling loop."""
|
|
51
|
+
if self._running:
|
|
52
|
+
return
|
|
53
|
+
self._running = True
|
|
54
|
+
self._task = asyncio.create_task(self._poll_loop())
|
|
55
|
+
logger.info("Output poller started (interval: %.2fs)", self.poll_interval)
|
|
56
|
+
|
|
57
|
+
async def stop(self) -> None:
|
|
58
|
+
"""Stop the polling loop."""
|
|
59
|
+
self._running = False
|
|
60
|
+
if self._task:
|
|
61
|
+
self._task.cancel()
|
|
62
|
+
try:
|
|
63
|
+
await self._task
|
|
64
|
+
except asyncio.CancelledError:
|
|
65
|
+
pass
|
|
66
|
+
logger.info("Output poller stopped")
|
|
67
|
+
|
|
68
|
+
async def _poll_loop(self) -> None:
|
|
69
|
+
"""Main polling loop - runs until stopped."""
|
|
70
|
+
while self._running:
|
|
71
|
+
try:
|
|
72
|
+
await self._poll_all_sessions()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error("Polling error: %s", e, exc_info=True)
|
|
75
|
+
await asyncio.sleep(self.poll_interval)
|
|
76
|
+
|
|
77
|
+
async def _poll_all_sessions(self) -> None:
|
|
78
|
+
"""Poll all active sessions for new output."""
|
|
79
|
+
for project in self.registry.list_all():
|
|
80
|
+
for session_id, session in project.sessions.items():
|
|
81
|
+
if session.status in ("stopped", "error"):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
await self._poll_session(project.id, session)
|
|
85
|
+
|
|
86
|
+
async def _poll_session(self, project_id: str, session: ToolSession) -> None:
|
|
87
|
+
"""Poll a single session for output changes.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
project_id: ID of the project owning the session
|
|
91
|
+
session: Session object to poll
|
|
92
|
+
"""
|
|
93
|
+
# Skip stopped or errored sessions
|
|
94
|
+
if session.status in ("stopped", "error"):
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Get or create buffer
|
|
98
|
+
if session.id not in self.buffers:
|
|
99
|
+
self.buffers[session.id] = OutputBuffer(session_id=session.id)
|
|
100
|
+
|
|
101
|
+
buffer = self.buffers[session.id]
|
|
102
|
+
|
|
103
|
+
# Capture output from tmux
|
|
104
|
+
try:
|
|
105
|
+
output = self.tmux.capture_output(
|
|
106
|
+
session.tmux_target, lines=self.capture_lines
|
|
107
|
+
)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning("Failed to capture output for %s: %s", session.id, e)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Check for changes
|
|
113
|
+
has_changed, new_content = buffer.update(output)
|
|
114
|
+
if not has_changed:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Update session output buffer
|
|
118
|
+
session.output_buffer = output
|
|
119
|
+
session.last_output_at = datetime.now(timezone.utc)
|
|
120
|
+
|
|
121
|
+
# Detect events - errors take priority
|
|
122
|
+
error_event = self.detector.detect_error(new_content)
|
|
123
|
+
if error_event:
|
|
124
|
+
logger.warning("Error detected in %s: %s", session.id, error_event.content)
|
|
125
|
+
session.status = "error"
|
|
126
|
+
self.registry.update_state(
|
|
127
|
+
project_id, ProjectState.ERROR, error_event.content
|
|
128
|
+
)
|
|
129
|
+
if self.on_error:
|
|
130
|
+
self.on_error(session.id, error_event)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Check for idle state
|
|
134
|
+
is_idle = self.detector.detect_idle(output)
|
|
135
|
+
if is_idle:
|
|
136
|
+
if session.status != "idle":
|
|
137
|
+
logger.debug("Session %s now idle", session.id)
|
|
138
|
+
session.status = "idle"
|
|
139
|
+
if self.on_idle:
|
|
140
|
+
self.on_idle(session.id)
|
|
141
|
+
elif session.status == "idle":
|
|
142
|
+
logger.debug("Session %s now running", session.id)
|
|
143
|
+
session.status = "running"
|
|
144
|
+
self.registry.update_state(project_id, ProjectState.WORKING)
|
|
145
|
+
|
|
146
|
+
def clear_buffer(self, session_id: str) -> None:
|
|
147
|
+
"""Clear the buffer for a session.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: ID of session to clear buffer for
|
|
151
|
+
"""
|
|
152
|
+
if session_id in self.buffers:
|
|
153
|
+
del self.buffers[session_id]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Project session lifecycle coordinator for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module manages per-project execution state, coordinating between
|
|
4
|
+
runtime processes, events, and work queue.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .models import Project, ProjectState
|
|
12
|
+
from .runtime.executor import RuntimeExecutor
|
|
13
|
+
from .runtime.monitor import RuntimeMonitor
|
|
14
|
+
from .tmux_orchestrator import TmuxOrchestrator
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SessionState(Enum):
|
|
20
|
+
"""Project session execution state.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
IDLE: No active work, ready to start
|
|
24
|
+
STARTING: Initializing session resources
|
|
25
|
+
RUNNING: Actively executing work
|
|
26
|
+
PAUSED: Execution paused (e.g., waiting for event resolution)
|
|
27
|
+
STOPPING: Shutting down gracefully
|
|
28
|
+
STOPPED: Session terminated
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
IDLE = "idle"
|
|
32
|
+
STARTING = "starting"
|
|
33
|
+
RUNNING = "running"
|
|
34
|
+
PAUSED = "paused"
|
|
35
|
+
STOPPING = "stopping"
|
|
36
|
+
STOPPED = "stopped"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProjectSession:
|
|
40
|
+
"""Manages lifecycle of a single project's work execution.
|
|
41
|
+
|
|
42
|
+
Coordinates between runtime executor, event system, and work queue
|
|
43
|
+
for autonomous task execution within a project.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
project: Associated project instance
|
|
47
|
+
orchestrator: Tmux orchestrator for process management
|
|
48
|
+
executor: RuntimeExecutor for spawning/managing processes
|
|
49
|
+
monitor: RuntimeMonitor for output monitoring and event detection
|
|
50
|
+
state: Current session execution state
|
|
51
|
+
pause_reason: Reason for pause (e.g., event ID)
|
|
52
|
+
active_pane: Tmux pane target for active runtime, if any
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> session = ProjectSession(project, tmux_orchestrator, executor, monitor)
|
|
56
|
+
>>> await session.start()
|
|
57
|
+
>>> session.state
|
|
58
|
+
<SessionState.RUNNING: 'running'>
|
|
59
|
+
>>> await session.pause("Event requires user input")
|
|
60
|
+
>>> await session.resume()
|
|
61
|
+
>>> await session.stop()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
project: Project,
|
|
67
|
+
orchestrator: TmuxOrchestrator,
|
|
68
|
+
executor: Optional[RuntimeExecutor] = None,
|
|
69
|
+
monitor: Optional[RuntimeMonitor] = None,
|
|
70
|
+
):
|
|
71
|
+
"""Initialize project session.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
project: Project instance to manage
|
|
75
|
+
orchestrator: TmuxOrchestrator for process control
|
|
76
|
+
executor: Optional RuntimeExecutor (created if not provided)
|
|
77
|
+
monitor: Optional RuntimeMonitor (not used if not provided)
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If project or orchestrator is None
|
|
81
|
+
"""
|
|
82
|
+
if project is None:
|
|
83
|
+
raise ValueError("Project cannot be None")
|
|
84
|
+
if orchestrator is None:
|
|
85
|
+
raise ValueError("Orchestrator cannot be None")
|
|
86
|
+
|
|
87
|
+
self.project = project
|
|
88
|
+
self.orchestrator = orchestrator
|
|
89
|
+
self.executor = executor or RuntimeExecutor(orchestrator)
|
|
90
|
+
self.monitor = monitor
|
|
91
|
+
self._state = SessionState.IDLE
|
|
92
|
+
self.pause_reason: Optional[str] = None
|
|
93
|
+
self.active_pane: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
logger.info(f"Created ProjectSession for project {project.id}")
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def state(self) -> SessionState:
|
|
99
|
+
"""Get current session state.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Current SessionState
|
|
103
|
+
"""
|
|
104
|
+
return self._state
|
|
105
|
+
|
|
106
|
+
async def start(self) -> None:
|
|
107
|
+
"""Initialize and start project session.
|
|
108
|
+
|
|
109
|
+
Transitions from IDLE → STARTING → RUNNING.
|
|
110
|
+
Spawns Claude Code via RuntimeExecutor and starts output monitoring
|
|
111
|
+
via RuntimeMonitor if available.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
RuntimeError: If session not in IDLE state
|
|
115
|
+
"""
|
|
116
|
+
if self._state != SessionState.IDLE:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"Cannot start session in state {self._state}. Must be IDLE."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
logger.info(f"Starting session for project {self.project.id}")
|
|
122
|
+
self._state = SessionState.STARTING
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Create tmux session if needed
|
|
126
|
+
if not self.orchestrator.session_exists():
|
|
127
|
+
self.orchestrator.create_session()
|
|
128
|
+
logger.debug("Created tmux session")
|
|
129
|
+
|
|
130
|
+
# Spawn Claude Code process
|
|
131
|
+
self.active_pane = await self.executor.spawn(self.project, command="claude")
|
|
132
|
+
logger.info(f"Spawned Claude Code in pane {self.active_pane}")
|
|
133
|
+
|
|
134
|
+
# Start monitoring if monitor available
|
|
135
|
+
if self.monitor:
|
|
136
|
+
await self.monitor.start_monitoring(self.active_pane, self.project.id)
|
|
137
|
+
logger.debug(f"Started monitoring pane {self.active_pane}")
|
|
138
|
+
|
|
139
|
+
# Update project state
|
|
140
|
+
self.project.state = ProjectState.IDLE
|
|
141
|
+
self.project.state_reason = None
|
|
142
|
+
|
|
143
|
+
# Transition to RUNNING
|
|
144
|
+
self._state = SessionState.RUNNING
|
|
145
|
+
logger.info(f"Session started for project {self.project.id}")
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Failed to start session: {e}")
|
|
149
|
+
self._state = SessionState.IDLE
|
|
150
|
+
# Clean up pane if created
|
|
151
|
+
if self.active_pane:
|
|
152
|
+
try:
|
|
153
|
+
await self.executor.terminate(self.active_pane)
|
|
154
|
+
except Exception as cleanup_error:
|
|
155
|
+
logger.debug(f"Error cleaning up pane: {cleanup_error}")
|
|
156
|
+
self.active_pane = None
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
async def pause(self, reason: str) -> None:
|
|
160
|
+
"""Pause execution (e.g., waiting for event resolution).
|
|
161
|
+
|
|
162
|
+
Transitions from RUNNING → PAUSED.
|
|
163
|
+
Execution will not resume until resume() is called.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
reason: Reason for pause (typically event ID or description)
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
RuntimeError: If session not in RUNNING state
|
|
170
|
+
"""
|
|
171
|
+
if self._state != SessionState.RUNNING:
|
|
172
|
+
raise RuntimeError(
|
|
173
|
+
f"Cannot pause session in state {self._state}. Must be RUNNING."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
logger.info(f"Pausing session for project {self.project.id}: {reason}")
|
|
177
|
+
self._state = SessionState.PAUSED
|
|
178
|
+
self.pause_reason = reason
|
|
179
|
+
|
|
180
|
+
# Update project state
|
|
181
|
+
self.project.state = ProjectState.BLOCKED
|
|
182
|
+
self.project.state_reason = reason
|
|
183
|
+
|
|
184
|
+
async def resume(self) -> None:
|
|
185
|
+
"""Resume execution after pause.
|
|
186
|
+
|
|
187
|
+
Transitions from PAUSED → RUNNING.
|
|
188
|
+
Clears pause reason and continues work execution.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
RuntimeError: If session not in PAUSED state
|
|
192
|
+
"""
|
|
193
|
+
if self._state != SessionState.PAUSED:
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f"Cannot resume session in state {self._state}. Must be PAUSED."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
logger.info(f"Resuming session for project {self.project.id}")
|
|
199
|
+
self._state = SessionState.RUNNING
|
|
200
|
+
self.pause_reason = None
|
|
201
|
+
|
|
202
|
+
# Update project state
|
|
203
|
+
self.project.state = ProjectState.WORKING
|
|
204
|
+
self.project.state_reason = None
|
|
205
|
+
|
|
206
|
+
async def stop(self) -> None:
|
|
207
|
+
"""Gracefully stop project session.
|
|
208
|
+
|
|
209
|
+
Transitions from any state → STOPPING → STOPPED.
|
|
210
|
+
Stops monitoring, terminates runtime, and cleans up resources.
|
|
211
|
+
"""
|
|
212
|
+
logger.info(f"Stopping session for project {self.project.id}")
|
|
213
|
+
self._state = SessionState.STOPPING
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Stop monitoring if active
|
|
217
|
+
if self.monitor and self.active_pane:
|
|
218
|
+
await self.monitor.stop_monitoring(self.active_pane)
|
|
219
|
+
logger.debug(f"Stopped monitoring pane {self.active_pane}")
|
|
220
|
+
|
|
221
|
+
# Terminate active pane
|
|
222
|
+
if self.active_pane:
|
|
223
|
+
await self.executor.terminate(self.active_pane)
|
|
224
|
+
logger.debug(f"Terminated pane {self.active_pane}")
|
|
225
|
+
self.active_pane = None
|
|
226
|
+
|
|
227
|
+
# Clean up any remaining sessions (backward compatibility)
|
|
228
|
+
for session_id in list(self.project.sessions.keys()):
|
|
229
|
+
try:
|
|
230
|
+
session = self.project.sessions[session_id]
|
|
231
|
+
if session.tmux_target:
|
|
232
|
+
# Kill tmux pane
|
|
233
|
+
self.orchestrator.kill_pane(session.tmux_target)
|
|
234
|
+
logger.debug(f"Killed pane {session.tmux_target}")
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.warning(f"Error killing session {session_id}: {e}")
|
|
237
|
+
|
|
238
|
+
# Update project state
|
|
239
|
+
self.project.state = ProjectState.IDLE
|
|
240
|
+
self.project.state_reason = None
|
|
241
|
+
|
|
242
|
+
# Transition to STOPPED
|
|
243
|
+
self._state = SessionState.STOPPED
|
|
244
|
+
logger.info(f"Session stopped for project {self.project.id}")
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Error during session stop: {e}")
|
|
248
|
+
self._state = SessionState.STOPPED
|
|
249
|
+
raise
|
|
250
|
+
|
|
251
|
+
def is_ready(self) -> bool:
|
|
252
|
+
"""Check if session can start new work.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if session is in RUNNING state (not blocked or paused)
|
|
256
|
+
"""
|
|
257
|
+
return self._state == SessionState.RUNNING
|
|
258
|
+
|
|
259
|
+
def can_accept_work(self) -> bool:
|
|
260
|
+
"""Check if session can accept new work items.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True if session is IDLE or RUNNING with no active work
|
|
264
|
+
"""
|
|
265
|
+
return (
|
|
266
|
+
self._state in (SessionState.IDLE, SessionState.RUNNING)
|
|
267
|
+
and self.project.active_work is None
|
|
268
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Output proxy and summarization for MPM Commander."""
|
|
2
|
+
|
|
3
|
+
from .formatter import OutputFormatter
|
|
4
|
+
from .output_handler import OutputChunk, OutputHandler
|
|
5
|
+
from .relay import OutputRelay
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"OutputChunk",
|
|
9
|
+
"OutputFormatter",
|
|
10
|
+
"OutputHandler",
|
|
11
|
+
"OutputRelay",
|
|
12
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Formats output for Commander chat display."""
|
|
2
|
+
|
|
3
|
+
from .output_handler import OutputChunk
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OutputFormatter:
|
|
7
|
+
"""Formats Claude Code output for user display."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, max_raw_display: int = 500):
|
|
10
|
+
"""Initialize OutputFormatter.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
max_raw_display: Maximum characters of raw output to display.
|
|
14
|
+
"""
|
|
15
|
+
self.max_raw_display = max_raw_display
|
|
16
|
+
|
|
17
|
+
def format_summary(self, chunk: OutputChunk) -> str:
|
|
18
|
+
"""Format a summarized output chunk for display.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
chunk: OutputChunk with summary.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Formatted output with summary and optionally truncated raw output.
|
|
25
|
+
"""
|
|
26
|
+
lines = []
|
|
27
|
+
|
|
28
|
+
# Instance header
|
|
29
|
+
status = "✓" if chunk.is_complete else "⋯"
|
|
30
|
+
lines.append(f"[{chunk.instance_name}] {status}")
|
|
31
|
+
|
|
32
|
+
# Summary if available
|
|
33
|
+
if chunk.summary:
|
|
34
|
+
lines.append(f"\n📝 Summary: {chunk.summary}")
|
|
35
|
+
|
|
36
|
+
# Show truncated raw output if it's short enough
|
|
37
|
+
if len(chunk.raw_output) <= self.max_raw_display:
|
|
38
|
+
lines.append(f"\n```\n{chunk.raw_output}\n```")
|
|
39
|
+
else:
|
|
40
|
+
# Show truncated preview
|
|
41
|
+
preview = chunk.raw_output[: self.max_raw_display]
|
|
42
|
+
lines.append(
|
|
43
|
+
f"\n```\n{preview}...\n(truncated, {len(chunk.raw_output)} chars total)\n```"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return "".join(lines)
|
|
47
|
+
|
|
48
|
+
def format_raw(self, chunk: OutputChunk, truncate: bool = True) -> str:
|
|
49
|
+
"""Format raw output, optionally truncated.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
chunk: OutputChunk to format.
|
|
53
|
+
truncate: Whether to truncate long output.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Formatted raw output.
|
|
57
|
+
"""
|
|
58
|
+
status = "✓" if chunk.is_complete else "⋯"
|
|
59
|
+
header = f"[{chunk.instance_name}] {status}"
|
|
60
|
+
|
|
61
|
+
raw = chunk.raw_output
|
|
62
|
+
if truncate and len(raw) > self.max_raw_display:
|
|
63
|
+
raw = raw[: self.max_raw_display] + "...\n(truncated)"
|
|
64
|
+
|
|
65
|
+
return f"{header}\n```\n{raw}\n```"
|
|
66
|
+
|
|
67
|
+
def format_status(self, instance_name: str, status: str) -> str:
|
|
68
|
+
"""Format a status message.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
instance_name: Name of the instance.
|
|
72
|
+
status: Status message.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Formatted status message.
|
|
76
|
+
"""
|
|
77
|
+
return f"[{instance_name}] ℹ️ {status}"
|
|
78
|
+
|
|
79
|
+
def format_error(self, instance_name: str, error: str) -> str:
|
|
80
|
+
"""Format an error message.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
instance_name: Name of the instance.
|
|
84
|
+
error: Error message.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Formatted error message.
|
|
88
|
+
"""
|
|
89
|
+
return f"[{instance_name}] ❌ Error: {error}"
|