claude-mpm 5.6.10__py3-none-any.whl → 5.6.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (61) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +173 -3
  3. claude_mpm/cli/parsers/commander_parser.py +41 -8
  4. claude_mpm/cli/startup.py +104 -1
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +4 -0
  22. claude_mpm/commander/core/__init__.py +10 -0
  23. claude_mpm/commander/core/block_manager.py +325 -0
  24. claude_mpm/commander/core/response_manager.py +323 -0
  25. claude_mpm/commander/daemon.py +206 -10
  26. claude_mpm/commander/env_loader.py +59 -0
  27. claude_mpm/commander/memory/__init__.py +45 -0
  28. claude_mpm/commander/memory/compression.py +347 -0
  29. claude_mpm/commander/memory/embeddings.py +230 -0
  30. claude_mpm/commander/memory/entities.py +310 -0
  31. claude_mpm/commander/memory/example_usage.py +290 -0
  32. claude_mpm/commander/memory/integration.py +325 -0
  33. claude_mpm/commander/memory/search.py +381 -0
  34. claude_mpm/commander/memory/store.py +657 -0
  35. claude_mpm/commander/registry.py +10 -4
  36. claude_mpm/commander/runtime/monitor.py +32 -2
  37. claude_mpm/commander/work/executor.py +38 -20
  38. claude_mpm/commander/workflow/event_handler.py +25 -3
  39. claude_mpm/core/claude_runner.py +143 -0
  40. claude_mpm/core/output_style_manager.py +34 -7
  41. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +22 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
  47. claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
  48. claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
  49. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  50. claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/skills/__init__.py +2 -1
  53. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  54. claude_mpm/skills/registry.py +295 -90
  55. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +5 -3
  56. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +55 -36
  57. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  58. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  61. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -4,25 +4,39 @@ This module implements REST endpoints for managing work items
4
4
  in project work queues.
5
5
  """
6
6
 
7
- from typing import List, Optional
7
+ from typing import Dict, List, Optional
8
8
 
9
- from fastapi import APIRouter, HTTPException, Query
9
+ from fastapi import APIRouter, HTTPException, Query, Request
10
10
 
11
11
  from ...models.work import WorkPriority, WorkState
12
12
  from ...work import WorkQueue
13
- from ..errors import ProjectNotFoundError
14
13
  from ..schemas import CreateWorkRequest, WorkItemResponse
15
14
 
16
15
  router = APIRouter()
17
16
 
18
17
 
19
- def _get_registry():
20
- """Get registry instance from app global."""
21
- from ..app import registry
22
-
23
- if registry is None:
18
+ def _get_registry(request: Request):
19
+ """Get registry instance from app.state."""
20
+ if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
24
21
  raise RuntimeError("Registry not initialized")
25
- return registry
22
+ return request.app.state.registry
23
+
24
+
25
+ def _get_work_queues(request: Request) -> Dict:
26
+ """Get work queues dict from app.state (shared with daemon)."""
27
+ if (
28
+ not hasattr(request.app.state, "work_queues")
29
+ or request.app.state.work_queues is None
30
+ ):
31
+ raise RuntimeError("Work queues not initialized")
32
+ return request.app.state.work_queues
33
+
34
+
35
+ def _get_daemon(request: Request):
36
+ """Get daemon instance from app.state."""
37
+ if not hasattr(request.app.state, "daemon_instance"):
38
+ return None
39
+ return request.app.state.daemon_instance
26
40
 
27
41
 
28
42
  def _work_item_to_response(work_item) -> WorkItemResponse:
@@ -51,10 +65,13 @@ def _work_item_to_response(work_item) -> WorkItemResponse:
51
65
 
52
66
 
53
67
  @router.post("/projects/{project_id}/work", response_model=WorkItemResponse)
54
- async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse:
68
+ async def add_work(
69
+ request: Request, project_id: str, work: CreateWorkRequest
70
+ ) -> WorkItemResponse:
55
71
  """Add work item to project queue.
56
72
 
57
73
  Args:
74
+ request: FastAPI request (for accessing app.state)
58
75
  project_id: Project identifier
59
76
  work: Work item creation request
60
77
 
@@ -80,22 +97,29 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
80
97
  ...
81
98
  }
82
99
  """
83
- registry = _get_registry()
100
+ registry = _get_registry(request)
101
+ work_queues = _get_work_queues(request)
102
+ daemon = _get_daemon(request)
84
103
 
85
104
  # Get project
86
- try:
87
- project = registry.get(project_id)
88
- except ProjectNotFoundError as e:
89
- raise HTTPException(status_code=404, detail=str(e)) from e
105
+ project = registry.get(project_id)
106
+ if project is None:
107
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
90
108
 
91
- # Get or create work queue for project
92
- # Note: In full implementation, this would be managed by ProjectSession
93
- # For now, we'll need to integrate with project's work queue
94
- # Access or create work queue
95
- if not hasattr(project, "_work_queue"):
96
- project._work_queue = WorkQueue(project_id)
109
+ # Get or create work queue (shared with daemon)
110
+ import logging
97
111
 
98
- queue = project._work_queue
112
+ logger = logging.getLogger(__name__)
113
+ logger.info(
114
+ f"work_queues dict id: {id(work_queues)}, keys: {list(work_queues.keys())}"
115
+ )
116
+
117
+ if project_id not in work_queues:
118
+ logger.info(f"Creating new work queue for {project_id}")
119
+ work_queues[project_id] = WorkQueue(project_id)
120
+ logger.info(f"After creation, work_queues keys: {list(work_queues.keys())}")
121
+
122
+ queue = work_queues[project_id]
99
123
 
100
124
  # Convert priority int to enum
101
125
  priority = WorkPriority(work.priority)
@@ -105,16 +129,25 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
105
129
  content=work.content, priority=priority, depends_on=work.depends_on
106
130
  )
107
131
 
132
+ # Ensure daemon has a session for this project (creates if needed)
133
+ if daemon and not daemon.sessions.get(project_id):
134
+ # Session creation will be handled by daemon's main loop
135
+ # when it detects work in the queue
136
+ pass
137
+
108
138
  return _work_item_to_response(work_item)
109
139
 
110
140
 
111
141
  @router.get("/projects/{project_id}/work", response_model=List[WorkItemResponse])
112
142
  async def list_work(
113
- project_id: str, state: Optional[str] = Query(None, description="Filter by state")
143
+ request: Request,
144
+ project_id: str,
145
+ state: Optional[str] = Query(None, description="Filter by state"),
114
146
  ) -> List[WorkItemResponse]:
115
147
  """List work items for project.
116
148
 
117
149
  Args:
150
+ request: FastAPI request (for accessing app.state)
118
151
  project_id: Project identifier
119
152
  state: Optional state filter (pending, queued, in_progress, etc.)
120
153
 
@@ -136,19 +169,20 @@ async def list_work(
136
169
  {"id": "work-1", "state": "queued", ...}
137
170
  ]
138
171
  """
139
- registry = _get_registry()
172
+ registry = _get_registry(request)
173
+ work_queues = _get_work_queues(request)
140
174
 
141
175
  # Get project
142
- try:
143
- project = registry.get(project_id)
144
- except ProjectNotFoundError as e:
145
- raise HTTPException(status_code=404, detail=str(e)) from e
176
+ project = registry.get(project_id)
177
+ if project is None:
178
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
146
179
 
147
- # Get work queue
148
- if not hasattr(project, "_work_queue"):
149
- project._work_queue = WorkQueue(project_id)
180
+ # Get work queue (shared with daemon)
181
+ if project_id not in work_queues:
182
+ # Return empty list if no work queue exists yet
183
+ return []
150
184
 
151
- queue = project._work_queue
185
+ queue = work_queues[project_id]
152
186
 
153
187
  # Parse state filter
154
188
  state_filter = None
@@ -169,10 +203,11 @@ async def list_work(
169
203
 
170
204
 
171
205
  @router.get("/projects/{project_id}/work/{work_id}", response_model=WorkItemResponse)
172
- async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
206
+ async def get_work(request: Request, project_id: str, work_id: str) -> WorkItemResponse:
173
207
  """Get work item details.
174
208
 
175
209
  Args:
210
+ request: FastAPI request (for accessing app.state)
176
211
  project_id: Project identifier
177
212
  work_id: Work item identifier
178
213
 
@@ -191,19 +226,19 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
191
226
  ...
192
227
  }
193
228
  """
194
- registry = _get_registry()
229
+ registry = _get_registry(request)
230
+ work_queues = _get_work_queues(request)
195
231
 
196
232
  # Get project
197
- try:
198
- project = registry.get(project_id)
199
- except ProjectNotFoundError as e:
200
- raise HTTPException(status_code=404, detail=str(e)) from e
233
+ project = registry.get(project_id)
234
+ if project is None:
235
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
201
236
 
202
- # Get work queue
203
- if not hasattr(project, "_work_queue"):
237
+ # Get work queue (shared with daemon)
238
+ if project_id not in work_queues:
204
239
  raise HTTPException(status_code=404, detail="Work queue not found")
205
240
 
206
- queue = project._work_queue
241
+ queue = work_queues[project_id]
207
242
 
208
243
  # Get work item
209
244
  work_item = queue.get(work_id)
@@ -214,10 +249,11 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
214
249
 
215
250
 
216
251
  @router.post("/projects/{project_id}/work/{work_id}/cancel")
217
- async def cancel_work(project_id: str, work_id: str) -> dict:
252
+ async def cancel_work(request: Request, project_id: str, work_id: str) -> dict:
218
253
  """Cancel pending work item.
219
254
 
220
255
  Args:
256
+ request: FastAPI request (for accessing app.state)
221
257
  project_id: Project identifier
222
258
  work_id: Work item identifier
223
259
 
@@ -231,19 +267,19 @@ async def cancel_work(project_id: str, work_id: str) -> dict:
231
267
  POST /api/projects/proj-123/work/work-xyz/cancel
232
268
  Response: {"status": "cancelled", "id": "work-xyz"}
233
269
  """
234
- registry = _get_registry()
270
+ registry = _get_registry(request)
271
+ work_queues = _get_work_queues(request)
235
272
 
236
273
  # Get project
237
- try:
238
- project = registry.get(project_id)
239
- except ProjectNotFoundError as e:
240
- raise HTTPException(status_code=404, detail=str(e)) from e
274
+ project = registry.get(project_id)
275
+ if project is None:
276
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
241
277
 
242
- # Get work queue
243
- if not hasattr(project, "_work_queue"):
278
+ # Get work queue (shared with daemon)
279
+ if project_id not in work_queues:
244
280
  raise HTTPException(status_code=404, detail="Work queue not found")
245
281
 
246
- queue = project._work_queue
282
+ queue = work_queues[project_id]
247
283
 
248
284
  # Cancel work item
249
285
  if not queue.cancel(work_id):
@@ -17,10 +17,14 @@ class RegisterProjectRequest(BaseModel):
17
17
 
18
18
  Attributes:
19
19
  path: Filesystem path to project directory
20
+ project_id: Optional project identifier (UUID generated if omitted)
20
21
  name: Optional display name (derived from path if omitted)
21
22
  """
22
23
 
23
24
  path: str = Field(..., description="Filesystem path to project")
25
+ project_id: Optional[str] = Field(
26
+ None, description="Project identifier (UUID generated if omitted)"
27
+ )
24
28
  name: Optional[str] = Field(
25
29
  None, description="Display name (derived from path if omitted)"
26
30
  )
@@ -5,6 +5,7 @@ import logging
5
5
  from pathlib import Path
6
6
  from typing import Optional
7
7
 
8
+ from claude_mpm.commander.env_loader import load_env
8
9
  from claude_mpm.commander.instance_manager import InstanceManager
9
10
  from claude_mpm.commander.llm.openrouter_client import (
10
11
  OpenRouterClient,
@@ -19,6 +20,9 @@ from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
19
20
 
20
21
  from .repl import CommanderREPL
21
22
 
23
+ # Load environment variables at module import
24
+ load_env()
25
+
22
26
  logger = logging.getLogger(__name__)
23
27
 
24
28
 
@@ -0,0 +1,10 @@
1
+ """Core coordination components for MPM Commander.
2
+
3
+ This module provides core components that coordinate between different
4
+ subsystems like events, work execution, and session management.
5
+ """
6
+
7
+ from .block_manager import BlockManager
8
+ from .response_manager import ResponseManager, ResponseRoute
9
+
10
+ __all__ = ["BlockManager", "ResponseManager", "ResponseRoute"]
@@ -0,0 +1,325 @@
1
+ """BlockManager for coordinating work blocking with events.
2
+
3
+ This module provides BlockManager which automatically blocks/unblocks
4
+ work items based on blocking event detection and resolution.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, List, Optional, Set
9
+
10
+ from ..events.manager import EventManager
11
+ from ..models.events import Event
12
+ from ..models.work import WorkState
13
+ from ..work.executor import WorkExecutor
14
+ from ..work.queue import WorkQueue
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class BlockManager:
20
+ """Coordinates blocking events with work execution.
21
+
22
+ Monitors blocking events and automatically blocks/unblocks work items
23
+ based on event lifecycle. Tracks event-to-work relationships for precise
24
+ unblocking when events are resolved.
25
+
26
+ Attributes:
27
+ event_manager: EventManager for querying blocking events
28
+ work_queues: Dict mapping project_id -> WorkQueue
29
+ work_executors: Dict mapping project_id -> WorkExecutor
30
+
31
+ Example:
32
+ >>> manager = BlockManager(event_manager, work_queues, work_executors)
33
+ >>> blocked = await manager.check_and_block(event)
34
+ >>> unblocked = await manager.check_and_unblock(event_id)
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ event_manager: EventManager,
40
+ work_queues: Dict[str, WorkQueue],
41
+ work_executors: Dict[str, WorkExecutor],
42
+ ):
43
+ """Initialize BlockManager.
44
+
45
+ Args:
46
+ event_manager: EventManager instance
47
+ work_queues: Dict mapping project_id -> WorkQueue
48
+ work_executors: Dict mapping project_id -> WorkExecutor
49
+
50
+ Raises:
51
+ ValueError: If any required parameter is None
52
+ """
53
+ if event_manager is None:
54
+ raise ValueError("EventManager cannot be None")
55
+ if work_queues is None:
56
+ raise ValueError("work_queues cannot be None")
57
+ if work_executors is None:
58
+ raise ValueError("work_executors cannot be None")
59
+
60
+ self.event_manager = event_manager
61
+ self.work_queues = work_queues
62
+ self.work_executors = work_executors
63
+
64
+ # Track event-to-work mapping: event_id -> set of work_ids
65
+ self._event_work_mapping: Dict[str, Set[str]] = {}
66
+
67
+ logger.debug("BlockManager initialized")
68
+
69
+ async def check_and_block(self, event: Event) -> List[str]:
70
+ """Check if event is blocking and block affected work.
71
+
72
+ When a blocking event is detected:
73
+ 1. Determine blocking scope (project or all)
74
+ 2. Find all in-progress work items in scope
75
+ 3. Block each work item via WorkExecutor
76
+ 4. Track event-to-work mapping for later unblocking
77
+
78
+ Args:
79
+ event: Event to check for blocking
80
+
81
+ Returns:
82
+ List of work item IDs that were blocked
83
+
84
+ Example:
85
+ >>> event = Event(type=EventType.ERROR, ...)
86
+ >>> blocked = await manager.check_and_block(event)
87
+ >>> print(f"Blocked {len(blocked)} work items")
88
+ """
89
+ if not event.is_blocking:
90
+ logger.debug("Event %s is not blocking, no action needed", event.id)
91
+ return []
92
+
93
+ logger.info(
94
+ "Processing blocking event %s (scope: %s): %s",
95
+ event.id,
96
+ event.blocking_scope,
97
+ event.title,
98
+ )
99
+
100
+ blocked_work_ids = []
101
+
102
+ # Determine which projects to block based on scope
103
+ if event.blocking_scope == "all":
104
+ # Block all projects
105
+ target_projects = list(self.work_queues.keys())
106
+ logger.info("Event %s blocks ALL projects", event.id)
107
+ elif event.blocking_scope == "project":
108
+ # Block only this project
109
+ target_projects = [event.project_id]
110
+ logger.info("Event %s blocks project %s only", event.id, event.project_id)
111
+ else:
112
+ logger.warning(
113
+ "Unknown blocking scope '%s' for event %s",
114
+ event.blocking_scope,
115
+ event.id,
116
+ )
117
+ return []
118
+
119
+ # Block in-progress work in target projects
120
+ for project_id in target_projects:
121
+ queue = self.work_queues.get(project_id)
122
+ if not queue:
123
+ logger.debug("No work queue for project %s", project_id)
124
+ continue
125
+
126
+ executor = self.work_executors.get(project_id)
127
+ if not executor:
128
+ logger.debug("No work executor for project %s", project_id)
129
+ continue
130
+
131
+ # Get in-progress work items
132
+ in_progress = queue.list(WorkState.IN_PROGRESS)
133
+
134
+ for work_item in in_progress:
135
+ # Block the work item
136
+ block_reason = f"Event {event.id}: {event.title}"
137
+ success = await executor.handle_block(work_item.id, block_reason)
138
+
139
+ if success:
140
+ blocked_work_ids.append(work_item.id)
141
+ logger.info(
142
+ "Blocked work item %s for project %s: %s",
143
+ work_item.id,
144
+ project_id,
145
+ block_reason,
146
+ )
147
+ else:
148
+ logger.warning(
149
+ "Failed to block work item %s for project %s",
150
+ work_item.id,
151
+ project_id,
152
+ )
153
+
154
+ # Track event-to-work mapping
155
+ if blocked_work_ids:
156
+ self._event_work_mapping[event.id] = set(blocked_work_ids)
157
+ logger.info(
158
+ "Event %s blocked %d work items: %s",
159
+ event.id,
160
+ len(blocked_work_ids),
161
+ blocked_work_ids,
162
+ )
163
+
164
+ return blocked_work_ids
165
+
166
+ async def check_and_unblock(self, event_id: str) -> List[str]:
167
+ """Unblock work items when event is resolved.
168
+
169
+ When a blocking event is resolved:
170
+ 1. Look up which work items were blocked by this event
171
+ 2. Unblock each work item via WorkExecutor
172
+ 3. Remove event-to-work mapping
173
+
174
+ Args:
175
+ event_id: ID of resolved event
176
+
177
+ Returns:
178
+ List of work item IDs that were unblocked
179
+
180
+ Example:
181
+ >>> unblocked = await manager.check_and_unblock("evt_123")
182
+ >>> print(f"Unblocked {len(unblocked)} work items")
183
+ """
184
+ # Get work items blocked by this event
185
+ work_ids = self._event_work_mapping.pop(event_id, set())
186
+
187
+ if not work_ids:
188
+ logger.debug("No work items blocked by event %s", event_id)
189
+ return []
190
+
191
+ logger.info(
192
+ "Unblocking %d work items for resolved event %s", len(work_ids), event_id
193
+ )
194
+
195
+ unblocked_work_ids = []
196
+
197
+ # Unblock each work item
198
+ for work_id in work_ids:
199
+ # Find which project this work belongs to
200
+ project_id = self._find_work_project(work_id)
201
+ if not project_id:
202
+ logger.warning("Cannot find project for work item %s", work_id)
203
+ continue
204
+
205
+ executor = self.work_executors.get(project_id)
206
+ if not executor:
207
+ logger.warning("No executor for project %s", project_id)
208
+ continue
209
+
210
+ # Unblock the work item
211
+ success = await executor.handle_unblock(work_id)
212
+
213
+ if success:
214
+ unblocked_work_ids.append(work_id)
215
+ logger.info("Unblocked work item %s", work_id)
216
+ else:
217
+ logger.warning("Failed to unblock work item %s", work_id)
218
+
219
+ return unblocked_work_ids
220
+
221
+ def _find_work_project(self, work_id: str) -> Optional[str]:
222
+ """Find which project a work item belongs to.
223
+
224
+ Args:
225
+ work_id: Work item ID to search for
226
+
227
+ Returns:
228
+ Project ID if found, None otherwise
229
+ """
230
+ for project_id, queue in self.work_queues.items():
231
+ work_item = queue.get(work_id)
232
+ if work_item:
233
+ return project_id
234
+ return None
235
+
236
+ def get_blocked_work(self, event_id: str) -> Set[str]:
237
+ """Get work items blocked by a specific event.
238
+
239
+ Args:
240
+ event_id: Event ID to check
241
+
242
+ Returns:
243
+ Set of work item IDs blocked by this event
244
+
245
+ Example:
246
+ >>> work_ids = manager.get_blocked_work("evt_123")
247
+ """
248
+ return self._event_work_mapping.get(event_id, set()).copy()
249
+
250
+ def get_blocking_events(self, work_id: str) -> List[str]:
251
+ """Get events that are blocking a specific work item.
252
+
253
+ Args:
254
+ work_id: Work item ID to check
255
+
256
+ Returns:
257
+ List of event IDs blocking this work item
258
+
259
+ Example:
260
+ >>> events = manager.get_blocking_events("work-123")
261
+ """
262
+ blocking_events = []
263
+ for event_id, work_ids in self._event_work_mapping.items():
264
+ if work_id in work_ids:
265
+ blocking_events.append(event_id)
266
+ return blocking_events
267
+
268
+ def is_work_blocked(self, work_id: str) -> bool:
269
+ """Check if a work item is currently blocked.
270
+
271
+ Args:
272
+ work_id: Work item ID to check
273
+
274
+ Returns:
275
+ True if work item is blocked by any event, False otherwise
276
+
277
+ Example:
278
+ >>> if manager.is_work_blocked("work-123"):
279
+ ... print("Work is blocked")
280
+ """
281
+ return len(self.get_blocking_events(work_id)) > 0
282
+
283
+ def clear_project_mappings(self, project_id: str) -> int:
284
+ """Clear all event-work mappings for a project.
285
+
286
+ Called when a project is shut down or reset.
287
+
288
+ Args:
289
+ project_id: Project ID to clear
290
+
291
+ Returns:
292
+ Number of work items that had mappings removed
293
+
294
+ Example:
295
+ >>> count = manager.clear_project_mappings("proj_123")
296
+ """
297
+ queue = self.work_queues.get(project_id)
298
+ if not queue:
299
+ return 0
300
+
301
+ # Get all work IDs for this project
302
+ all_work = queue.list()
303
+ project_work_ids = {w.id for w in all_work}
304
+
305
+ removed_count = 0
306
+
307
+ # Remove work items from event mappings
308
+ for event_id in list(self._event_work_mapping.keys()):
309
+ work_ids = self._event_work_mapping[event_id]
310
+ original_len = len(work_ids)
311
+
312
+ # Remove project work items
313
+ work_ids.difference_update(project_work_ids)
314
+
315
+ removed_count += original_len - len(work_ids)
316
+
317
+ # Remove empty mappings
318
+ if not work_ids:
319
+ del self._event_work_mapping[event_id]
320
+
321
+ logger.info(
322
+ "Cleared %d work item mappings for project %s", removed_count, project_id
323
+ )
324
+
325
+ return removed_count