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,121 @@
|
|
|
1
|
+
"""Event models for MPM Commander (Phase 2).
|
|
2
|
+
|
|
3
|
+
This module defines the complete event model supporting all event types,
|
|
4
|
+
priorities, and statuses for the inbox system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _utc_now() -> datetime:
|
|
14
|
+
"""Return current UTC time with timezone info."""
|
|
15
|
+
return datetime.now(timezone.utc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventType(Enum):
|
|
19
|
+
"""Types of events that can be raised by projects."""
|
|
20
|
+
|
|
21
|
+
DECISION_NEEDED = "decision_needed" # Tool asking which option
|
|
22
|
+
CLARIFICATION = "clarification" # Tool needs more info
|
|
23
|
+
ERROR = "error" # Something failed
|
|
24
|
+
APPROVAL = "approval" # Destructive action pending
|
|
25
|
+
TASK_COMPLETE = "task_complete" # Work item finished
|
|
26
|
+
MILESTONE = "milestone" # Significant progress
|
|
27
|
+
STATUS = "status" # General update
|
|
28
|
+
PROJECT_IDLE = "project_idle" # Project has no work
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EventPriority(Enum):
|
|
32
|
+
"""Priority levels for events, ordered from highest to lowest."""
|
|
33
|
+
|
|
34
|
+
CRITICAL = "critical" # Blocking all progress
|
|
35
|
+
HIGH = "high" # Blocking this project
|
|
36
|
+
NORMAL = "normal" # Needs response, not blocking
|
|
37
|
+
LOW = "low" # Informational, can batch
|
|
38
|
+
INFO = "info" # No response needed
|
|
39
|
+
|
|
40
|
+
def __lt__(self, other: "EventPriority") -> bool:
|
|
41
|
+
"""Enable priority comparison for sorting."""
|
|
42
|
+
order = [self.CRITICAL, self.HIGH, self.NORMAL, self.LOW, self.INFO]
|
|
43
|
+
return order.index(self) < order.index(other)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EventStatus(Enum):
|
|
47
|
+
"""Lifecycle status of an event."""
|
|
48
|
+
|
|
49
|
+
PENDING = "pending" # Awaiting response
|
|
50
|
+
ACKNOWLEDGED = "acknowledged" # Seen but not resolved
|
|
51
|
+
RESOLVED = "resolved" # Response provided
|
|
52
|
+
DISMISSED = "dismissed" # Intentionally ignored
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Default priority by event type
|
|
56
|
+
DEFAULT_PRIORITIES: Dict[EventType, EventPriority] = {
|
|
57
|
+
EventType.ERROR: EventPriority.CRITICAL,
|
|
58
|
+
EventType.DECISION_NEEDED: EventPriority.HIGH,
|
|
59
|
+
EventType.APPROVAL: EventPriority.HIGH,
|
|
60
|
+
EventType.CLARIFICATION: EventPriority.NORMAL,
|
|
61
|
+
EventType.TASK_COMPLETE: EventPriority.LOW,
|
|
62
|
+
EventType.MILESTONE: EventPriority.LOW,
|
|
63
|
+
EventType.STATUS: EventPriority.INFO,
|
|
64
|
+
EventType.PROJECT_IDLE: EventPriority.INFO,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Which event types block progress and their scope
|
|
69
|
+
BLOCKING_EVENTS: Dict[EventType, str] = {
|
|
70
|
+
EventType.ERROR: "all", # Blocks all projects
|
|
71
|
+
EventType.DECISION_NEEDED: "project", # Blocks this project
|
|
72
|
+
EventType.APPROVAL: "project", # Blocks this project
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Event:
|
|
78
|
+
"""Represents an event in the MPM Commander system.
|
|
79
|
+
|
|
80
|
+
Events are raised by projects to communicate with the user or
|
|
81
|
+
system about state changes, decisions needed, or progress updates.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
id: Unique event identifier
|
|
85
|
+
project_id: ID of project that raised this event
|
|
86
|
+
type: Type of event (decision, error, status, etc.)
|
|
87
|
+
priority: Urgency level
|
|
88
|
+
title: Short summary of the event
|
|
89
|
+
session_id: Optional session ID if event is session-specific
|
|
90
|
+
status: Current lifecycle status
|
|
91
|
+
content: Detailed event message or description
|
|
92
|
+
context: Additional structured data about the event
|
|
93
|
+
options: For DECISION_NEEDED events, list of choices
|
|
94
|
+
response: User's response to the event
|
|
95
|
+
responded_at: When the response was recorded
|
|
96
|
+
created_at: When the event was created
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
id: str
|
|
100
|
+
project_id: str
|
|
101
|
+
type: EventType
|
|
102
|
+
priority: EventPriority
|
|
103
|
+
title: str
|
|
104
|
+
session_id: Optional[str] = None
|
|
105
|
+
status: EventStatus = EventStatus.PENDING
|
|
106
|
+
content: str = ""
|
|
107
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
108
|
+
options: Optional[List[str]] = None
|
|
109
|
+
response: Optional[str] = None
|
|
110
|
+
responded_at: Optional[datetime] = None
|
|
111
|
+
created_at: datetime = field(default_factory=_utc_now)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def is_blocking(self) -> bool:
|
|
115
|
+
"""Check if this event blocks progress."""
|
|
116
|
+
return self.type in BLOCKING_EVENTS and self.status == EventStatus.PENDING
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def blocking_scope(self) -> Optional[str]:
|
|
120
|
+
"""Get the scope this event blocks ('all' or 'project')."""
|
|
121
|
+
return BLOCKING_EVENTS.get(self.type)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Project data models for MPM Commander.
|
|
2
|
+
|
|
3
|
+
This module defines the core data structures for managing projects,
|
|
4
|
+
tool sessions, and conversation threads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _utc_now() -> datetime:
|
|
14
|
+
"""Return current UTC time with timezone awareness."""
|
|
15
|
+
return datetime.now(timezone.utc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectState(Enum):
|
|
19
|
+
"""Project execution state.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
IDLE: No pending work in queue
|
|
23
|
+
WORKING: Currently executing a work item
|
|
24
|
+
BLOCKED: Waiting on human input or approval
|
|
25
|
+
PAUSED: Manually paused by user
|
|
26
|
+
ERROR: Encountered unrecoverable error
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
IDLE = "idle"
|
|
30
|
+
WORKING = "working"
|
|
31
|
+
BLOCKED = "blocked"
|
|
32
|
+
PAUSED = "paused"
|
|
33
|
+
ERROR = "error"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ToolSession:
|
|
38
|
+
"""Tool runtime session (Claude Code, Codex, Aider, etc.).
|
|
39
|
+
|
|
40
|
+
Each session runs in an isolated tmux pane for output capture.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
id: Unique session identifier
|
|
44
|
+
project_id: Parent project ID
|
|
45
|
+
runtime: Tool runtime name (e.g., "claude-code", "aider")
|
|
46
|
+
tmux_target: Tmux window:pane identifier (e.g., "commander:proj-a-cc")
|
|
47
|
+
status: Current session status
|
|
48
|
+
output_buffer: Captured output from tool
|
|
49
|
+
created_at: Session creation timestamp
|
|
50
|
+
last_output_at: Last output received timestamp
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> session = ToolSession(
|
|
54
|
+
... id="sess-123",
|
|
55
|
+
... project_id="proj-abc",
|
|
56
|
+
... runtime="claude-code",
|
|
57
|
+
... tmux_target="commander:proj-abc-cc"
|
|
58
|
+
... )
|
|
59
|
+
>>> session.status
|
|
60
|
+
'initializing'
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
id: str
|
|
64
|
+
project_id: str
|
|
65
|
+
runtime: str
|
|
66
|
+
tmux_target: str
|
|
67
|
+
status: str = "initializing"
|
|
68
|
+
output_buffer: str = ""
|
|
69
|
+
created_at: datetime = field(default_factory=_utc_now)
|
|
70
|
+
last_output_at: Optional[datetime] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ThreadMessage:
|
|
75
|
+
"""Conversation thread message.
|
|
76
|
+
|
|
77
|
+
Represents a single message in the project's conversation history,
|
|
78
|
+
which can come from user, assistant, system, or tool outputs.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
id: Unique message identifier
|
|
82
|
+
role: Message sender role
|
|
83
|
+
content: Message content
|
|
84
|
+
session_id: Associated tool session (if from tool)
|
|
85
|
+
event_id: Associated event (Phase 2)
|
|
86
|
+
timestamp: Message creation timestamp
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> msg = ThreadMessage(
|
|
90
|
+
... id="msg-1",
|
|
91
|
+
... role="user",
|
|
92
|
+
... content="Fix the login bug"
|
|
93
|
+
... )
|
|
94
|
+
>>> msg.role
|
|
95
|
+
'user'
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
id: str
|
|
99
|
+
role: str # user, assistant, system, tool
|
|
100
|
+
content: str
|
|
101
|
+
session_id: Optional[str] = None
|
|
102
|
+
event_id: Optional[str] = None
|
|
103
|
+
timestamp: datetime = field(default_factory=_utc_now)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Project:
|
|
108
|
+
"""Project context and state management.
|
|
109
|
+
|
|
110
|
+
Maintains isolated state for a single project including:
|
|
111
|
+
- Execution state and work queue
|
|
112
|
+
- Tool sessions (Claude Code, Aider, etc.)
|
|
113
|
+
- Conversation thread
|
|
114
|
+
- Events and history (Phase 2)
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
id: Unique project identifier (UUID)
|
|
118
|
+
path: Absolute filesystem path to project
|
|
119
|
+
name: Human-readable project name
|
|
120
|
+
state: Current execution state
|
|
121
|
+
state_reason: Optional reason for state (e.g., error message)
|
|
122
|
+
config_loaded: Whether .claude-mpm/ config loaded
|
|
123
|
+
config: Loaded configuration dict
|
|
124
|
+
sessions: Active tool sessions by session_id
|
|
125
|
+
work_queue: Pending work items (Phase 3)
|
|
126
|
+
active_work: Currently executing work item
|
|
127
|
+
completed_work: History of completed work items
|
|
128
|
+
pending_events: Events pending processing (Phase 2)
|
|
129
|
+
event_history: Processed events (Phase 2)
|
|
130
|
+
thread: Conversation message history
|
|
131
|
+
created_at: Project registration timestamp
|
|
132
|
+
last_activity: Last activity timestamp (updated on any change)
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> from pathlib import Path
|
|
136
|
+
>>> project = Project(
|
|
137
|
+
... id="proj-abc123",
|
|
138
|
+
... path="/Users/masa/Projects/my-app",
|
|
139
|
+
... name="my-app"
|
|
140
|
+
... )
|
|
141
|
+
>>> project.state
|
|
142
|
+
<ProjectState.IDLE: 'idle'>
|
|
143
|
+
>>> project.state = ProjectState.WORKING
|
|
144
|
+
>>> project.state_reason = "Processing ticket #123"
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
id: str
|
|
148
|
+
path: str
|
|
149
|
+
name: str
|
|
150
|
+
state: ProjectState = ProjectState.IDLE
|
|
151
|
+
state_reason: Optional[str] = None
|
|
152
|
+
config_loaded: bool = False
|
|
153
|
+
config: Optional[Dict[str, Any]] = None
|
|
154
|
+
sessions: Dict[str, ToolSession] = field(default_factory=dict)
|
|
155
|
+
work_queue: List[Any] = field(default_factory=list)
|
|
156
|
+
active_work: Optional[Any] = None
|
|
157
|
+
completed_work: List[Any] = field(default_factory=list)
|
|
158
|
+
pending_events: List[Any] = field(default_factory=list)
|
|
159
|
+
event_history: List[Any] = field(default_factory=list)
|
|
160
|
+
thread: List[ThreadMessage] = field(default_factory=list)
|
|
161
|
+
created_at: datetime = field(default_factory=_utc_now)
|
|
162
|
+
last_activity: datetime = field(default_factory=_utc_now)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Work item models for MPM Commander work queue.
|
|
2
|
+
|
|
3
|
+
This module defines work items that represent tasks to be executed
|
|
4
|
+
by the Commander daemon across multiple projects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _utc_now() -> datetime:
|
|
14
|
+
"""Return current UTC time with timezone info."""
|
|
15
|
+
return datetime.now(timezone.utc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkState(Enum):
|
|
19
|
+
"""Lifecycle states of a work item."""
|
|
20
|
+
|
|
21
|
+
PENDING = "pending" # Created but not yet queued
|
|
22
|
+
QUEUED = "queued" # In queue, waiting for execution
|
|
23
|
+
IN_PROGRESS = "in_progress" # Currently being executed
|
|
24
|
+
BLOCKED = "blocked" # Paused due to blocking event
|
|
25
|
+
COMPLETED = "completed" # Successfully finished
|
|
26
|
+
FAILED = "failed" # Failed with error
|
|
27
|
+
CANCELLED = "cancelled" # Manually cancelled
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkPriority(Enum):
|
|
31
|
+
"""Priority levels for work items, ordered from highest to lowest."""
|
|
32
|
+
|
|
33
|
+
CRITICAL = 4 # Execute immediately
|
|
34
|
+
HIGH = 3 # Execute as soon as possible
|
|
35
|
+
MEDIUM = 2 # Normal priority
|
|
36
|
+
LOW = 1 # Execute when idle
|
|
37
|
+
|
|
38
|
+
def __lt__(self, other: "WorkPriority") -> bool:
|
|
39
|
+
"""Enable priority comparison for sorting (higher value = higher priority)."""
|
|
40
|
+
if not isinstance(other, WorkPriority):
|
|
41
|
+
return NotImplemented
|
|
42
|
+
return self.value < other.value
|
|
43
|
+
|
|
44
|
+
def __le__(self, other: "WorkPriority") -> bool:
|
|
45
|
+
"""Less than or equal comparison."""
|
|
46
|
+
if not isinstance(other, WorkPriority):
|
|
47
|
+
return NotImplemented
|
|
48
|
+
return self.value <= other.value
|
|
49
|
+
|
|
50
|
+
def __gt__(self, other: "WorkPriority") -> bool:
|
|
51
|
+
"""Greater than comparison."""
|
|
52
|
+
if not isinstance(other, WorkPriority):
|
|
53
|
+
return NotImplemented
|
|
54
|
+
return self.value > other.value
|
|
55
|
+
|
|
56
|
+
def __ge__(self, other: "WorkPriority") -> bool:
|
|
57
|
+
"""Greater than or equal comparison."""
|
|
58
|
+
if not isinstance(other, WorkPriority):
|
|
59
|
+
return NotImplemented
|
|
60
|
+
return self.value >= other.value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class WorkItem:
|
|
65
|
+
"""Represents a work item to be executed by the Commander.
|
|
66
|
+
|
|
67
|
+
Work items are queued tasks that will be executed by RuntimeExecutor.
|
|
68
|
+
They support priority-based execution and dependency chains.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
id: Unique work item identifier
|
|
72
|
+
project_id: ID of project this work belongs to
|
|
73
|
+
content: The message/task to send to Claude
|
|
74
|
+
state: Current lifecycle state
|
|
75
|
+
priority: Execution priority
|
|
76
|
+
created_at: When the work item was created
|
|
77
|
+
started_at: When execution started (if IN_PROGRESS or later)
|
|
78
|
+
completed_at: When execution completed (if COMPLETED/FAILED)
|
|
79
|
+
result: Result output on completion
|
|
80
|
+
error: Error message if FAILED
|
|
81
|
+
depends_on: List of work item IDs that must complete first
|
|
82
|
+
metadata: Additional structured data about the work
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> work = WorkItem(
|
|
86
|
+
... id="work-123",
|
|
87
|
+
... project_id="proj-abc",
|
|
88
|
+
... content="Implement OAuth2 authentication",
|
|
89
|
+
... priority=WorkPriority.HIGH
|
|
90
|
+
... )
|
|
91
|
+
>>> work.can_start({"work-122"}) # Check if dependencies satisfied
|
|
92
|
+
True
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
id: str
|
|
96
|
+
project_id: str
|
|
97
|
+
content: str
|
|
98
|
+
state: WorkState = WorkState.PENDING
|
|
99
|
+
priority: WorkPriority = WorkPriority.MEDIUM
|
|
100
|
+
created_at: datetime = field(default_factory=_utc_now)
|
|
101
|
+
started_at: Optional[datetime] = None
|
|
102
|
+
completed_at: Optional[datetime] = None
|
|
103
|
+
result: Optional[str] = None
|
|
104
|
+
error: Optional[str] = None
|
|
105
|
+
depends_on: List[str] = field(default_factory=list)
|
|
106
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
def can_start(self, completed_ids: set[str]) -> bool:
|
|
109
|
+
"""Check if dependencies are satisfied.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
completed_ids: Set of work item IDs that have completed
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if all dependencies in completed_ids, False otherwise
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> work = WorkItem(id="w1", project_id="p1", content="Task",
|
|
119
|
+
... depends_on=["w0"])
|
|
120
|
+
>>> work.can_start({"w0"})
|
|
121
|
+
True
|
|
122
|
+
>>> work.can_start(set())
|
|
123
|
+
False
|
|
124
|
+
"""
|
|
125
|
+
if not self.depends_on:
|
|
126
|
+
return True
|
|
127
|
+
return all(dep_id in completed_ids for dep_id in self.depends_on)
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
130
|
+
"""Serialize for persistence.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Dictionary representation suitable for JSON serialization
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> work = WorkItem(id="w1", project_id="p1", content="Task")
|
|
137
|
+
>>> data = work.to_dict()
|
|
138
|
+
>>> data["id"]
|
|
139
|
+
'w1'
|
|
140
|
+
"""
|
|
141
|
+
return {
|
|
142
|
+
"id": self.id,
|
|
143
|
+
"project_id": self.project_id,
|
|
144
|
+
"content": self.content,
|
|
145
|
+
"state": self.state.value,
|
|
146
|
+
"priority": self.priority.value,
|
|
147
|
+
"created_at": self.created_at.isoformat(),
|
|
148
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
149
|
+
"completed_at": (
|
|
150
|
+
self.completed_at.isoformat() if self.completed_at else None
|
|
151
|
+
),
|
|
152
|
+
"result": self.result,
|
|
153
|
+
"error": self.error,
|
|
154
|
+
"depends_on": self.depends_on,
|
|
155
|
+
"metadata": self.metadata,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_dict(cls, data: Dict[str, Any]) -> "WorkItem":
|
|
160
|
+
"""Deserialize from persistence.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
data: Dictionary from to_dict()
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Reconstructed WorkItem instance
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If required fields missing or invalid
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> data = {"id": "w1", "project_id": "p1", "content": "Task",
|
|
173
|
+
... "state": "pending", "priority": 2}
|
|
174
|
+
>>> work = WorkItem.from_dict(data)
|
|
175
|
+
>>> work.id
|
|
176
|
+
'w1'
|
|
177
|
+
"""
|
|
178
|
+
# Parse enums
|
|
179
|
+
try:
|
|
180
|
+
state = WorkState(data["state"])
|
|
181
|
+
except (KeyError, ValueError) as e:
|
|
182
|
+
raise ValueError(f"Invalid or missing state: {e}") from e
|
|
183
|
+
|
|
184
|
+
# Priority can be int or string
|
|
185
|
+
priority_val = data.get("priority", 2)
|
|
186
|
+
if isinstance(priority_val, str):
|
|
187
|
+
priority = WorkPriority[priority_val.upper()]
|
|
188
|
+
else:
|
|
189
|
+
priority = WorkPriority(priority_val)
|
|
190
|
+
|
|
191
|
+
# Parse datetimes
|
|
192
|
+
def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]:
|
|
193
|
+
if not dt_str:
|
|
194
|
+
return None
|
|
195
|
+
return datetime.fromisoformat(dt_str)
|
|
196
|
+
|
|
197
|
+
created_at = parse_datetime(data.get("created_at"))
|
|
198
|
+
if not created_at:
|
|
199
|
+
created_at = _utc_now()
|
|
200
|
+
|
|
201
|
+
return cls(
|
|
202
|
+
id=data["id"],
|
|
203
|
+
project_id=data["project_id"],
|
|
204
|
+
content=data["content"],
|
|
205
|
+
state=state,
|
|
206
|
+
priority=priority,
|
|
207
|
+
created_at=created_at,
|
|
208
|
+
started_at=parse_datetime(data.get("started_at")),
|
|
209
|
+
completed_at=parse_datetime(data.get("completed_at")),
|
|
210
|
+
result=data.get("result"),
|
|
211
|
+
error=data.get("error"),
|
|
212
|
+
depends_on=data.get("depends_on", []),
|
|
213
|
+
metadata=data.get("metadata", {}),
|
|
214
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Output parsing module for detecting events in tool output."""
|
|
2
|
+
|
|
3
|
+
from .extractor import (
|
|
4
|
+
extract_action_details,
|
|
5
|
+
extract_error_context,
|
|
6
|
+
extract_options,
|
|
7
|
+
strip_code_blocks,
|
|
8
|
+
)
|
|
9
|
+
from .output_parser import OutputParser, ParseResult
|
|
10
|
+
from .patterns import ALL_PATTERNS
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ALL_PATTERNS",
|
|
14
|
+
"OutputParser",
|
|
15
|
+
"ParseResult",
|
|
16
|
+
"extract_action_details",
|
|
17
|
+
"extract_error_context",
|
|
18
|
+
"extract_options",
|
|
19
|
+
"strip_code_blocks",
|
|
20
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Extract options and context from matched patterns."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_options(content: str) -> Optional[List[str]]:
|
|
8
|
+
"""Extract options from decision content.
|
|
9
|
+
|
|
10
|
+
Supports multiple formats:
|
|
11
|
+
- Numbered lists: "1. Option A\n2. Option B"
|
|
12
|
+
- Bullet lists: "• Option A\n- Option B"
|
|
13
|
+
- Inline options: "(option1/option2/option3)"
|
|
14
|
+
- Y/n style: "[Y/n]", "[yes/no]"
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
content: Content to extract options from
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of option strings if found, None otherwise
|
|
21
|
+
"""
|
|
22
|
+
# Numbered list: "1. Option A\n2. Option B"
|
|
23
|
+
numbered = re.findall(r"^\s*\d+[\.\)]\s*(.+)$", content, re.MULTILINE)
|
|
24
|
+
if numbered:
|
|
25
|
+
return [opt.strip() for opt in numbered]
|
|
26
|
+
|
|
27
|
+
# Bullet list: "• Option A\n- Option B"
|
|
28
|
+
bullets = re.findall(r"^\s*[-•*]\s*(.+)$", content, re.MULTILINE)
|
|
29
|
+
if bullets:
|
|
30
|
+
return [opt.strip() for opt in bullets]
|
|
31
|
+
|
|
32
|
+
# Inline options: "(option1/option2/option3)"
|
|
33
|
+
inline = re.search(r"\(([^)]+/[^)]+)\)", content)
|
|
34
|
+
if inline:
|
|
35
|
+
return [o.strip() for o in inline.group(1).split("/")]
|
|
36
|
+
|
|
37
|
+
# Y/n style
|
|
38
|
+
yn = re.search(r"\[(Y/n|y/N|yes/no|Yes/No)\]", content, re.I)
|
|
39
|
+
if yn:
|
|
40
|
+
parts = yn.group(1).split("/")
|
|
41
|
+
return [p.strip() for p in parts]
|
|
42
|
+
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_error_context(
|
|
47
|
+
content: str, match_start: int, match_end: int, context_lines: int = 5
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""Extract surrounding context for error.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
content: Full content text
|
|
53
|
+
match_start: Character position where match starts
|
|
54
|
+
match_end: Character position where match ends
|
|
55
|
+
context_lines: Number of lines to include before/after error
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict with surrounding lines and error position info
|
|
59
|
+
"""
|
|
60
|
+
lines = content.split("\n")
|
|
61
|
+
|
|
62
|
+
# Find which line the match is on
|
|
63
|
+
char_count = 0
|
|
64
|
+
match_line = 0
|
|
65
|
+
for i, line in enumerate(lines):
|
|
66
|
+
char_count += len(line) + 1 # +1 for newline
|
|
67
|
+
if char_count > match_start:
|
|
68
|
+
match_line = i
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Get context lines
|
|
72
|
+
start = max(0, match_line - context_lines)
|
|
73
|
+
end = min(len(lines), match_line + context_lines + 1)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"surrounding_lines": lines[start:end],
|
|
77
|
+
"error_line_index": match_line - start,
|
|
78
|
+
"total_lines": len(lines),
|
|
79
|
+
"match_line": match_line,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_action_details(content: str, match: re.Match) -> Dict[str, Any]:
|
|
84
|
+
"""Extract details about an action from approval match.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
content: Full content text
|
|
88
|
+
match: Regex match object for the approval pattern
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with action type, target, and reversibility info
|
|
92
|
+
"""
|
|
93
|
+
groups = match.groups()
|
|
94
|
+
# Use the first captured group if available, otherwise the full match
|
|
95
|
+
action_text = groups[0] if groups else match.group(0)
|
|
96
|
+
|
|
97
|
+
# Try to identify the action type from the full matched text
|
|
98
|
+
full_match = match.group(0)
|
|
99
|
+
action_type = "unknown"
|
|
100
|
+
if re.search(r"delete|remove", full_match, re.I):
|
|
101
|
+
action_type = "delete"
|
|
102
|
+
elif re.search(r"overwrite|modify|change", full_match, re.I):
|
|
103
|
+
action_type = "modify"
|
|
104
|
+
elif re.search(r"create|add", full_match, re.I):
|
|
105
|
+
action_type = "create"
|
|
106
|
+
|
|
107
|
+
# Check if reversible
|
|
108
|
+
reversible = "cannot be undone" not in content.lower()
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"action": action_type,
|
|
112
|
+
"target": action_text.strip(),
|
|
113
|
+
"reversible": reversible,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def strip_code_blocks(content: str) -> str:
|
|
118
|
+
"""Remove code blocks to avoid false positives.
|
|
119
|
+
|
|
120
|
+
Replaces fenced code blocks and inline code with placeholders
|
|
121
|
+
to prevent pattern matching inside code.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
content: Content to strip code blocks from
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Content with code blocks replaced by placeholders
|
|
128
|
+
"""
|
|
129
|
+
# Remove fenced code blocks
|
|
130
|
+
content = re.sub(r"```[\s\S]*?```", "[CODE_BLOCK]", content)
|
|
131
|
+
# Remove inline code and return
|
|
132
|
+
return re.sub(r"`[^`]+`", "[INLINE_CODE]", content)
|