claude-mpm 4.0.28__py3-none-any.whl → 4.0.29__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 (38) hide show
  1. claude_mpm/agents/templates/agent-manager.json +24 -0
  2. claude_mpm/agents/templates/agent-manager.md +304 -0
  3. claude_mpm/cli/__init__.py +2 -0
  4. claude_mpm/cli/commands/__init__.py +2 -0
  5. claude_mpm/cli/commands/agent_manager.py +517 -0
  6. claude_mpm/cli/commands/memory.py +1 -1
  7. claude_mpm/cli/parsers/agent_manager_parser.py +247 -0
  8. claude_mpm/cli/parsers/base_parser.py +7 -0
  9. claude_mpm/cli/shared/__init__.py +1 -1
  10. claude_mpm/constants.py +1 -0
  11. claude_mpm/core/claude_runner.py +3 -2
  12. claude_mpm/core/constants.py +2 -2
  13. claude_mpm/core/socketio_pool.py +2 -2
  14. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  15. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  16. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  17. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  18. claude_mpm/dashboard/static/css/dashboard.css +170 -0
  19. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  20. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  21. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  22. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +21 -3
  23. claude_mpm/dashboard/static/js/components/module-viewer.js +129 -1
  24. claude_mpm/dashboard/static/js/dashboard.js +116 -0
  25. claude_mpm/dashboard/static/js/socket-client.js +0 -1
  26. claude_mpm/hooks/claude_hooks/connection_pool.py +1 -1
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -1
  28. claude_mpm/services/agents/agent_builder.py +455 -0
  29. claude_mpm/services/agents/deployment/agent_template_builder.py +10 -3
  30. claude_mpm/services/memory/__init__.py +2 -0
  31. claude_mpm/services/socketio/handlers/connection.py +27 -33
  32. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/METADATA +1 -1
  33. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/RECORD +38 -33
  34. /claude_mpm/cli/shared/{command_base.py → base_command.py} +0 -0
  35. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/WHEEL +0 -0
  36. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/entry_points.txt +0 -0
  37. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/licenses/LICENSE +0 -0
  38. {claude_mpm-4.0.28.dist-info → claude_mpm-4.0.29.dist-info}/top_level.txt +0 -0
@@ -36,7 +36,7 @@ try:
36
36
  except ImportError:
37
37
  # Fallback values if constants module not available
38
38
  class NetworkConfig:
39
- SOCKETIO_PORT_RANGE = (8080, 8099)
39
+ SOCKETIO_PORT_RANGE = (8765, 8785)
40
40
  RECONNECTION_DELAY = 0.5
41
41
  SOCKET_WAIT_TIMEOUT = 1.0
42
42
 
@@ -0,0 +1,455 @@
1
+ """Agent Builder Service for programmatic agent creation and management.
2
+
3
+ This service provides comprehensive agent lifecycle management including:
4
+ - Template-based agent generation
5
+ - Agent variant creation with inheritance
6
+ - Configuration validation and sanitization
7
+ - PM instruction customization
8
+ - Integration with deployment services
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ import re
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+ from claude_mpm.core.exceptions import AgentDeploymentError
19
+ from claude_mpm.core.logging_config import get_logger
20
+
21
+
22
+ class AgentBuilderService:
23
+ """Service for building and managing agent configurations."""
24
+
25
+ # Valid agent models
26
+ VALID_MODELS = ["sonnet", "opus", "haiku"]
27
+
28
+ # Valid tool choices
29
+ VALID_TOOL_CHOICES = ["auto", "required", "any", "none"]
30
+
31
+ # Agent categories
32
+ AGENT_CATEGORIES = [
33
+ "engineering", "qa", "documentation", "ops",
34
+ "research", "security", "system", "utility"
35
+ ]
36
+
37
+ def __init__(self, templates_dir: Optional[Path] = None):
38
+ """Initialize the Agent Builder Service.
39
+
40
+ Args:
41
+ templates_dir: Path to agent templates directory
42
+ """
43
+ self.logger = get_logger(__name__)
44
+ self.templates_dir = templates_dir or Path(__file__).parent.parent.parent / "agents" / "templates"
45
+ self._template_cache = {}
46
+
47
+ def create_agent(
48
+ self,
49
+ agent_id: str,
50
+ name: str,
51
+ description: str,
52
+ model: str = "sonnet",
53
+ tool_choice: str = "auto",
54
+ instructions: Optional[str] = None,
55
+ metadata: Optional[Dict[str, Any]] = None,
56
+ base_template: Optional[str] = None
57
+ ) -> Dict[str, Any]:
58
+ """Create a new agent configuration.
59
+
60
+ Args:
61
+ agent_id: Unique identifier for the agent
62
+ name: Display name for the agent
63
+ description: Agent purpose and capabilities
64
+ model: LLM model to use (sonnet/opus/haiku)
65
+ tool_choice: Tool selection strategy
66
+ instructions: Markdown instructions content
67
+ metadata: Additional agent metadata
68
+ base_template: Optional base template to extend
69
+
70
+ Returns:
71
+ Complete agent configuration dictionary
72
+
73
+ Raises:
74
+ AgentDeploymentError: If validation fails
75
+ """
76
+ # Validate inputs
77
+ self._validate_agent_id(agent_id)
78
+ self._validate_model(model)
79
+ self._validate_tool_choice(tool_choice)
80
+
81
+ # Start with base template if provided
82
+ if base_template:
83
+ config = self._load_template(base_template)
84
+ config["id"] = agent_id # Override ID
85
+ else:
86
+ config = {}
87
+
88
+ # Build agent configuration
89
+ config.update({
90
+ "id": agent_id,
91
+ "name": name,
92
+ "prompt": f"{agent_id}.md",
93
+ "model": model,
94
+ "tool_choice": tool_choice
95
+ })
96
+
97
+ # Build metadata
98
+ agent_metadata = {
99
+ "description": description,
100
+ "version": "1.0.0",
101
+ "created": datetime.now().isoformat(),
102
+ "author": "Agent Manager",
103
+ "category": "custom"
104
+ }
105
+
106
+ if metadata:
107
+ agent_metadata.update(metadata)
108
+
109
+ config["metadata"] = agent_metadata
110
+
111
+ # Generate instructions if not provided
112
+ if instructions is None:
113
+ instructions = self._generate_default_instructions(agent_id, name, description)
114
+
115
+ return config, instructions
116
+
117
+ def create_variant(
118
+ self,
119
+ base_agent_id: str,
120
+ variant_id: str,
121
+ variant_name: str,
122
+ modifications: Dict[str, Any],
123
+ instructions_append: Optional[str] = None
124
+ ) -> Tuple[Dict[str, Any], str]:
125
+ """Create an agent variant based on an existing agent.
126
+
127
+ Args:
128
+ base_agent_id: ID of the base agent to extend
129
+ variant_id: Unique ID for the variant
130
+ variant_name: Display name for the variant
131
+ modifications: Configuration changes for the variant
132
+ instructions_append: Additional instructions to append
133
+
134
+ Returns:
135
+ Tuple of (variant configuration, variant instructions)
136
+
137
+ Raises:
138
+ AgentDeploymentError: If base agent not found or validation fails
139
+ """
140
+ # Load base agent
141
+ base_config = self._load_template(base_agent_id)
142
+ base_instructions = self._load_instructions(base_agent_id)
143
+
144
+ # Validate variant ID
145
+ self._validate_agent_id(variant_id)
146
+
147
+ # Create variant configuration
148
+ variant_config = base_config.copy()
149
+ variant_config["id"] = variant_id
150
+ variant_config["name"] = variant_name
151
+ variant_config["prompt"] = f"{variant_id}.md"
152
+
153
+ # Apply modifications
154
+ for key, value in modifications.items():
155
+ if key in ["model", "tool_choice"]:
156
+ if key == "model":
157
+ self._validate_model(value)
158
+ elif key == "tool_choice":
159
+ self._validate_tool_choice(value)
160
+ variant_config[key] = value
161
+
162
+ # Update metadata
163
+ if "metadata" not in variant_config:
164
+ variant_config["metadata"] = {}
165
+
166
+ variant_config["metadata"].update({
167
+ "base_agent": base_agent_id,
168
+ "variant": True,
169
+ "variant_created": datetime.now().isoformat()
170
+ })
171
+
172
+ # Build variant instructions
173
+ variant_instructions = f"# {variant_name} (Variant of {base_config.get('name', base_agent_id)})\n\n"
174
+ variant_instructions += base_instructions
175
+
176
+ if instructions_append:
177
+ variant_instructions += f"\n\n## Variant-Specific Instructions\n\n{instructions_append}"
178
+
179
+ return variant_config, variant_instructions
180
+
181
+ def validate_configuration(self, config: Dict[str, Any]) -> List[str]:
182
+ """Validate an agent configuration.
183
+
184
+ Args:
185
+ config: Agent configuration to validate
186
+
187
+ Returns:
188
+ List of validation errors (empty if valid)
189
+ """
190
+ errors = []
191
+
192
+ # Required fields
193
+ required_fields = ["id", "name", "prompt", "model"]
194
+ for field in required_fields:
195
+ if field not in config:
196
+ errors.append(f"Missing required field: {field}")
197
+
198
+ # Validate ID
199
+ if "id" in config:
200
+ try:
201
+ self._validate_agent_id(config["id"])
202
+ except AgentDeploymentError as e:
203
+ errors.append(str(e))
204
+
205
+ # Validate model
206
+ if "model" in config:
207
+ try:
208
+ self._validate_model(config["model"])
209
+ except AgentDeploymentError as e:
210
+ errors.append(str(e))
211
+
212
+ # Validate tool_choice
213
+ if "tool_choice" in config:
214
+ try:
215
+ self._validate_tool_choice(config["tool_choice"])
216
+ except AgentDeploymentError as e:
217
+ errors.append(str(e))
218
+
219
+ # Validate metadata
220
+ if "metadata" in config:
221
+ if not isinstance(config["metadata"], dict):
222
+ errors.append("Metadata must be a dictionary")
223
+
224
+ return errors
225
+
226
+ def generate_pm_instructions(
227
+ self,
228
+ delegation_patterns: Optional[List[str]] = None,
229
+ workflow_overrides: Optional[Dict[str, str]] = None,
230
+ custom_rules: Optional[List[str]] = None
231
+ ) -> str:
232
+ """Generate customized PM instructions.
233
+
234
+ Args:
235
+ delegation_patterns: Custom delegation patterns
236
+ workflow_overrides: Workflow sequence modifications
237
+ custom_rules: Additional PM rules
238
+
239
+ Returns:
240
+ Customized PM instructions markdown
241
+ """
242
+ instructions = "# Custom PM Instructions\n\n"
243
+
244
+ if delegation_patterns:
245
+ instructions += "## Custom Delegation Patterns\n\n"
246
+ for pattern in delegation_patterns:
247
+ instructions += f"- {pattern}\n"
248
+ instructions += "\n"
249
+
250
+ if workflow_overrides:
251
+ instructions += "## Workflow Overrides\n\n"
252
+ for workflow, override in workflow_overrides.items():
253
+ instructions += f"### {workflow}\n{override}\n\n"
254
+
255
+ if custom_rules:
256
+ instructions += "## Additional Rules\n\n"
257
+ for rule in custom_rules:
258
+ instructions += f"- {rule}\n"
259
+
260
+ return instructions
261
+
262
+ def list_available_templates(self) -> List[Dict[str, Any]]:
263
+ """List all available agent templates.
264
+
265
+ Returns:
266
+ List of template metadata dictionaries
267
+ """
268
+ templates = []
269
+
270
+ if not self.templates_dir.exists():
271
+ return templates
272
+
273
+ for template_file in self.templates_dir.glob("*.json"):
274
+ try:
275
+ with open(template_file, 'r') as f:
276
+ config = json.load(f)
277
+
278
+ # Use filename stem as ID if not specified in config
279
+ template_id = config.get("id") or template_file.stem
280
+
281
+ templates.append({
282
+ "id": template_id,
283
+ "name": config.get("name", template_id),
284
+ "description": config.get("metadata", {}).get("description"),
285
+ "category": config.get("metadata", {}).get("category"),
286
+ "version": config.get("metadata", {}).get("version"),
287
+ "file": str(template_file)
288
+ })
289
+ except Exception as e:
290
+ self.logger.warning(f"Failed to load template {template_file}: {e}")
291
+
292
+ return templates
293
+
294
+ def _validate_agent_id(self, agent_id: str) -> None:
295
+ """Validate agent ID format.
296
+
297
+ Args:
298
+ agent_id: Agent ID to validate
299
+
300
+ Raises:
301
+ AgentDeploymentError: If ID is invalid
302
+ """
303
+ if not agent_id:
304
+ raise AgentDeploymentError("Agent ID cannot be empty")
305
+
306
+ if len(agent_id) > 50:
307
+ raise AgentDeploymentError("Agent ID must be 50 characters or less")
308
+
309
+ if not re.match(r'^[a-z0-9-]+$', agent_id):
310
+ raise AgentDeploymentError(
311
+ "Agent ID must contain only lowercase letters, numbers, and hyphens"
312
+ )
313
+
314
+ def _validate_model(self, model: str) -> None:
315
+ """Validate model selection.
316
+
317
+ Args:
318
+ model: Model to validate
319
+
320
+ Raises:
321
+ AgentDeploymentError: If model is invalid
322
+ """
323
+ if model not in self.VALID_MODELS:
324
+ raise AgentDeploymentError(
325
+ f"Invalid model '{model}'. Must be one of: {', '.join(self.VALID_MODELS)}"
326
+ )
327
+
328
+ def _validate_tool_choice(self, tool_choice: str) -> None:
329
+ """Validate tool choice setting.
330
+
331
+ Args:
332
+ tool_choice: Tool choice to validate
333
+
334
+ Raises:
335
+ AgentDeploymentError: If tool choice is invalid
336
+ """
337
+ if tool_choice not in self.VALID_TOOL_CHOICES:
338
+ raise AgentDeploymentError(
339
+ f"Invalid tool_choice '{tool_choice}'. Must be one of: {', '.join(self.VALID_TOOL_CHOICES)}"
340
+ )
341
+
342
+ def _load_template(self, template_id: str) -> Dict[str, Any]:
343
+ """Load an agent template.
344
+
345
+ Args:
346
+ template_id: Template ID to load
347
+
348
+ Returns:
349
+ Template configuration dictionary
350
+
351
+ Raises:
352
+ AgentDeploymentError: If template not found
353
+ """
354
+ if template_id in self._template_cache:
355
+ return self._template_cache[template_id].copy()
356
+
357
+ template_file = self.templates_dir / f"{template_id}.json"
358
+
359
+ if not template_file.exists():
360
+ raise AgentDeploymentError(f"Template '{template_id}' not found")
361
+
362
+ try:
363
+ with open(template_file, 'r') as f:
364
+ config = json.load(f)
365
+ self._template_cache[template_id] = config
366
+ return config.copy()
367
+ except Exception as e:
368
+ raise AgentDeploymentError(f"Failed to load template '{template_id}': {e}")
369
+
370
+ def _load_instructions(self, agent_id: str) -> str:
371
+ """Load agent instructions.
372
+
373
+ Args:
374
+ agent_id: Agent ID to load instructions for
375
+
376
+ Returns:
377
+ Instructions markdown content
378
+
379
+ Raises:
380
+ AgentDeploymentError: If instructions not found
381
+ """
382
+ # Try multiple possible locations
383
+ possible_files = [
384
+ self.templates_dir / f"{agent_id}.md",
385
+ self.templates_dir / f"{agent_id}_instructions.md",
386
+ self.templates_dir / f"{agent_id}-instructions.md"
387
+ ]
388
+
389
+ for instructions_file in possible_files:
390
+ if instructions_file.exists():
391
+ try:
392
+ with open(instructions_file, 'r') as f:
393
+ return f.read()
394
+ except Exception as e:
395
+ self.logger.warning(f"Failed to read instructions from {instructions_file}: {e}")
396
+
397
+ # If no instructions found, return empty
398
+ return ""
399
+
400
+ def _generate_default_instructions(self, agent_id: str, name: str, description: str) -> str:
401
+ """Generate default agent instructions.
402
+
403
+ Args:
404
+ agent_id: Agent identifier
405
+ name: Agent display name
406
+ description: Agent description
407
+
408
+ Returns:
409
+ Default instructions markdown
410
+ """
411
+ return f"""# {name}
412
+
413
+ ## Core Identity
414
+
415
+ You are {name}, a specialized agent in the Claude MPM framework.
416
+
417
+ ## Purpose
418
+
419
+ {description}
420
+
421
+ ## Responsibilities
422
+
423
+ - Primary focus on your specialized domain
424
+ - Collaborate with other agents as needed
425
+ - Follow Claude MPM framework conventions
426
+ - Maintain high quality standards
427
+
428
+ ## Operating Principles
429
+
430
+ 1. **Expertise**: Apply deep knowledge in your domain
431
+ 2. **Efficiency**: Complete tasks effectively and quickly
432
+ 3. **Communication**: Provide clear, actionable responses
433
+ 4. **Collaboration**: Work well with other agents
434
+ 5. **Quality**: Maintain high standards in all outputs
435
+
436
+ ## Output Format
437
+
438
+ Provide structured responses with:
439
+ - Clear summaries of actions taken
440
+ - Detailed results when appropriate
441
+ - Any issues or blockers encountered
442
+ - Recommendations for next steps
443
+
444
+ ## Integration
445
+
446
+ - Follow framework patterns and conventions
447
+ - Use appropriate tools for the task
448
+ - Coordinate with PM for complex workflows
449
+ - Report completion status clearly
450
+
451
+ ---
452
+
453
+ *Agent ID: {agent_id}*
454
+ *Generated by Agent Manager*
455
+ """
@@ -60,17 +60,23 @@ class AgentTemplateBuilder:
60
60
  raise
61
61
 
62
62
  # Extract tools from template with fallback
63
+ # Handle both dict and list formats for capabilities (backward compatibility)
64
+ capabilities = template_data.get("capabilities", {})
65
+ capabilities_tools = capabilities.get("tools") if isinstance(capabilities, dict) else None
66
+
63
67
  tools = (
64
68
  template_data.get("tools")
65
- or template_data.get("capabilities", {}).get("tools")
69
+ or capabilities_tools
66
70
  or template_data.get("configuration_fields", {}).get("tools")
67
71
  or ["Read", "Write", "Edit", "Grep", "Glob", "LS"] # Default fallback
68
72
  )
69
73
 
70
74
  # Extract model from template with fallback
75
+ capabilities_model = capabilities.get("model") if isinstance(capabilities, dict) else None
76
+
71
77
  model = (
72
78
  template_data.get("model")
73
- or template_data.get("capabilities", {}).get("model")
79
+ or capabilities_model
74
80
  or template_data.get("configuration_fields", {}).get("model")
75
81
  or "sonnet" # Default fallback
76
82
  )
@@ -132,7 +138,8 @@ class AgentTemplateBuilder:
132
138
  # Extract custom metadata fields
133
139
  agent_version = template_data.get("agent_version", "1.0.0")
134
140
  agent_type = template_data.get("agent_type", "general")
135
- model_type = template_data.get("capabilities", {}).get("model", "sonnet")
141
+ # Use the capabilities_model we already extracted earlier
142
+ model_type = capabilities_model or "sonnet"
136
143
 
137
144
  # Map our model types to Claude Code format
138
145
  if model_type in ["opus", "sonnet", "haiku"]:
@@ -9,9 +9,11 @@ This module provides memory management services including:
9
9
  from .builder import MemoryBuilder
10
10
  from .optimizer import MemoryOptimizer
11
11
  from .router import MemoryRouter
12
+ from .indexed_memory import IndexedMemoryService
12
13
 
13
14
  __all__ = [
14
15
  "MemoryBuilder",
15
16
  "MemoryRouter",
16
17
  "MemoryOptimizer",
18
+ "IndexedMemoryService",
17
19
  ]
@@ -27,59 +27,53 @@ def timeout_handler(timeout_seconds: float = 5.0):
27
27
  """
28
28
  def decorator(func: Callable) -> Callable:
29
29
  @functools.wraps(func)
30
- async def wrapper(self, *args, **kwargs):
30
+ async def wrapper(*args, **kwargs):
31
31
  handler_name = func.__name__
32
32
  start_time = time.time()
33
33
 
34
34
  try:
35
35
  # Create a task with timeout
36
36
  result = await asyncio.wait_for(
37
- func(self, *args, **kwargs),
37
+ func(*args, **kwargs),
38
38
  timeout=timeout_seconds
39
39
  )
40
40
 
41
41
  elapsed = time.time() - start_time
42
42
  if elapsed > timeout_seconds * 0.8: # Warn if close to timeout
43
- self.logger.warning(
44
- f"⚠️ Handler {handler_name} took {elapsed:.2f}s "
45
- f"(close to {timeout_seconds}s timeout)"
46
- )
43
+ # Try to get logger from closure scope or fallback to print
44
+ try:
45
+ import logging
46
+ logger = logging.getLogger(__name__)
47
+ logger.warning(
48
+ f"⚠️ Handler {handler_name} took {elapsed:.2f}s "
49
+ f"(close to {timeout_seconds}s timeout)"
50
+ )
51
+ except:
52
+ print(f"⚠️ Handler {handler_name} took {elapsed:.2f}s (close to {timeout_seconds}s timeout)")
47
53
 
48
54
  return result
49
55
 
50
56
  except asyncio.TimeoutError:
51
57
  elapsed = time.time() - start_time
52
- self.logger.error(
53
- f"❌ Handler {handler_name} timed out after {elapsed:.2f}s"
54
- )
55
-
56
- # Try to send error response to client if we have their sid
57
- if args and isinstance(args[0], str): # First arg is usually sid
58
- sid = args[0]
59
- try:
60
- # Use a short timeout for error response
61
- await asyncio.wait_for(
62
- self.emit_to_client(
63
- sid,
64
- "error",
65
- {
66
- "message": f"Handler {handler_name} timed out",
67
- "handler": handler_name,
68
- "timeout": timeout_seconds
69
- }
70
- ),
71
- timeout=1.0
72
- )
73
- except:
74
- pass # Best effort error notification
58
+ # Try to get logger from closure scope or fallback to print
59
+ try:
60
+ import logging
61
+ logger = logging.getLogger(__name__)
62
+ logger.error(f"❌ Handler {handler_name} timed out after {elapsed:.2f}s")
63
+ except:
64
+ print(f"❌ Handler {handler_name} timed out after {elapsed:.2f}s")
75
65
 
76
66
  return None
77
67
 
78
68
  except Exception as e:
79
69
  elapsed = time.time() - start_time
80
- self.logger.error(
81
- f"❌ Handler {handler_name} failed after {elapsed:.2f}s: {e}"
82
- )
70
+ # Try to get logger from closure scope or fallback to print
71
+ try:
72
+ import logging
73
+ logger = logging.getLogger(__name__)
74
+ logger.error(f"❌ Handler {handler_name} failed after {elapsed:.2f}s: {e}")
75
+ except:
76
+ print(f"❌ Handler {handler_name} failed after {elapsed:.2f}s: {e}")
83
77
  raise
84
78
 
85
79
  return wrapper
@@ -325,7 +319,7 @@ class ConnectionEventHandler(BaseEventHandler):
325
319
 
326
320
  @self.sio.event
327
321
  @timeout_handler(timeout_seconds=3.0)
328
- async def disconnect(sid):
322
+ async def disconnect(sid, *args):
329
323
  """Handle client disconnection.
330
324
 
331
325
  WHY: We need to clean up client tracking when they disconnect
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.0.28
3
+ Version: 4.0.29
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team