claude-mpm 5.4.85__py3-none-any.whl → 5.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +101 -703
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +566 -0
- claude_mpm/cli/commands/commander.py +46 -0
- 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/executor.py +119 -16
- claude_mpm/cli/parsers/base_parser.py +71 -1
- claude_mpm/cli/parsers/commander_parser.py +83 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/startup.py +54 -16
- 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 +112 -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 +215 -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/core/config.py +5 -0
- claude_mpm/core/hook_manager.py +51 -3
- claude_mpm/core/logger.py +10 -7
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/output_style_manager.py +15 -5
- claude_mpm/core/unified_config.py +10 -6
- 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 +486 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +250 -11
- 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 +69 -5
- claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
- 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/scripts/claude-hook-handler.sh +36 -10
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- 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/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 +51 -2
- claude_mpm/services/skills/selective_skill_deployer.py +114 -16
- claude_mpm/services/skills/skill_discovery_service.py +57 -3
- 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-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/{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/skill_manager.py +4 -4
- claude_mpm-5.6.1.dist-info/METADATA +391 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/RECORD +244 -145
- 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.1.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Parse tool output and detect events."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from ..events.manager import EventManager
|
|
9
|
+
from ..models.events import EventType
|
|
10
|
+
from .extractor import (
|
|
11
|
+
extract_action_details,
|
|
12
|
+
extract_error_context,
|
|
13
|
+
extract_options,
|
|
14
|
+
strip_code_blocks,
|
|
15
|
+
)
|
|
16
|
+
from .patterns import ALL_PATTERNS
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# ANSI escape code pattern
|
|
21
|
+
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ParseResult:
|
|
26
|
+
"""Result of parsing output."""
|
|
27
|
+
|
|
28
|
+
event_type: EventType
|
|
29
|
+
title: str
|
|
30
|
+
content: str
|
|
31
|
+
options: Optional[List[str]] = None
|
|
32
|
+
context: Optional[Dict[str, Any]] = None
|
|
33
|
+
match_start: int = 0
|
|
34
|
+
match_end: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OutputParser:
|
|
38
|
+
"""Parses tool output and detects events.
|
|
39
|
+
|
|
40
|
+
Detects various event types including:
|
|
41
|
+
- Decisions needed (with option extraction)
|
|
42
|
+
- Approvals required (with action details)
|
|
43
|
+
- Errors (with context)
|
|
44
|
+
- Task completions
|
|
45
|
+
- Clarifications needed
|
|
46
|
+
|
|
47
|
+
Features:
|
|
48
|
+
- ANSI escape code stripping
|
|
49
|
+
- Code block exclusion (avoid false positives)
|
|
50
|
+
- Overlap deduplication
|
|
51
|
+
- Option extraction from various formats
|
|
52
|
+
- Integration with EventManager
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, event_manager: Optional[EventManager] = None):
|
|
56
|
+
"""Initialize parser.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
event_manager: Optional EventManager for automatic event creation
|
|
60
|
+
"""
|
|
61
|
+
self.event_manager = event_manager
|
|
62
|
+
self._patterns = ALL_PATTERNS
|
|
63
|
+
|
|
64
|
+
def strip_ansi(self, text: str) -> str:
|
|
65
|
+
"""Remove ANSI escape codes.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
text: Text potentially containing ANSI codes
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Text with ANSI codes removed
|
|
72
|
+
"""
|
|
73
|
+
return ANSI_ESCAPE.sub("", text)
|
|
74
|
+
|
|
75
|
+
def parse(
|
|
76
|
+
self,
|
|
77
|
+
content: str,
|
|
78
|
+
project_id: str,
|
|
79
|
+
session_id: Optional[str] = None,
|
|
80
|
+
create_events: bool = True,
|
|
81
|
+
) -> List[ParseResult]:
|
|
82
|
+
"""Parse output and detect all events.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
content: Output content to parse
|
|
86
|
+
project_id: Project identifier for event creation
|
|
87
|
+
session_id: Optional session identifier
|
|
88
|
+
create_events: Whether to create events via EventManager
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of ParseResult objects for all detected events
|
|
92
|
+
"""
|
|
93
|
+
results: List[ParseResult] = []
|
|
94
|
+
|
|
95
|
+
# Clean content
|
|
96
|
+
clean_content = self.strip_ansi(content)
|
|
97
|
+
|
|
98
|
+
# Strip code blocks for pattern matching
|
|
99
|
+
matchable_content = strip_code_blocks(clean_content)
|
|
100
|
+
|
|
101
|
+
# Check each pattern category
|
|
102
|
+
for category, patterns in self._patterns.items():
|
|
103
|
+
for pattern, event_type in patterns:
|
|
104
|
+
for match in pattern.finditer(matchable_content):
|
|
105
|
+
result = self._create_result(
|
|
106
|
+
event_type=event_type,
|
|
107
|
+
match=match,
|
|
108
|
+
original_content=clean_content,
|
|
109
|
+
matchable_content=matchable_content,
|
|
110
|
+
)
|
|
111
|
+
if result:
|
|
112
|
+
results.append(result)
|
|
113
|
+
|
|
114
|
+
# Deduplicate overlapping results
|
|
115
|
+
results = self._deduplicate(results)
|
|
116
|
+
|
|
117
|
+
# Create events if manager provided and flag set
|
|
118
|
+
if self.event_manager and create_events:
|
|
119
|
+
for result in results:
|
|
120
|
+
self.event_manager.create(
|
|
121
|
+
project_id=project_id,
|
|
122
|
+
session_id=session_id,
|
|
123
|
+
event_type=result.event_type,
|
|
124
|
+
title=result.title,
|
|
125
|
+
content=result.content,
|
|
126
|
+
options=result.options,
|
|
127
|
+
context=result.context,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
logger.debug("Parsed %d events from output", len(results))
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
def _create_result(
|
|
134
|
+
self,
|
|
135
|
+
event_type: EventType,
|
|
136
|
+
match: re.Match,
|
|
137
|
+
original_content: str,
|
|
138
|
+
matchable_content: str,
|
|
139
|
+
) -> Optional[ParseResult]:
|
|
140
|
+
"""Create a ParseResult from a pattern match.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
event_type: Type of event detected
|
|
144
|
+
match: Regex match object
|
|
145
|
+
original_content: Original content (with code blocks)
|
|
146
|
+
matchable_content: Content with code blocks stripped
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
ParseResult if valid, None otherwise
|
|
150
|
+
"""
|
|
151
|
+
matched_text = match.group(0)
|
|
152
|
+
options = None
|
|
153
|
+
context = {}
|
|
154
|
+
|
|
155
|
+
# Generate title and extract context based on event type
|
|
156
|
+
if event_type == EventType.ERROR:
|
|
157
|
+
title = self._extract_error_title(matched_text)
|
|
158
|
+
context = extract_error_context(
|
|
159
|
+
original_content, match.start(), match.end()
|
|
160
|
+
)
|
|
161
|
+
elif event_type == EventType.DECISION_NEEDED:
|
|
162
|
+
title = "Decision needed"
|
|
163
|
+
# Look ahead for options (up to 500 chars)
|
|
164
|
+
options = extract_options(
|
|
165
|
+
original_content[match.start() : match.start() + 500]
|
|
166
|
+
)
|
|
167
|
+
context = {"options_detected": len(options) if options else 0}
|
|
168
|
+
elif event_type == EventType.APPROVAL:
|
|
169
|
+
title = "Approval required"
|
|
170
|
+
context = extract_action_details(original_content, match)
|
|
171
|
+
options = ["Yes", "No"]
|
|
172
|
+
elif event_type == EventType.TASK_COMPLETE:
|
|
173
|
+
title = "Task completed"
|
|
174
|
+
elif event_type == EventType.CLARIFICATION:
|
|
175
|
+
title = "Clarification needed"
|
|
176
|
+
else:
|
|
177
|
+
title = f"{event_type.value} detected"
|
|
178
|
+
|
|
179
|
+
return ParseResult(
|
|
180
|
+
event_type=event_type,
|
|
181
|
+
title=title,
|
|
182
|
+
content=matched_text[:500], # Truncate long matches
|
|
183
|
+
options=options
|
|
184
|
+
if event_type in (EventType.DECISION_NEEDED, EventType.APPROVAL)
|
|
185
|
+
else None,
|
|
186
|
+
context=context,
|
|
187
|
+
match_start=match.start(),
|
|
188
|
+
match_end=match.end(),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _extract_error_title(self, matched_text: str) -> str:
|
|
192
|
+
"""Extract a concise error title.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
matched_text: Text that matched the error pattern
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Concise error title (max 80 chars)
|
|
199
|
+
"""
|
|
200
|
+
# Try to get the error type and message
|
|
201
|
+
error_match = re.search(
|
|
202
|
+
r"(\w+(?:Error|Exception)): (.+?)(?:\n|$)", matched_text
|
|
203
|
+
)
|
|
204
|
+
if error_match:
|
|
205
|
+
error_type = error_match.group(1)
|
|
206
|
+
error_msg = error_match.group(2)[:50]
|
|
207
|
+
return f"{error_type}: {error_msg}"
|
|
208
|
+
|
|
209
|
+
# Fallback to first line
|
|
210
|
+
first_line = matched_text.split("\n")[0].strip()
|
|
211
|
+
return first_line[:80] if first_line else "Error detected"
|
|
212
|
+
|
|
213
|
+
def _deduplicate(self, results: List[ParseResult]) -> List[ParseResult]:
|
|
214
|
+
"""Remove duplicate or overlapping results.
|
|
215
|
+
|
|
216
|
+
When results overlap, keep the one with higher priority.
|
|
217
|
+
Priority order: ERROR > APPROVAL > DECISION > CLARIFICATION > COMPLETION
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
results: List of ParseResult objects to deduplicate
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Deduplicated list of ParseResult objects
|
|
224
|
+
"""
|
|
225
|
+
if not results:
|
|
226
|
+
return results
|
|
227
|
+
|
|
228
|
+
# Sort by position
|
|
229
|
+
sorted_results = sorted(results, key=lambda r: r.match_start)
|
|
230
|
+
|
|
231
|
+
# Priority order (lower index = higher priority)
|
|
232
|
+
priority_order = [
|
|
233
|
+
EventType.ERROR,
|
|
234
|
+
EventType.APPROVAL,
|
|
235
|
+
EventType.DECISION_NEEDED,
|
|
236
|
+
EventType.CLARIFICATION,
|
|
237
|
+
EventType.TASK_COMPLETE,
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
deduped: List[ParseResult] = []
|
|
241
|
+
for result in sorted_results:
|
|
242
|
+
# Check if overlaps with any existing result
|
|
243
|
+
overlaps = False
|
|
244
|
+
for existing in deduped:
|
|
245
|
+
if (
|
|
246
|
+
result.match_start < existing.match_end
|
|
247
|
+
and result.match_end > existing.match_start
|
|
248
|
+
):
|
|
249
|
+
# Overlap detected - keep higher priority
|
|
250
|
+
result_priority = (
|
|
251
|
+
priority_order.index(result.event_type)
|
|
252
|
+
if result.event_type in priority_order
|
|
253
|
+
else 99
|
|
254
|
+
)
|
|
255
|
+
existing_priority = (
|
|
256
|
+
priority_order.index(existing.event_type)
|
|
257
|
+
if existing.event_type in priority_order
|
|
258
|
+
else 99
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if result_priority < existing_priority:
|
|
262
|
+
deduped.remove(existing)
|
|
263
|
+
else:
|
|
264
|
+
overlaps = True
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
if not overlaps:
|
|
268
|
+
deduped.append(result)
|
|
269
|
+
|
|
270
|
+
return deduped
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Detection patterns for various event types in tool output."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
from ..models.events import EventType
|
|
7
|
+
|
|
8
|
+
# Pattern format: (compiled_regex, event_type)
|
|
9
|
+
|
|
10
|
+
DECISION_PATTERNS: List[Tuple[re.Pattern, EventType]] = [
|
|
11
|
+
(
|
|
12
|
+
re.compile(
|
|
13
|
+
r"Which (?:option|approach|method) (?:would you|do you) prefer\?", re.I
|
|
14
|
+
),
|
|
15
|
+
EventType.DECISION_NEEDED,
|
|
16
|
+
),
|
|
17
|
+
(
|
|
18
|
+
re.compile(
|
|
19
|
+
r"Should I (?:proceed|continue|use|implement) (?:with )?(.+)\?", re.I
|
|
20
|
+
),
|
|
21
|
+
EventType.DECISION_NEEDED,
|
|
22
|
+
),
|
|
23
|
+
(re.compile(r"Do you want me to (.+)\?", re.I), EventType.DECISION_NEEDED),
|
|
24
|
+
(
|
|
25
|
+
re.compile(r"Please choose:?\s*\n(?:\s*\d+[\.\)]\s*.+\n?)+", re.I | re.M),
|
|
26
|
+
EventType.DECISION_NEEDED,
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
re.compile(r"Options:?\s*\n(?:\s*[-•]\s*.+\n?)+", re.I | re.M),
|
|
30
|
+
EventType.DECISION_NEEDED,
|
|
31
|
+
),
|
|
32
|
+
(re.compile(r"\(y/n\)\??", re.I), EventType.DECISION_NEEDED),
|
|
33
|
+
(re.compile(r"\[Y/n\]", re.I), EventType.DECISION_NEEDED),
|
|
34
|
+
(re.compile(r"\[yes/no\]", re.I), EventType.DECISION_NEEDED),
|
|
35
|
+
(re.compile(r"Select an option:", re.I), EventType.DECISION_NEEDED),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
APPROVAL_PATTERNS: List[Tuple[re.Pattern, EventType]] = [
|
|
39
|
+
(
|
|
40
|
+
re.compile(r"This will (?:delete|remove|overwrite|modify) (.+)", re.I),
|
|
41
|
+
EventType.APPROVAL,
|
|
42
|
+
),
|
|
43
|
+
(re.compile(r"Are you sure you want to (.+)\?", re.I), EventType.APPROVAL),
|
|
44
|
+
(re.compile(r"This action cannot be undone", re.I), EventType.APPROVAL),
|
|
45
|
+
(re.compile(r"Warning: This will (.+)", re.I), EventType.APPROVAL),
|
|
46
|
+
(re.compile(r"Do you want to allow (.+)\?", re.I), EventType.APPROVAL),
|
|
47
|
+
(re.compile(r"Permanently delete", re.I), EventType.APPROVAL),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
ERROR_PATTERNS: List[Tuple[re.Pattern, EventType]] = [
|
|
51
|
+
(re.compile(r"Traceback \(most recent call last\):", re.I), EventType.ERROR),
|
|
52
|
+
(re.compile(r"(\w+Error): (.+)", re.I), EventType.ERROR),
|
|
53
|
+
(re.compile(r"(\w+Exception): (.+)", re.I), EventType.ERROR),
|
|
54
|
+
(re.compile(r"^Error: (.+)", re.I | re.M), EventType.ERROR),
|
|
55
|
+
(re.compile(r"^Failed: (.+)", re.I | re.M), EventType.ERROR),
|
|
56
|
+
(re.compile(r"^FATAL: (.+)", re.I | re.M), EventType.ERROR),
|
|
57
|
+
(re.compile(r"Permission denied", re.I), EventType.ERROR),
|
|
58
|
+
(re.compile(r"Access denied", re.I), EventType.ERROR),
|
|
59
|
+
(re.compile(r"(?:File|Directory) not found", re.I), EventType.ERROR),
|
|
60
|
+
(re.compile(r"Connection refused", re.I), EventType.ERROR),
|
|
61
|
+
(re.compile(r"Timeout(?:Error)?", re.I), EventType.ERROR),
|
|
62
|
+
(re.compile(r"✗", re.I), EventType.ERROR), # Claude Code error indicator
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
COMPLETION_PATTERNS: List[Tuple[re.Pattern, EventType]] = [
|
|
66
|
+
(
|
|
67
|
+
re.compile(r"(?:Done|Complete|Finished|Success)[\.\!]?\s*$", re.I | re.M),
|
|
68
|
+
EventType.TASK_COMPLETE,
|
|
69
|
+
),
|
|
70
|
+
(re.compile(r"Successfully (.+)", re.I), EventType.TASK_COMPLETE),
|
|
71
|
+
(
|
|
72
|
+
re.compile(r"I(?:'ve| have) (?:completed|finished|done) (.+)", re.I),
|
|
73
|
+
EventType.TASK_COMPLETE,
|
|
74
|
+
),
|
|
75
|
+
(re.compile(r"Task (?:complete|finished)", re.I), EventType.TASK_COMPLETE),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
CLARIFICATION_PATTERNS: List[Tuple[re.Pattern, EventType]] = [
|
|
79
|
+
(
|
|
80
|
+
re.compile(
|
|
81
|
+
r"Could you (?:please )?(?:clarify|explain|provide more details)", re.I
|
|
82
|
+
),
|
|
83
|
+
EventType.CLARIFICATION,
|
|
84
|
+
),
|
|
85
|
+
(re.compile(r"I need more information about", re.I), EventType.CLARIFICATION),
|
|
86
|
+
(re.compile(r"What do you mean by", re.I), EventType.CLARIFICATION),
|
|
87
|
+
(re.compile(r"Can you be more specific", re.I), EventType.CLARIFICATION),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Patterns to ignore (inside code blocks, etc.)
|
|
91
|
+
CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.M)
|
|
92
|
+
INLINE_CODE_PATTERN = re.compile(r"`[^`]+`")
|
|
93
|
+
|
|
94
|
+
ALL_PATTERNS = {
|
|
95
|
+
"decision": DECISION_PATTERNS,
|
|
96
|
+
"approval": APPROVAL_PATTERNS,
|
|
97
|
+
"error": ERROR_PATTERNS,
|
|
98
|
+
"completion": COMPLETION_PATTERNS,
|
|
99
|
+
"clarification": CLARIFICATION_PATTERNS,
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Persistence layer for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module provides state persistence and recovery capabilities for
|
|
4
|
+
the Commander daemon, including atomic writes and graceful recovery.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .event_store import EventStore
|
|
8
|
+
from .state_store import StateStore
|
|
9
|
+
from .work_store import WorkStore
|
|
10
|
+
|
|
11
|
+
__all__ = ["EventStore", "StateStore", "WorkStore"]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Event persistence for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module handles persistence and recovery of the event queue/inbox,
|
|
4
|
+
including append-only event logging and efficient event removal.
|
|
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.events import Event, EventPriority, EventStatus, EventType
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventStore:
|
|
21
|
+
"""Persists and recovers events.
|
|
22
|
+
|
|
23
|
+
Provides efficient event persistence with:
|
|
24
|
+
- Batch save of all events
|
|
25
|
+
- Append-only logging for real-time persistence
|
|
26
|
+
- Safe event removal
|
|
27
|
+
- Atomic writes to prevent corruption
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
state_dir: Directory for state files
|
|
31
|
+
events_path: Path to events.json
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> store = EventStore(Path("~/.claude-mpm/commander"))
|
|
35
|
+
>>> await store.save_events(inbox)
|
|
36
|
+
>>> events = await store.load_events()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
VERSION = "1.0"
|
|
40
|
+
|
|
41
|
+
def __init__(self, state_dir: Path):
|
|
42
|
+
"""Initialize event store.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
state_dir: Directory for state files (created if needed)
|
|
46
|
+
"""
|
|
47
|
+
self.state_dir = state_dir.expanduser()
|
|
48
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
self.events_path = self.state_dir / "events.json"
|
|
51
|
+
|
|
52
|
+
logger.info(f"Initialized EventStore at {self.state_dir}")
|
|
53
|
+
|
|
54
|
+
async def save_events(self, inbox: "Inbox") -> None: # noqa: F821
|
|
55
|
+
"""Save pending events to disk.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
inbox: Inbox containing events to persist
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
IOError: If write fails
|
|
62
|
+
"""
|
|
63
|
+
# Get all pending events from event manager
|
|
64
|
+
events = inbox.events.get_pending()
|
|
65
|
+
|
|
66
|
+
data = {
|
|
67
|
+
"version": self.VERSION,
|
|
68
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
69
|
+
"events": [self._serialize_event(e) for e in events],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Run sync I/O in executor
|
|
73
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
74
|
+
None, self._atomic_write, self.events_path, data
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info(f"Saved {len(events)} events to {self.events_path}")
|
|
78
|
+
|
|
79
|
+
async def load_events(self) -> List[Event]:
|
|
80
|
+
"""Load events from disk.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of Event instances (empty if file missing or corrupt)
|
|
84
|
+
"""
|
|
85
|
+
if not self.events_path.exists():
|
|
86
|
+
logger.info("No events file found, returning empty list")
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Run sync I/O in executor
|
|
91
|
+
data = await asyncio.get_event_loop().run_in_executor(
|
|
92
|
+
None, self._read_json, self.events_path
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if data.get("version") != self.VERSION:
|
|
96
|
+
logger.warning(
|
|
97
|
+
f"Version mismatch: expected {self.VERSION}, "
|
|
98
|
+
f"got {data.get('version')}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
events = [self._deserialize_event(e) for e in data.get("events", [])]
|
|
102
|
+
|
|
103
|
+
logger.info(f"Loaded {len(events)} events from {self.events_path}")
|
|
104
|
+
return events
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Failed to load events: {e}", exc_info=True)
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
async def append_event(self, event: Event) -> None:
|
|
111
|
+
"""Append single event (for real-time persistence).
|
|
112
|
+
|
|
113
|
+
Loads existing events, adds new event, and saves atomically.
|
|
114
|
+
For high-frequency updates, consider batching with save_events().
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
event: Event to append
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
IOError: If write fails
|
|
121
|
+
"""
|
|
122
|
+
# Load existing events
|
|
123
|
+
events = await self.load_events()
|
|
124
|
+
|
|
125
|
+
# Add new event
|
|
126
|
+
events.append(event)
|
|
127
|
+
|
|
128
|
+
# Save back
|
|
129
|
+
data = {
|
|
130
|
+
"version": self.VERSION,
|
|
131
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
132
|
+
"events": [self._serialize_event(e) for e in events],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
136
|
+
None, self._atomic_write, self.events_path, data
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
logger.debug(f"Appended event {event.id} to {self.events_path}")
|
|
140
|
+
|
|
141
|
+
async def remove_event(self, event_id: str) -> None:
|
|
142
|
+
"""Remove resolved event from store.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
event_id: ID of event to remove
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
IOError: If write fails
|
|
149
|
+
"""
|
|
150
|
+
# Load existing events
|
|
151
|
+
events = await self.load_events()
|
|
152
|
+
|
|
153
|
+
# Filter out resolved event
|
|
154
|
+
filtered = [e for e in events if e.id != event_id]
|
|
155
|
+
|
|
156
|
+
if len(filtered) == len(events):
|
|
157
|
+
logger.warning(f"Event {event_id} not found in store")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Save back
|
|
161
|
+
data = {
|
|
162
|
+
"version": self.VERSION,
|
|
163
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
164
|
+
"events": [self._serialize_event(e) for e in filtered],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
168
|
+
None, self._atomic_write, self.events_path, data
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
logger.debug(f"Removed event {event_id} from {self.events_path}")
|
|
172
|
+
|
|
173
|
+
def _atomic_write(self, path: Path, data: Dict) -> None:
|
|
174
|
+
"""Write atomically (write to temp, then rename).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
path: Target file path
|
|
178
|
+
data: Data to serialize as JSON
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
IOError: If write fails
|
|
182
|
+
"""
|
|
183
|
+
# Write to temporary file in same directory
|
|
184
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
185
|
+
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
with open(fd, "w") as f:
|
|
190
|
+
json.dump(data, f, indent=2)
|
|
191
|
+
|
|
192
|
+
# Atomic rename
|
|
193
|
+
Path(tmp_path).rename(path)
|
|
194
|
+
|
|
195
|
+
logger.debug(f"Atomically wrote to {path}")
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
# Clean up temp file on error
|
|
199
|
+
try:
|
|
200
|
+
Path(tmp_path).unlink()
|
|
201
|
+
except Exception: # nosec B110
|
|
202
|
+
pass # Ignore errors during cleanup
|
|
203
|
+
raise OSError(f"Failed to write {path}: {e}") from e
|
|
204
|
+
|
|
205
|
+
def _read_json(self, path: Path) -> Dict:
|
|
206
|
+
"""Read JSON file.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: File to read
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Parsed JSON data
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
IOError: If read fails
|
|
216
|
+
"""
|
|
217
|
+
with open(path) as f:
|
|
218
|
+
return json.load(f)
|
|
219
|
+
|
|
220
|
+
def _serialize_event(self, event: Event) -> Dict[str, Any]:
|
|
221
|
+
"""Serialize Event to JSON-compatible dict.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
event: Event instance
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
JSON-serializable dict
|
|
228
|
+
"""
|
|
229
|
+
return {
|
|
230
|
+
"id": event.id,
|
|
231
|
+
"project_id": event.project_id,
|
|
232
|
+
"type": event.type.value,
|
|
233
|
+
"priority": event.priority.value,
|
|
234
|
+
"title": event.title,
|
|
235
|
+
"session_id": event.session_id,
|
|
236
|
+
"status": event.status.value,
|
|
237
|
+
"content": event.content,
|
|
238
|
+
"context": event.context,
|
|
239
|
+
"options": event.options,
|
|
240
|
+
"response": event.response,
|
|
241
|
+
"responded_at": (
|
|
242
|
+
event.responded_at.isoformat() if event.responded_at else None
|
|
243
|
+
),
|
|
244
|
+
"created_at": event.created_at.isoformat(),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def _deserialize_event(self, data: Dict[str, Any]) -> Event:
|
|
248
|
+
"""Deserialize Event from JSON dict.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
data: Serialized event data
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Event instance
|
|
255
|
+
"""
|
|
256
|
+
return Event(
|
|
257
|
+
id=data["id"],
|
|
258
|
+
project_id=data["project_id"],
|
|
259
|
+
type=EventType(data["type"]),
|
|
260
|
+
priority=EventPriority(data["priority"]),
|
|
261
|
+
title=data["title"],
|
|
262
|
+
session_id=data.get("session_id"),
|
|
263
|
+
status=EventStatus(data["status"]),
|
|
264
|
+
content=data.get("content", ""),
|
|
265
|
+
context=data.get("context", {}),
|
|
266
|
+
options=data.get("options"),
|
|
267
|
+
response=data.get("response"),
|
|
268
|
+
responded_at=(
|
|
269
|
+
datetime.fromisoformat(data["responded_at"])
|
|
270
|
+
if data.get("responded_at")
|
|
271
|
+
else None
|
|
272
|
+
),
|
|
273
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
274
|
+
)
|