claude-mpm 5.6.4__py3-none-any.whl → 5.6.30__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.
Files changed (103) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/cli/commands/commander.py +174 -4
  4. claude_mpm/cli/commands/skill_source.py +51 -2
  5. claude_mpm/cli/commands/skills.py +5 -3
  6. claude_mpm/cli/parsers/commander_parser.py +43 -10
  7. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  8. claude_mpm/cli/parsers/skills_parser.py +5 -0
  9. claude_mpm/cli/startup.py +140 -20
  10. claude_mpm/cli/startup_display.py +2 -1
  11. claude_mpm/commander/__init__.py +6 -0
  12. claude_mpm/commander/adapters/__init__.py +32 -3
  13. claude_mpm/commander/adapters/auggie.py +260 -0
  14. claude_mpm/commander/adapters/base.py +98 -1
  15. claude_mpm/commander/adapters/claude_code.py +32 -1
  16. claude_mpm/commander/adapters/codex.py +237 -0
  17. claude_mpm/commander/adapters/example_usage.py +310 -0
  18. claude_mpm/commander/adapters/mpm.py +389 -0
  19. claude_mpm/commander/adapters/registry.py +204 -0
  20. claude_mpm/commander/api/app.py +32 -16
  21. claude_mpm/commander/api/errors.py +21 -0
  22. claude_mpm/commander/api/routes/messages.py +11 -11
  23. claude_mpm/commander/api/routes/projects.py +20 -20
  24. claude_mpm/commander/api/routes/sessions.py +37 -26
  25. claude_mpm/commander/api/routes/work.py +86 -50
  26. claude_mpm/commander/api/schemas.py +4 -0
  27. claude_mpm/commander/chat/cli.py +42 -3
  28. claude_mpm/commander/config.py +5 -3
  29. claude_mpm/commander/core/__init__.py +10 -0
  30. claude_mpm/commander/core/block_manager.py +325 -0
  31. claude_mpm/commander/core/response_manager.py +323 -0
  32. claude_mpm/commander/daemon.py +215 -10
  33. claude_mpm/commander/env_loader.py +59 -0
  34. claude_mpm/commander/frameworks/base.py +4 -1
  35. claude_mpm/commander/instance_manager.py +124 -11
  36. claude_mpm/commander/memory/__init__.py +45 -0
  37. claude_mpm/commander/memory/compression.py +347 -0
  38. claude_mpm/commander/memory/embeddings.py +230 -0
  39. claude_mpm/commander/memory/entities.py +310 -0
  40. claude_mpm/commander/memory/example_usage.py +290 -0
  41. claude_mpm/commander/memory/integration.py +325 -0
  42. claude_mpm/commander/memory/search.py +381 -0
  43. claude_mpm/commander/memory/store.py +657 -0
  44. claude_mpm/commander/registry.py +10 -4
  45. claude_mpm/commander/runtime/monitor.py +32 -2
  46. claude_mpm/commander/work/executor.py +38 -20
  47. claude_mpm/commander/workflow/event_handler.py +25 -3
  48. claude_mpm/config/skill_sources.py +16 -0
  49. claude_mpm/core/claude_runner.py +152 -0
  50. claude_mpm/core/config.py +30 -22
  51. claude_mpm/core/config_constants.py +74 -9
  52. claude_mpm/core/constants.py +56 -12
  53. claude_mpm/core/interactive_session.py +5 -4
  54. claude_mpm/core/logging_utils.py +4 -2
  55. claude_mpm/core/network_config.py +148 -0
  56. claude_mpm/core/oneshot_session.py +7 -6
  57. claude_mpm/core/output_style_manager.py +37 -7
  58. claude_mpm/core/socketio_pool.py +13 -5
  59. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  68. claude_mpm/hooks/claude_hooks/event_handlers.py +22 -0
  69. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -6
  70. claude_mpm/hooks/claude_hooks/installer.py +43 -2
  71. claude_mpm/hooks/claude_hooks/memory_integration.py +31 -22
  72. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  76. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  79. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  80. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  81. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  82. claude_mpm/hooks/session_resume_hook.py +22 -18
  83. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  84. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  85. claude_mpm/scripts/claude-hook-handler.sh +8 -8
  86. claude_mpm/services/agents/agent_selection_service.py +2 -2
  87. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  88. claude_mpm/services/command_deployment_service.py +44 -26
  89. claude_mpm/services/pm_skills_deployer.py +3 -2
  90. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  91. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  92. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  93. claude_mpm/services/skills_deployer.py +31 -5
  94. claude_mpm/skills/__init__.py +2 -1
  95. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  96. claude_mpm/skills/registry.py +295 -90
  97. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/METADATA +5 -3
  98. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/RECORD +103 -71
  99. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/WHEEL +0 -0
  100. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/entry_points.txt +0 -0
  101. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE +0 -0
  102. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  103. {claude_mpm-5.6.4.dist-info → claude_mpm-5.6.30.dist-info}/top_level.txt +0 -0
@@ -2,9 +2,11 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ from dataclasses import dataclass
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
9
+ from claude_mpm.commander.env_loader import load_env
8
10
  from claude_mpm.commander.instance_manager import InstanceManager
9
11
  from claude_mpm.commander.llm.openrouter_client import (
10
12
  OpenRouterClient,
@@ -19,23 +21,53 @@ from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
19
21
 
20
22
  from .repl import CommanderREPL
21
23
 
24
+ # Load environment variables at module import
25
+ load_env()
26
+
22
27
  logger = logging.getLogger(__name__)
23
28
 
24
29
 
30
+ @dataclass
31
+ class CommanderCLIConfig:
32
+ """Configuration for Commander CLI mode.
33
+
34
+ Attributes:
35
+ summarize_responses: Whether to use LLM to summarize instance responses
36
+ port: Port for internal services (reserved for future use)
37
+ state_dir: Directory for state persistence (optional)
38
+
39
+ Example:
40
+ >>> config = CommanderCLIConfig(summarize_responses=False)
41
+ """
42
+
43
+ summarize_responses: bool = True
44
+ port: int = 8765
45
+ state_dir: Optional[Path] = None
46
+
47
+
25
48
  async def run_commander(
26
49
  port: int = 8765,
27
50
  state_dir: Optional[Path] = None,
51
+ config: Optional[CommanderCLIConfig] = None,
28
52
  ) -> None:
29
53
  """Run Commander in interactive mode.
30
54
 
31
55
  Args:
32
56
  port: Port for internal services (unused currently).
33
57
  state_dir: Directory for state persistence (optional).
58
+ config: Commander CLI configuration (optional, uses defaults if None).
34
59
 
35
60
  Example:
36
61
  >>> asyncio.run(run_commander())
37
62
  # Starts interactive Commander REPL
63
+ >>> config = CommanderCLIConfig(summarize_responses=False)
64
+ >>> asyncio.run(run_commander(config=config))
65
+ # Starts Commander without response summarization
38
66
  """
67
+ # Use default config if not provided
68
+ if config is None:
69
+ config = CommanderCLIConfig(port=port, state_dir=state_dir)
70
+
39
71
  # Setup logging
40
72
  logging.basicConfig(
41
73
  level=logging.INFO,
@@ -57,8 +89,8 @@ async def run_commander(
57
89
  # Try to initialize LLM client (optional)
58
90
  llm_client: Optional[OpenRouterClient] = None
59
91
  try:
60
- config = OpenRouterConfig()
61
- llm_client = OpenRouterClient(config)
92
+ llm_config = OpenRouterConfig()
93
+ llm_client = OpenRouterClient(llm_config)
62
94
  logger.info("LLM client initialized")
63
95
  except ValueError as e:
64
96
  logger.warning(f"LLM client not available: {e}")
@@ -68,7 +100,14 @@ async def run_commander(
68
100
  output_relay: Optional[OutputRelay] = None
69
101
  if llm_client:
70
102
  try:
71
- summarizer = OutputSummarizer(llm_client)
103
+ # Only create summarizer if summarize_responses is enabled
104
+ summarizer = None
105
+ if config.summarize_responses:
106
+ summarizer = OutputSummarizer(llm_client)
107
+ logger.info("Response summarization enabled")
108
+ else:
109
+ logger.info("Response summarization disabled")
110
+
72
111
  handler = OutputHandler(orchestrator, summarizer)
73
112
  formatter = OutputFormatter()
74
113
  output_relay = OutputRelay(handler, formatter)
@@ -14,28 +14,30 @@ class DaemonConfig:
14
14
 
15
15
  Attributes:
16
16
  host: API server bind address
17
- port: API server port
17
+ port: API server port (default: 8766 from NetworkPorts.COMMANDER_DEFAULT)
18
18
  log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
19
19
  state_dir: Directory for state persistence
20
20
  max_projects: Maximum concurrent projects
21
21
  healthcheck_interval: Healthcheck interval in seconds
22
22
  save_interval: State persistence interval in seconds
23
23
  poll_interval: Event polling interval in seconds
24
+ summarize_responses: Whether to use LLM to summarize instance responses
24
25
 
25
26
  Example:
26
- >>> config = DaemonConfig(port=8765, log_level="DEBUG")
27
+ >>> config = DaemonConfig(port=8766, log_level="DEBUG")
27
28
  >>> config.state_dir
28
29
  PosixPath('/Users/user/.claude-mpm/commander')
29
30
  """
30
31
 
31
32
  host: str = "127.0.0.1"
32
- port: int = 8765
33
+ port: int = 8766 # Default commander port (from network_config.NetworkPorts.COMMANDER_DEFAULT)
33
34
  log_level: str = "INFO"
34
35
  state_dir: Path = Path.home() / ".claude-mpm" / "commander"
35
36
  max_projects: int = 10
36
37
  healthcheck_interval: int = 30
37
38
  save_interval: int = 30
38
39
  poll_interval: float = 2.0
40
+ summarize_responses: bool = True
39
41
 
40
42
  def __post_init__(self):
41
43
  """Ensure state_dir is a Path object and create if needed."""
@@ -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