claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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/templates/research.json +39 -13
- claude_mpm/cli/__init__.py +2 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/configure.py +1221 -0
- claude_mpm/cli/commands/configure_tui.py +1921 -0
- claude_mpm/cli/commands/tickets.py +365 -784
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/configure_parser.py +119 -0
- claude_mpm/cli/startup_logging.py +39 -12
- claude_mpm/constants.py +1 -0
- claude_mpm/core/output_style_manager.py +24 -0
- claude_mpm/core/socketio_pool.py +35 -3
- claude_mpm/core/unified_agent_registry.py +46 -15
- claude_mpm/dashboard/static/css/connection-status.css +370 -0
- claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
- claude_mpm/dashboard/static/js/connection-manager.js +536 -0
- claude_mpm/dashboard/templates/index.html +11 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
- claude_mpm/services/event_bus/direct_relay.py +173 -0
- claude_mpm/services/infrastructure/__init__.py +31 -5
- claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
- claude_mpm/services/infrastructure/monitoring/base.py +130 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +218 -0
- claude_mpm/services/infrastructure/monitoring/process.py +342 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
- claude_mpm/services/infrastructure/monitoring/service.py +367 -0
- claude_mpm/services/infrastructure/monitoring.py +67 -1030
- claude_mpm/services/project/analyzer.py +13 -4
- claude_mpm/services/project/analyzer_refactored.py +450 -0
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +410 -0
- claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/connection_manager.py +516 -0
- claude_mpm/services/socketio/server/core.py +63 -0
- claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
- claude_mpm/services/socketio/server/main.py +27 -1
- claude_mpm/services/ticket_manager.py +5 -1
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
- claude_mpm/agents/OUTPUT_STYLE.md +0 -73
- claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
- claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
- claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
- claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation service for tickets.
|
|
3
|
+
|
|
4
|
+
WHY: Centralizes all validation logic to ensure data integrity and
|
|
5
|
+
provide consistent error messages across the ticket system.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Returns validation results with detailed error messages
|
|
9
|
+
- Validates ticket IDs, types, statuses, priorities
|
|
10
|
+
- Handles pagination parameter validation
|
|
11
|
+
- Provides sanitization for user inputs
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TicketValidationService:
|
|
18
|
+
"""Service for validating ticket inputs."""
|
|
19
|
+
|
|
20
|
+
# Valid ticket types
|
|
21
|
+
VALID_TYPES = ["task", "issue", "epic", "bug", "feature", "story"]
|
|
22
|
+
|
|
23
|
+
# Valid ticket statuses
|
|
24
|
+
VALID_STATUSES = [
|
|
25
|
+
"open",
|
|
26
|
+
"in_progress",
|
|
27
|
+
"ready",
|
|
28
|
+
"tested",
|
|
29
|
+
"done",
|
|
30
|
+
"waiting",
|
|
31
|
+
"closed",
|
|
32
|
+
"blocked",
|
|
33
|
+
"all",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Valid priorities
|
|
37
|
+
VALID_PRIORITIES = ["low", "medium", "high", "critical"]
|
|
38
|
+
|
|
39
|
+
# Valid workflow states
|
|
40
|
+
VALID_WORKFLOW_STATES = [
|
|
41
|
+
"todo",
|
|
42
|
+
"in_progress",
|
|
43
|
+
"ready",
|
|
44
|
+
"tested",
|
|
45
|
+
"done",
|
|
46
|
+
"blocked",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def validate_ticket_id(self, ticket_id: Any) -> Tuple[bool, Optional[str]]:
|
|
50
|
+
"""
|
|
51
|
+
Validate a ticket ID.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Tuple of (is_valid, error_message)
|
|
55
|
+
"""
|
|
56
|
+
if not ticket_id:
|
|
57
|
+
return False, "No ticket ID provided"
|
|
58
|
+
|
|
59
|
+
if not isinstance(ticket_id, str):
|
|
60
|
+
return False, "Ticket ID must be a string"
|
|
61
|
+
|
|
62
|
+
if len(ticket_id) < 3:
|
|
63
|
+
return False, "Invalid ticket ID format"
|
|
64
|
+
|
|
65
|
+
return True, None
|
|
66
|
+
|
|
67
|
+
def validate_ticket_type(self, ticket_type: str) -> Tuple[bool, Optional[str]]:
|
|
68
|
+
"""
|
|
69
|
+
Validate a ticket type.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple of (is_valid, error_message)
|
|
73
|
+
"""
|
|
74
|
+
if not ticket_type:
|
|
75
|
+
return True, None # Type is optional, default will be used
|
|
76
|
+
|
|
77
|
+
if ticket_type not in self.VALID_TYPES:
|
|
78
|
+
return (
|
|
79
|
+
False,
|
|
80
|
+
f"Invalid ticket type: {ticket_type}. Valid types: {', '.join(self.VALID_TYPES)}",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return True, None
|
|
84
|
+
|
|
85
|
+
def validate_status(self, status: str) -> Tuple[bool, Optional[str]]:
|
|
86
|
+
"""
|
|
87
|
+
Validate a ticket status.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (is_valid, error_message)
|
|
91
|
+
"""
|
|
92
|
+
if not status:
|
|
93
|
+
return True, None # Status is optional
|
|
94
|
+
|
|
95
|
+
if status not in self.VALID_STATUSES:
|
|
96
|
+
return (
|
|
97
|
+
False,
|
|
98
|
+
f"Invalid status: {status}. Valid statuses: {', '.join(self.VALID_STATUSES)}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return True, None
|
|
102
|
+
|
|
103
|
+
def validate_priority(self, priority: str) -> Tuple[bool, Optional[str]]:
|
|
104
|
+
"""
|
|
105
|
+
Validate a ticket priority.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tuple of (is_valid, error_message)
|
|
109
|
+
"""
|
|
110
|
+
if not priority:
|
|
111
|
+
return True, None # Priority is optional, default will be used
|
|
112
|
+
|
|
113
|
+
if priority not in self.VALID_PRIORITIES:
|
|
114
|
+
return (
|
|
115
|
+
False,
|
|
116
|
+
f"Invalid priority: {priority}. Valid priorities: {', '.join(self.VALID_PRIORITIES)}",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return True, None
|
|
120
|
+
|
|
121
|
+
def validate_workflow_state(self, state: str) -> Tuple[bool, Optional[str]]:
|
|
122
|
+
"""
|
|
123
|
+
Validate a workflow state.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tuple of (is_valid, error_message)
|
|
127
|
+
"""
|
|
128
|
+
if not state:
|
|
129
|
+
return False, "No workflow state provided"
|
|
130
|
+
|
|
131
|
+
if state not in self.VALID_WORKFLOW_STATES:
|
|
132
|
+
return (
|
|
133
|
+
False,
|
|
134
|
+
f"Invalid workflow state: {state}. Valid states: {', '.join(self.VALID_WORKFLOW_STATES)}",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return True, None
|
|
138
|
+
|
|
139
|
+
def validate_pagination(
|
|
140
|
+
self, page: int, page_size: int
|
|
141
|
+
) -> Tuple[bool, Optional[str]]:
|
|
142
|
+
"""
|
|
143
|
+
Validate pagination parameters.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (is_valid, error_message)
|
|
147
|
+
"""
|
|
148
|
+
if page < 1:
|
|
149
|
+
return False, "Page number must be 1 or greater"
|
|
150
|
+
|
|
151
|
+
if page_size < 1:
|
|
152
|
+
return False, "Page size must be 1 or greater"
|
|
153
|
+
|
|
154
|
+
if page_size > 100:
|
|
155
|
+
return False, "Page size cannot exceed 100"
|
|
156
|
+
|
|
157
|
+
return True, None
|
|
158
|
+
|
|
159
|
+
def validate_create_params(
|
|
160
|
+
self, params: Dict[str, Any]
|
|
161
|
+
) -> Tuple[bool, Optional[str]]:
|
|
162
|
+
"""
|
|
163
|
+
Validate parameters for ticket creation.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Tuple of (is_valid, error_message)
|
|
167
|
+
"""
|
|
168
|
+
# Title is required
|
|
169
|
+
if not params.get("title"):
|
|
170
|
+
return False, "Title is required for ticket creation"
|
|
171
|
+
|
|
172
|
+
if len(params["title"]) < 3:
|
|
173
|
+
return False, "Title must be at least 3 characters long"
|
|
174
|
+
|
|
175
|
+
if len(params["title"]) > 200:
|
|
176
|
+
return False, "Title cannot exceed 200 characters"
|
|
177
|
+
|
|
178
|
+
# Validate optional fields if provided
|
|
179
|
+
if "type" in params:
|
|
180
|
+
valid, error = self.validate_ticket_type(params["type"])
|
|
181
|
+
if not valid:
|
|
182
|
+
return False, error
|
|
183
|
+
|
|
184
|
+
if "priority" in params:
|
|
185
|
+
valid, error = self.validate_priority(params["priority"])
|
|
186
|
+
if not valid:
|
|
187
|
+
return False, error
|
|
188
|
+
|
|
189
|
+
# Validate parent references
|
|
190
|
+
if params.get("parent_epic"):
|
|
191
|
+
valid, error = self.validate_ticket_id(params["parent_epic"])
|
|
192
|
+
if not valid:
|
|
193
|
+
return False, f"Invalid parent epic: {error}"
|
|
194
|
+
|
|
195
|
+
if params.get("parent_issue"):
|
|
196
|
+
valid, error = self.validate_ticket_id(params["parent_issue"])
|
|
197
|
+
if not valid:
|
|
198
|
+
return False, f"Invalid parent issue: {error}"
|
|
199
|
+
|
|
200
|
+
return True, None
|
|
201
|
+
|
|
202
|
+
def validate_update_params(
|
|
203
|
+
self, params: Dict[str, Any]
|
|
204
|
+
) -> Tuple[bool, Optional[str]]:
|
|
205
|
+
"""
|
|
206
|
+
Validate parameters for ticket update.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Tuple of (is_valid, error_message)
|
|
210
|
+
"""
|
|
211
|
+
# At least one update field must be provided
|
|
212
|
+
update_fields = ["status", "priority", "description", "tags", "assign"]
|
|
213
|
+
if not any(field in params for field in update_fields):
|
|
214
|
+
return False, "No update fields specified"
|
|
215
|
+
|
|
216
|
+
# Validate status if provided
|
|
217
|
+
if "status" in params:
|
|
218
|
+
valid, error = self.validate_status(params["status"])
|
|
219
|
+
if not valid:
|
|
220
|
+
return False, error
|
|
221
|
+
|
|
222
|
+
# Validate priority if provided
|
|
223
|
+
if "priority" in params:
|
|
224
|
+
valid, error = self.validate_priority(params["priority"])
|
|
225
|
+
if not valid:
|
|
226
|
+
return False, error
|
|
227
|
+
|
|
228
|
+
return True, None
|
|
229
|
+
|
|
230
|
+
def sanitize_tags(self, tags: Any) -> List[str]:
|
|
231
|
+
"""
|
|
232
|
+
Sanitize and parse tags input.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of sanitized tags
|
|
236
|
+
"""
|
|
237
|
+
if not tags:
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
if isinstance(tags, str):
|
|
241
|
+
# Split comma-separated tags
|
|
242
|
+
tag_list = [tag.strip() for tag in tags.split(",")]
|
|
243
|
+
elif isinstance(tags, list):
|
|
244
|
+
tag_list = [str(tag).strip() for tag in tags]
|
|
245
|
+
else:
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
# Remove empty tags and duplicates
|
|
249
|
+
return list(filter(None, dict.fromkeys(tag_list)))
|
|
250
|
+
|
|
251
|
+
def sanitize_description(self, description: Any) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Sanitize description input.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Sanitized description string
|
|
257
|
+
"""
|
|
258
|
+
if not description:
|
|
259
|
+
return ""
|
|
260
|
+
|
|
261
|
+
if isinstance(description, list):
|
|
262
|
+
# Join list elements with spaces
|
|
263
|
+
return " ".join(str(item) for item in description)
|
|
264
|
+
|
|
265
|
+
return str(description).strip()
|
|
266
|
+
|
|
267
|
+
def validate_search_query(self, query: str) -> Tuple[bool, Optional[str]]:
|
|
268
|
+
"""
|
|
269
|
+
Validate a search query.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tuple of (is_valid, error_message)
|
|
273
|
+
"""
|
|
274
|
+
if not query:
|
|
275
|
+
return False, "Search query cannot be empty"
|
|
276
|
+
|
|
277
|
+
if len(query) < 2:
|
|
278
|
+
return False, "Search query must be at least 2 characters"
|
|
279
|
+
|
|
280
|
+
if len(query) > 100:
|
|
281
|
+
return False, "Search query cannot exceed 100 characters"
|
|
282
|
+
|
|
283
|
+
return True, None
|
|
284
|
+
|
|
285
|
+
def validate_comment(self, comment: Any) -> Tuple[bool, Optional[str]]:
|
|
286
|
+
"""
|
|
287
|
+
Validate a comment.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Tuple of (is_valid, error_message)
|
|
291
|
+
"""
|
|
292
|
+
if not comment:
|
|
293
|
+
return False, "Comment cannot be empty"
|
|
294
|
+
|
|
295
|
+
comment_str = self.sanitize_description(comment)
|
|
296
|
+
|
|
297
|
+
if len(comment_str) < 1:
|
|
298
|
+
return False, "Comment cannot be empty"
|
|
299
|
+
|
|
300
|
+
if len(comment_str) > 5000:
|
|
301
|
+
return False, "Comment cannot exceed 5000 characters"
|
|
302
|
+
|
|
303
|
+
return True, None
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow service for ticket state transitions.
|
|
3
|
+
|
|
4
|
+
WHY: Manages ticket workflow states and transitions, ensuring valid
|
|
5
|
+
state changes and maintaining workflow integrity.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Enforces valid state transitions
|
|
9
|
+
- Handles workflow-specific operations (comments, notifications)
|
|
10
|
+
- Provides workflow history tracking
|
|
11
|
+
- Abstracts aitrackdown workflow commands
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from ...core.logger import get_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TicketWorkflowService:
|
|
21
|
+
"""Service for managing ticket workflow states."""
|
|
22
|
+
|
|
23
|
+
# Valid workflow transitions
|
|
24
|
+
WORKFLOW_TRANSITIONS = {
|
|
25
|
+
"todo": ["in_progress", "blocked"],
|
|
26
|
+
"in_progress": ["ready", "blocked", "todo"],
|
|
27
|
+
"ready": ["tested", "in_progress", "blocked"],
|
|
28
|
+
"tested": ["done", "ready", "blocked"],
|
|
29
|
+
"done": ["tested", "ready"], # Can reopen
|
|
30
|
+
"blocked": ["todo", "in_progress", "ready"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Status to workflow state mapping
|
|
34
|
+
STATUS_TO_WORKFLOW = {
|
|
35
|
+
"open": "todo",
|
|
36
|
+
"in_progress": "in_progress",
|
|
37
|
+
"ready": "ready",
|
|
38
|
+
"tested": "tested",
|
|
39
|
+
"done": "done",
|
|
40
|
+
"closed": "done",
|
|
41
|
+
"blocked": "blocked",
|
|
42
|
+
"waiting": "blocked",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
"""Initialize the workflow service."""
|
|
47
|
+
self.logger = get_logger("services.ticket_workflow")
|
|
48
|
+
|
|
49
|
+
def transition_ticket(
|
|
50
|
+
self,
|
|
51
|
+
ticket_id: str,
|
|
52
|
+
new_state: str,
|
|
53
|
+
comment: Optional[str] = None,
|
|
54
|
+
force: bool = False,
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Transition a ticket to a new workflow state.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
ticket_id: ID of the ticket
|
|
61
|
+
new_state: Target workflow state
|
|
62
|
+
comment: Optional comment for the transition
|
|
63
|
+
force: Force transition even if not normally allowed
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dict with success status and message
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
# Validate the transition if not forced
|
|
70
|
+
if not force:
|
|
71
|
+
valid, error = self.validate_transition(ticket_id, new_state)
|
|
72
|
+
if not valid:
|
|
73
|
+
return {"success": False, "error": error}
|
|
74
|
+
|
|
75
|
+
# Use aitrackdown CLI for the transition
|
|
76
|
+
result = self._transition_via_aitrackdown(ticket_id, new_state, comment)
|
|
77
|
+
|
|
78
|
+
if result["success"]:
|
|
79
|
+
# Log successful transition
|
|
80
|
+
self.logger.info(f"Transitioned {ticket_id} to {new_state}")
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(f"Error transitioning ticket {ticket_id}: {e}")
|
|
86
|
+
return {"success": False, "error": str(e)}
|
|
87
|
+
|
|
88
|
+
def _transition_via_aitrackdown(
|
|
89
|
+
self, ticket_id: str, state: str, comment: Optional[str]
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""Transition ticket using aitrackdown CLI."""
|
|
92
|
+
try:
|
|
93
|
+
cmd = ["aitrackdown", "transition", ticket_id, state]
|
|
94
|
+
|
|
95
|
+
if comment:
|
|
96
|
+
cmd.extend(["--comment", comment])
|
|
97
|
+
|
|
98
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"success": True,
|
|
102
|
+
"message": f"Updated workflow state for {ticket_id} to: {state}",
|
|
103
|
+
}
|
|
104
|
+
except subprocess.CalledProcessError as e:
|
|
105
|
+
self.logger.error(f"Failed to transition via CLI: {e}")
|
|
106
|
+
return {
|
|
107
|
+
"success": False,
|
|
108
|
+
"error": f"Failed to update workflow state for ticket: {ticket_id}",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def validate_transition(
|
|
112
|
+
self, ticket_id: str, new_state: str
|
|
113
|
+
) -> Tuple[bool, Optional[str]]:
|
|
114
|
+
"""
|
|
115
|
+
Validate if a workflow transition is allowed.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple of (is_valid, error_message)
|
|
119
|
+
"""
|
|
120
|
+
# For now, we assume all transitions are valid since we don't
|
|
121
|
+
# have access to current state without fetching the ticket
|
|
122
|
+
# In a real implementation, this would check current state
|
|
123
|
+
|
|
124
|
+
if new_state not in self.WORKFLOW_TRANSITIONS:
|
|
125
|
+
return False, f"Invalid workflow state: {new_state}"
|
|
126
|
+
|
|
127
|
+
# TODO: Fetch current state and validate transition
|
|
128
|
+
# current_state = self._get_current_state(ticket_id)
|
|
129
|
+
# if new_state not in self.WORKFLOW_TRANSITIONS.get(current_state, []):
|
|
130
|
+
# return False, f"Cannot transition from {current_state} to {new_state}"
|
|
131
|
+
|
|
132
|
+
return True, None
|
|
133
|
+
|
|
134
|
+
def add_comment(self, ticket_id: str, comment: str) -> Dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Add a comment to a ticket.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
ticket_id: ID of the ticket
|
|
140
|
+
comment: Comment text
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dict with success status and message
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
cmd = ["aitrackdown", "comment", ticket_id, comment]
|
|
147
|
+
|
|
148
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
149
|
+
|
|
150
|
+
return {"success": True, "message": f"Added comment to ticket: {ticket_id}"}
|
|
151
|
+
except subprocess.CalledProcessError as e:
|
|
152
|
+
self.logger.error(f"Failed to add comment: {e}")
|
|
153
|
+
return {
|
|
154
|
+
"success": False,
|
|
155
|
+
"error": f"Failed to add comment to ticket: {ticket_id}",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def get_workflow_states(self) -> List[str]:
|
|
159
|
+
"""
|
|
160
|
+
Get list of all valid workflow states.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of workflow state names
|
|
164
|
+
"""
|
|
165
|
+
return list(self.WORKFLOW_TRANSITIONS.keys())
|
|
166
|
+
|
|
167
|
+
def get_valid_transitions(self, current_state: str) -> List[str]:
|
|
168
|
+
"""
|
|
169
|
+
Get valid transitions from a given state.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
current_state: Current workflow state
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of valid target states
|
|
176
|
+
"""
|
|
177
|
+
return self.WORKFLOW_TRANSITIONS.get(current_state, [])
|
|
178
|
+
|
|
179
|
+
def map_status_to_workflow(self, status: str) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Map a ticket status to a workflow state.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
status: Ticket status
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Corresponding workflow state
|
|
188
|
+
"""
|
|
189
|
+
return self.STATUS_TO_WORKFLOW.get(status, "todo")
|
|
190
|
+
|
|
191
|
+
def bulk_transition(
|
|
192
|
+
self, ticket_ids: List[str], new_state: str, comment: Optional[str] = None
|
|
193
|
+
) -> Dict[str, Any]:
|
|
194
|
+
"""
|
|
195
|
+
Transition multiple tickets to a new state.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
ticket_ids: List of ticket IDs
|
|
199
|
+
new_state: Target workflow state
|
|
200
|
+
comment: Optional comment for all transitions
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dict with results for each ticket
|
|
204
|
+
"""
|
|
205
|
+
results = {"succeeded": [], "failed": [], "total": len(ticket_ids)}
|
|
206
|
+
|
|
207
|
+
for ticket_id in ticket_ids:
|
|
208
|
+
result = self.transition_ticket(ticket_id, new_state, comment)
|
|
209
|
+
|
|
210
|
+
if result["success"]:
|
|
211
|
+
results["succeeded"].append(ticket_id)
|
|
212
|
+
else:
|
|
213
|
+
results["failed"].append(
|
|
214
|
+
{
|
|
215
|
+
"ticket_id": ticket_id,
|
|
216
|
+
"error": result.get("error", "Unknown error"),
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return {"success": len(results["failed"]) == 0, "results": results}
|
|
221
|
+
|
|
222
|
+
def get_workflow_summary(self, tickets: List[Dict[str, Any]]) -> Dict[str, int]:
|
|
223
|
+
"""
|
|
224
|
+
Get summary of tickets by workflow state.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
tickets: List of ticket dictionaries
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Dict mapping workflow states to counts
|
|
231
|
+
"""
|
|
232
|
+
summary = dict.fromkeys(self.WORKFLOW_TRANSITIONS.keys(), 0)
|
|
233
|
+
summary["unknown"] = 0
|
|
234
|
+
|
|
235
|
+
for ticket in tickets:
|
|
236
|
+
status = ticket.get("status", "unknown")
|
|
237
|
+
workflow_state = self.map_status_to_workflow(status)
|
|
238
|
+
|
|
239
|
+
if workflow_state in summary:
|
|
240
|
+
summary[workflow_state] += 1
|
|
241
|
+
else:
|
|
242
|
+
summary["unknown"] += 1
|
|
243
|
+
|
|
244
|
+
return summary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-mpm
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.6
|
|
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
|
|
@@ -46,6 +46,8 @@ Requires-Dist: toml>=0.10.2
|
|
|
46
46
|
Requires-Dist: packaging>=21.0
|
|
47
47
|
Requires-Dist: pydantic>=2.0.0
|
|
48
48
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
49
|
+
Requires-Dist: textual>=0.47.0
|
|
50
|
+
Requires-Dist: rich>=13.0.0
|
|
49
51
|
Requires-Dist: pyee>=13.0.0
|
|
50
52
|
Requires-Dist: importlib-resources>=5.0; python_version < "3.9"
|
|
51
53
|
Provides-Extra: dev
|