mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""User-specific ticket management tools.
|
|
2
2
|
|
|
3
3
|
This module provides tools for managing tickets from a user's perspective,
|
|
4
|
-
including
|
|
5
|
-
states with validation.
|
|
4
|
+
including transitioning tickets through workflow states with validation.
|
|
6
5
|
|
|
7
6
|
Design Decision: Workflow State Validation
|
|
8
7
|
------------------------------------------
|
|
@@ -20,177 +19,38 @@ Valid workflow transitions:
|
|
|
20
19
|
- CLOSED → (no transitions, terminal state)
|
|
21
20
|
|
|
22
21
|
Performance Considerations:
|
|
23
|
-
- get_my_tickets uses adapter's native filtering when available
|
|
24
|
-
- Falls back to client-side filtering for adapters without assignee filter
|
|
25
22
|
- State transition validation is O(1) lookup in predefined state machine
|
|
26
23
|
"""
|
|
27
24
|
|
|
28
|
-
from pathlib import Path
|
|
29
25
|
from typing import Any
|
|
30
26
|
|
|
27
|
+
from ....core.adapter import BaseAdapter
|
|
31
28
|
from ....core.models import TicketState
|
|
32
|
-
from ....core.
|
|
29
|
+
from ....core.state_matcher import get_state_matcher
|
|
33
30
|
from ..server_sdk import get_adapter, mcp
|
|
34
31
|
|
|
35
32
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
ConfigResolver instance for current working directory
|
|
41
|
-
|
|
42
|
-
"""
|
|
43
|
-
return ConfigResolver(project_path=Path.cwd())
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@mcp.tool()
|
|
47
|
-
async def get_my_tickets(
|
|
48
|
-
state: str | None = None,
|
|
49
|
-
limit: int = 10,
|
|
33
|
+
def _build_adapter_metadata(
|
|
34
|
+
adapter: BaseAdapter,
|
|
35
|
+
ticket_id: str | None = None,
|
|
50
36
|
) -> dict[str, Any]:
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
limit: Maximum number of tickets to return (default: 10, max: 100)
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Dictionary containing:
|
|
63
|
-
- status: "completed" or "error"
|
|
64
|
-
- tickets: List of ticket objects assigned to user
|
|
65
|
-
- count: Number of tickets returned
|
|
66
|
-
- user: User ID that was queried
|
|
67
|
-
- state_filter: State filter applied (if any)
|
|
68
|
-
- error: Error details (if failed)
|
|
69
|
-
|
|
70
|
-
Example:
|
|
71
|
-
>>> result = await get_my_tickets(state="in_progress", limit=5)
|
|
72
|
-
>>> print(result)
|
|
73
|
-
{
|
|
74
|
-
"status": "completed",
|
|
75
|
-
"tickets": [
|
|
76
|
-
{"id": "TICKET-1", "title": "Fix bug", "state": "in_progress"},
|
|
77
|
-
{"id": "TICKET-2", "title": "Add feature", "state": "in_progress"}
|
|
78
|
-
],
|
|
79
|
-
"count": 2,
|
|
80
|
-
"user": "user@example.com",
|
|
81
|
-
"state_filter": "in_progress"
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
Error Conditions:
|
|
85
|
-
- No default user configured: Returns error with setup instructions
|
|
86
|
-
- Invalid state: Returns error with valid state options
|
|
87
|
-
- Adapter query failure: Returns error with details
|
|
88
|
-
|
|
89
|
-
Usage Notes:
|
|
90
|
-
- Requires default_user to be set in configuration
|
|
91
|
-
- Use config_set_default_user() to configure the user first
|
|
92
|
-
- Limit is capped at 100 to prevent performance issues
|
|
93
|
-
|
|
94
|
-
"""
|
|
95
|
-
try:
|
|
96
|
-
# Validate limit
|
|
97
|
-
if limit > 100:
|
|
98
|
-
limit = 100
|
|
99
|
-
|
|
100
|
-
# Load configuration to get default user
|
|
101
|
-
resolver = get_config_resolver()
|
|
102
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
103
|
-
|
|
104
|
-
if not config.default_user:
|
|
105
|
-
return {
|
|
106
|
-
"status": "error",
|
|
107
|
-
"error": "No default user configured. Use config_set_default_user() to set a default user first.",
|
|
108
|
-
"setup_command": "config_set_default_user",
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# Validate state if provided
|
|
112
|
-
state_filter = None
|
|
113
|
-
if state is not None:
|
|
114
|
-
try:
|
|
115
|
-
state_filter = TicketState(state.lower())
|
|
116
|
-
except ValueError:
|
|
117
|
-
valid_states = [s.value for s in TicketState]
|
|
118
|
-
return {
|
|
119
|
-
"status": "error",
|
|
120
|
-
"error": f"Invalid state '{state}'. Must be one of: {', '.join(valid_states)}",
|
|
121
|
-
"valid_states": valid_states,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
# Build filters
|
|
125
|
-
filters: dict[str, Any] = {"assignee": config.default_user}
|
|
126
|
-
if state_filter:
|
|
127
|
-
filters["state"] = state_filter
|
|
128
|
-
|
|
129
|
-
# Query adapter
|
|
130
|
-
adapter = get_adapter()
|
|
131
|
-
tickets = await adapter.list(limit=limit, offset=0, filters=filters)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
"status": "completed",
|
|
135
|
-
"tickets": [ticket.model_dump() for ticket in tickets],
|
|
136
|
-
"count": len(tickets),
|
|
137
|
-
"user": config.default_user,
|
|
138
|
-
"state_filter": state if state else "all",
|
|
139
|
-
"limit": limit,
|
|
140
|
-
}
|
|
141
|
-
except Exception as e:
|
|
142
|
-
return {
|
|
143
|
-
"status": "error",
|
|
144
|
-
"error": f"Failed to retrieve tickets: {str(e)}",
|
|
145
|
-
}
|
|
37
|
+
"""Build adapter metadata for MCP responses."""
|
|
38
|
+
metadata = {
|
|
39
|
+
"adapter": adapter.adapter_type,
|
|
40
|
+
"adapter_name": adapter.adapter_display_name,
|
|
41
|
+
}
|
|
42
|
+
if ticket_id:
|
|
43
|
+
metadata["ticket_id"] = ticket_id
|
|
44
|
+
return metadata
|
|
146
45
|
|
|
147
46
|
|
|
148
47
|
@mcp.tool()
|
|
149
48
|
async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
|
|
150
|
-
"""Get valid next states for
|
|
151
|
-
|
|
152
|
-
Retrieves the ticket's current state and returns all valid target states
|
|
153
|
-
according to the defined workflow state machine. This helps AI agents and
|
|
154
|
-
users understand which state transitions are allowed.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
ticket_id: Unique identifier of the ticket
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
Dictionary containing:
|
|
161
|
-
- status: "completed" or "error"
|
|
162
|
-
- ticket_id: ID of the queried ticket
|
|
163
|
-
- current_state: Current workflow state
|
|
164
|
-
- available_transitions: List of valid target states
|
|
165
|
-
- transition_descriptions: Human-readable descriptions of each transition
|
|
166
|
-
- error: Error details (if failed)
|
|
167
|
-
|
|
168
|
-
Example:
|
|
169
|
-
>>> result = await get_available_transitions("TICKET-123")
|
|
170
|
-
>>> print(result)
|
|
171
|
-
{
|
|
172
|
-
"status": "completed",
|
|
173
|
-
"ticket_id": "TICKET-123",
|
|
174
|
-
"current_state": "in_progress",
|
|
175
|
-
"available_transitions": ["ready", "waiting", "blocked", "open"],
|
|
176
|
-
"transition_descriptions": {
|
|
177
|
-
"ready": "Mark work as complete and ready for review",
|
|
178
|
-
"waiting": "Pause work while waiting for external dependency",
|
|
179
|
-
"blocked": "Work is blocked by an impediment",
|
|
180
|
-
"open": "Move back to backlog"
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
Error Conditions:
|
|
185
|
-
- Ticket not found: Returns error with ticket ID
|
|
186
|
-
- Adapter query failure: Returns error with details
|
|
187
|
-
- Terminal state (CLOSED): Returns empty transitions list
|
|
188
|
-
|
|
189
|
-
Usage Notes:
|
|
190
|
-
- CLOSED is a terminal state with no valid transitions
|
|
191
|
-
- Use this before ticket_transition() to validate intended state change
|
|
192
|
-
- Transition validation prevents workflow violations
|
|
49
|
+
"""Get valid next states for ticket based on workflow state machine.
|
|
193
50
|
|
|
51
|
+
Args: ticket_id (required)
|
|
52
|
+
Returns: TransitionResponse with current_state, available_transitions, transition_descriptions, is_terminal
|
|
53
|
+
See: docs/ticket-workflows.md#valid-state-transitions
|
|
194
54
|
"""
|
|
195
55
|
try:
|
|
196
56
|
# Get ticket from adapter
|
|
@@ -231,7 +91,7 @@ async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
|
|
|
231
91
|
|
|
232
92
|
return {
|
|
233
93
|
"status": "completed",
|
|
234
|
-
|
|
94
|
+
**_build_adapter_metadata(adapter, ticket_id),
|
|
235
95
|
"current_state": current_state.value,
|
|
236
96
|
"available_transitions": [state.value for state in available],
|
|
237
97
|
"transition_descriptions": transition_descriptions,
|
|
@@ -249,63 +109,13 @@ async def ticket_transition(
|
|
|
249
109
|
ticket_id: str,
|
|
250
110
|
to_state: str,
|
|
251
111
|
comment: str | None = None,
|
|
112
|
+
auto_confirm: bool = True,
|
|
252
113
|
) -> dict[str, Any]:
|
|
253
|
-
"""Move ticket through workflow with validation and
|
|
254
|
-
|
|
255
|
-
Transitions a ticket to a new state, validating the transition against the
|
|
256
|
-
defined workflow rules. Optionally adds a comment explaining the transition.
|
|
257
|
-
|
|
258
|
-
Workflow State Machine:
|
|
259
|
-
OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
|
|
260
|
-
IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
|
|
261
|
-
READY → TESTED, IN_PROGRESS, BLOCKED
|
|
262
|
-
TESTED → DONE, IN_PROGRESS
|
|
263
|
-
DONE → CLOSED
|
|
264
|
-
WAITING → OPEN, IN_PROGRESS, CLOSED
|
|
265
|
-
BLOCKED → OPEN, IN_PROGRESS, CLOSED
|
|
266
|
-
CLOSED → (no transitions)
|
|
267
|
-
|
|
268
|
-
Args:
|
|
269
|
-
ticket_id: Unique identifier of the ticket to transition
|
|
270
|
-
to_state: Target state - must be valid for current state
|
|
271
|
-
comment: Optional comment explaining the transition reason
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
Dictionary containing:
|
|
275
|
-
- status: "completed" or "error"
|
|
276
|
-
- ticket: Updated ticket object with new state
|
|
277
|
-
- previous_state: State before transition
|
|
278
|
-
- new_state: State after transition
|
|
279
|
-
- comment_added: Whether a comment was added (if applicable)
|
|
280
|
-
- error: Error details (if failed)
|
|
281
|
-
|
|
282
|
-
Example:
|
|
283
|
-
>>> result = await ticket_transition(
|
|
284
|
-
... "TICKET-123",
|
|
285
|
-
... "ready",
|
|
286
|
-
... "Work complete, ready for code review"
|
|
287
|
-
... )
|
|
288
|
-
>>> print(result)
|
|
289
|
-
{
|
|
290
|
-
"status": "completed",
|
|
291
|
-
"ticket": {"id": "TICKET-123", "state": "ready", ...},
|
|
292
|
-
"previous_state": "in_progress",
|
|
293
|
-
"new_state": "ready",
|
|
294
|
-
"comment_added": True
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
Error Conditions:
|
|
298
|
-
- Ticket not found: Returns error with ticket ID
|
|
299
|
-
- Invalid transition: Returns error with valid options
|
|
300
|
-
- Invalid state name: Returns error with valid states
|
|
301
|
-
- Adapter update failure: Returns error with details
|
|
302
|
-
|
|
303
|
-
Usage Notes:
|
|
304
|
-
- Use get_available_transitions() first to see valid options
|
|
305
|
-
- Comments are adapter-dependent (some may not support them)
|
|
306
|
-
- Validation prevents workflow violations
|
|
307
|
-
- Terminal state (CLOSED) has no valid transitions
|
|
114
|
+
"""Move ticket through workflow with validation and semantic matching (natural language support).
|
|
308
115
|
|
|
116
|
+
Args: ticket_id (required), to_state (supports natural language like "working on it"), comment (optional), auto_confirm (default: True)
|
|
117
|
+
Returns: TransitionResponse with status, ticket, previous_state, new_state, matched_state, confidence, suggestions (if ambiguous)
|
|
118
|
+
See: docs/ticket-workflows.md#semantic-state-matching, docs/ticket-workflows.md#valid-state-transitions
|
|
309
119
|
"""
|
|
310
120
|
try:
|
|
311
121
|
# Get ticket from adapter
|
|
@@ -318,41 +128,130 @@ async def ticket_transition(
|
|
|
318
128
|
"error": f"Ticket {ticket_id} not found",
|
|
319
129
|
}
|
|
320
130
|
|
|
321
|
-
# Validate target state
|
|
322
|
-
try:
|
|
323
|
-
target_state = TicketState(to_state.lower())
|
|
324
|
-
except ValueError:
|
|
325
|
-
valid_states = [s.value for s in TicketState]
|
|
326
|
-
return {
|
|
327
|
-
"status": "error",
|
|
328
|
-
"error": f"Invalid state '{to_state}'. Must be one of: {', '.join(valid_states)}",
|
|
329
|
-
"valid_states": valid_states,
|
|
330
|
-
}
|
|
331
|
-
|
|
332
131
|
# Store current state for response
|
|
333
132
|
current_state = ticket.state
|
|
334
133
|
# Handle both TicketState enum and string values
|
|
335
134
|
if isinstance(current_state, str):
|
|
336
135
|
current_state = TicketState(current_state)
|
|
337
136
|
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
137
|
+
# Use semantic matcher to resolve target state
|
|
138
|
+
matcher = get_state_matcher()
|
|
139
|
+
match_result = matcher.match_state(to_state)
|
|
140
|
+
|
|
141
|
+
# Build response with semantic match info
|
|
142
|
+
response: dict[str, Any] = {
|
|
143
|
+
"ticket_id": ticket_id,
|
|
144
|
+
"original_input": to_state,
|
|
145
|
+
"matched_state": match_result.state.value,
|
|
146
|
+
"confidence": match_result.confidence,
|
|
147
|
+
"match_type": match_result.match_type,
|
|
148
|
+
"current_state": current_state.value,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Handle low confidence - provide suggestions
|
|
152
|
+
if match_result.is_low_confidence():
|
|
153
|
+
suggestions = matcher.suggest_states(to_state, top_n=3)
|
|
342
154
|
return {
|
|
343
|
-
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
|
|
155
|
+
**response,
|
|
156
|
+
"status": "ambiguous",
|
|
157
|
+
"message": "Input is ambiguous. Please choose from suggestions.",
|
|
158
|
+
"suggestions": [
|
|
159
|
+
{
|
|
160
|
+
"state": s.state.value,
|
|
161
|
+
"confidence": s.confidence,
|
|
162
|
+
"description": _get_state_description(s.state),
|
|
163
|
+
}
|
|
164
|
+
for s in suggestions
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Handle medium confidence - needs confirmation unless auto_confirm
|
|
169
|
+
if match_result.is_medium_confidence() and not auto_confirm:
|
|
170
|
+
return {
|
|
171
|
+
**response,
|
|
172
|
+
"status": "needs_confirmation",
|
|
173
|
+
"message": f"Matched '{to_state}' to '{match_result.state.value}' with {match_result.confidence:.0%} confidence. Please confirm.",
|
|
174
|
+
"confirm_required": True,
|
|
349
175
|
}
|
|
350
176
|
|
|
177
|
+
target_state = match_result.state
|
|
178
|
+
|
|
179
|
+
# Validate transition using adapter (includes parent/child state constraints)
|
|
180
|
+
is_valid = await adapter.validate_transition(ticket_id, target_state)
|
|
181
|
+
if not is_valid:
|
|
182
|
+
# Check if it's a workflow violation or parent constraint violation
|
|
183
|
+
workflow_valid = current_state.can_transition_to(target_state)
|
|
184
|
+
valid_transitions = TicketState.valid_transitions().get(current_state, [])
|
|
185
|
+
valid_values = [s.value for s in valid_transitions]
|
|
186
|
+
|
|
187
|
+
if workflow_valid:
|
|
188
|
+
# Workflow is valid, so this must be a parent constraint violation
|
|
189
|
+
# Get children to determine max child state
|
|
190
|
+
from ....core.models import Task
|
|
191
|
+
|
|
192
|
+
if isinstance(ticket, Task) and ticket.children:
|
|
193
|
+
try:
|
|
194
|
+
children = await adapter.list_tasks_by_issue(ticket_id)
|
|
195
|
+
if children:
|
|
196
|
+
max_child_state = None
|
|
197
|
+
max_child_level = 0
|
|
198
|
+
for child in children:
|
|
199
|
+
child_state = child.state
|
|
200
|
+
if isinstance(child_state, str):
|
|
201
|
+
try:
|
|
202
|
+
child_state = TicketState(child_state)
|
|
203
|
+
except ValueError:
|
|
204
|
+
continue
|
|
205
|
+
child_level = child_state.completion_level()
|
|
206
|
+
if child_level > max_child_level:
|
|
207
|
+
max_child_level = child_level
|
|
208
|
+
max_child_state = child_state
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
**response,
|
|
212
|
+
"status": "error",
|
|
213
|
+
"error": f"Cannot transition to '{target_state.value}': parent issue has children in higher completion states",
|
|
214
|
+
"reason": "parent_constraint_violation",
|
|
215
|
+
"max_child_state": (
|
|
216
|
+
max_child_state.value if max_child_state else None
|
|
217
|
+
),
|
|
218
|
+
"message": f"Cannot transition to {target_state.value}: "
|
|
219
|
+
f"parent issue has children in higher completion states (max child state: {max_child_state.value if max_child_state else 'unknown'}). "
|
|
220
|
+
f"Please update child states first.",
|
|
221
|
+
"valid_transitions": valid_values,
|
|
222
|
+
}
|
|
223
|
+
except Exception:
|
|
224
|
+
# Fallback to generic message if we can't determine child states
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# Generic parent constraint violation message
|
|
228
|
+
return {
|
|
229
|
+
**response,
|
|
230
|
+
"status": "error",
|
|
231
|
+
"error": f"Cannot transition to '{target_state.value}': parent/child state constraint violation",
|
|
232
|
+
"reason": "parent_constraint_violation",
|
|
233
|
+
"message": f"Cannot transition to {target_state.value}: "
|
|
234
|
+
f"parent issue has children in higher completion states. Please update child states first.",
|
|
235
|
+
"valid_transitions": valid_values,
|
|
236
|
+
}
|
|
237
|
+
else:
|
|
238
|
+
# Workflow violation
|
|
239
|
+
return {
|
|
240
|
+
**response,
|
|
241
|
+
"status": "error",
|
|
242
|
+
"error": f"Invalid transition from '{current_state.value}' to '{target_state.value}'",
|
|
243
|
+
"reason": "workflow_violation",
|
|
244
|
+
"valid_transitions": valid_values,
|
|
245
|
+
"message": f"Cannot transition from {current_state.value} to {target_state.value}. "
|
|
246
|
+
f"Valid transitions: {', '.join(valid_values) if valid_values else 'none (terminal state)'}",
|
|
247
|
+
}
|
|
248
|
+
|
|
351
249
|
# Update ticket state
|
|
352
250
|
updated = await adapter.update(ticket_id, {"state": target_state})
|
|
353
251
|
|
|
354
252
|
if updated is None:
|
|
355
253
|
return {
|
|
254
|
+
**response,
|
|
356
255
|
"status": "error",
|
|
357
256
|
"error": f"Failed to update ticket {ticket_id}",
|
|
358
257
|
}
|
|
@@ -367,7 +266,61 @@ async def ticket_transition(
|
|
|
367
266
|
# Log but don't fail the transition
|
|
368
267
|
comment_added = False
|
|
369
268
|
|
|
370
|
-
|
|
269
|
+
# Auto project update hook (1M-315)
|
|
270
|
+
# Trigger automatic project update if enabled and ticket has parent epic
|
|
271
|
+
auto_update_result = None
|
|
272
|
+
try:
|
|
273
|
+
from pathlib import Path
|
|
274
|
+
|
|
275
|
+
from ....automation.project_updates import AutoProjectUpdateManager
|
|
276
|
+
from ....core.project_config import ConfigResolver
|
|
277
|
+
|
|
278
|
+
# Load config
|
|
279
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
280
|
+
config_obj = resolver.load_project_config()
|
|
281
|
+
config_dict = config_obj.to_dict() if config_obj else {}
|
|
282
|
+
|
|
283
|
+
# Check if auto updates enabled
|
|
284
|
+
auto_updates_mgr = AutoProjectUpdateManager(config_dict, adapter)
|
|
285
|
+
if auto_updates_mgr.is_enabled():
|
|
286
|
+
# Check if ticket has parent_epic
|
|
287
|
+
parent_epic = (
|
|
288
|
+
updated.parent_epic if hasattr(updated, "parent_epic") else None
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if parent_epic:
|
|
292
|
+
# Only trigger on configured frequency
|
|
293
|
+
update_frequency = auto_updates_mgr.get_update_frequency()
|
|
294
|
+
should_trigger = update_frequency == "on_transition"
|
|
295
|
+
|
|
296
|
+
# For "on_completion", only trigger if transitioned to done/closed
|
|
297
|
+
if update_frequency == "on_completion":
|
|
298
|
+
from ....core.models import TicketState as TSEnum
|
|
299
|
+
|
|
300
|
+
should_trigger = target_state in (TSEnum.DONE, TSEnum.CLOSED)
|
|
301
|
+
|
|
302
|
+
if should_trigger:
|
|
303
|
+
auto_update_result = (
|
|
304
|
+
await auto_updates_mgr.create_transition_update(
|
|
305
|
+
ticket_id=ticket_id,
|
|
306
|
+
ticket_title=updated.title or "",
|
|
307
|
+
old_state=current_state.value,
|
|
308
|
+
new_state=target_state.value,
|
|
309
|
+
parent_epic=parent_epic,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
# Log error but don't block the transition
|
|
314
|
+
import logging
|
|
315
|
+
|
|
316
|
+
logging.getLogger(__name__).warning(
|
|
317
|
+
f"Auto project update failed (non-blocking): {e}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Build final response
|
|
321
|
+
final_response = {
|
|
322
|
+
**response,
|
|
323
|
+
**_build_adapter_metadata(adapter, ticket_id),
|
|
371
324
|
"status": "completed",
|
|
372
325
|
"ticket": updated.model_dump(),
|
|
373
326
|
"previous_state": current_state.value,
|
|
@@ -375,8 +328,37 @@ async def ticket_transition(
|
|
|
375
328
|
"comment_added": comment_added,
|
|
376
329
|
"message": f"Ticket {ticket_id} transitioned from {current_state.value} to {target_state.value}",
|
|
377
330
|
}
|
|
331
|
+
|
|
332
|
+
# Include auto update result if applicable
|
|
333
|
+
if auto_update_result:
|
|
334
|
+
final_response["auto_project_update"] = auto_update_result
|
|
335
|
+
|
|
336
|
+
return final_response
|
|
378
337
|
except Exception as e:
|
|
379
338
|
return {
|
|
380
339
|
"status": "error",
|
|
381
340
|
"error": f"Failed to transition ticket: {str(e)}",
|
|
382
341
|
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _get_state_description(state: TicketState) -> str:
|
|
345
|
+
"""Get human-readable description of a state.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
state: TicketState to describe
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Description string
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
descriptions = {
|
|
355
|
+
TicketState.OPEN: "Work not yet started, in backlog",
|
|
356
|
+
TicketState.IN_PROGRESS: "Work is actively being done",
|
|
357
|
+
TicketState.READY: "Work complete, ready for review or testing",
|
|
358
|
+
TicketState.TESTED: "Work has been tested and verified",
|
|
359
|
+
TicketState.DONE: "Work is complete and accepted",
|
|
360
|
+
TicketState.WAITING: "Work paused, waiting for external dependency",
|
|
361
|
+
TicketState.BLOCKED: "Work blocked by an impediment",
|
|
362
|
+
TicketState.CLOSED: "Ticket closed or archived (final state)",
|
|
363
|
+
}
|
|
364
|
+
return descriptions.get(state, "")
|
mcp_ticketer/queue/worker.py
CHANGED
|
@@ -184,7 +184,7 @@ class Worker:
|
|
|
184
184
|
logger.info(f"Processing batch of {len(batch)} items")
|
|
185
185
|
|
|
186
186
|
# Group items by adapter for concurrent processing
|
|
187
|
-
adapter_groups = {}
|
|
187
|
+
adapter_groups: dict[str, list[Any]] = {}
|
|
188
188
|
for item in batch:
|
|
189
189
|
if item.adapter not in adapter_groups:
|
|
190
190
|
adapter_groups[item.adapter] = []
|