mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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.

Files changed (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,547 @@
1
+ """User-specific ticket management tools.
2
+
3
+ This module provides tools for managing tickets from a user's perspective,
4
+ including listing assigned tickets and transitioning tickets through workflow
5
+ states with validation.
6
+
7
+ Design Decision: Workflow State Validation
8
+ ------------------------------------------
9
+ State transitions are validated using TicketState.can_transition_to() to ensure
10
+ tickets follow the defined workflow. This prevents invalid state changes that
11
+ could break integrations or confuse team members.
12
+
13
+ Valid workflow transitions:
14
+ - OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
15
+ - IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
16
+ - READY → TESTED, IN_PROGRESS, BLOCKED
17
+ - TESTED → DONE, IN_PROGRESS
18
+ - DONE → CLOSED
19
+ - WAITING/BLOCKED → OPEN, IN_PROGRESS, CLOSED
20
+ - CLOSED → (no transitions, terminal state)
21
+
22
+ 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
+ - State transition validation is O(1) lookup in predefined state machine
26
+ """
27
+
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from ....core.adapter import BaseAdapter
32
+ from ....core.models import TicketState
33
+ from ....core.project_config import ConfigResolver, TicketerConfig
34
+ from ....core.state_matcher import get_state_matcher
35
+ from ..server_sdk import get_adapter, mcp
36
+
37
+
38
+ def _build_adapter_metadata(
39
+ adapter: BaseAdapter,
40
+ ticket_id: str | None = None,
41
+ ) -> dict[str, Any]:
42
+ """Build adapter metadata for MCP responses."""
43
+ metadata = {
44
+ "adapter": adapter.adapter_type,
45
+ "adapter_name": adapter.adapter_display_name,
46
+ }
47
+ if ticket_id:
48
+ metadata["ticket_id"] = ticket_id
49
+ return metadata
50
+
51
+
52
+ def get_config_resolver() -> ConfigResolver:
53
+ """Get configuration resolver for current project.
54
+
55
+ Returns:
56
+ ConfigResolver instance for current working directory
57
+
58
+ """
59
+ return ConfigResolver(project_path=Path.cwd())
60
+
61
+
62
+ @mcp.tool()
63
+ async def get_my_tickets(
64
+ state: str | None = None,
65
+ limit: int = 10,
66
+ ) -> dict[str, Any]:
67
+ """Get tickets assigned to the configured default user.
68
+
69
+ Retrieves tickets assigned to the user specified in default_user configuration.
70
+ Requires default_user to be set via config_set_default_user().
71
+
72
+ Args:
73
+ state: Optional state filter - must be one of: open, in_progress, ready,
74
+ tested, done, closed, waiting, blocked
75
+ limit: Maximum number of tickets to return (default: 10, max: 100)
76
+
77
+ Returns:
78
+ Dictionary containing:
79
+ - status: "completed" or "error"
80
+ - tickets: List of ticket objects assigned to user
81
+ - count: Number of tickets returned
82
+ - user: User ID that was queried
83
+ - state_filter: State filter applied (if any)
84
+ - error: Error details (if failed)
85
+
86
+ Example:
87
+ >>> result = await get_my_tickets(state="in_progress", limit=5)
88
+ >>> print(result)
89
+ {
90
+ "status": "completed",
91
+ "tickets": [
92
+ {"id": "TICKET-1", "title": "Fix bug", "state": "in_progress"},
93
+ {"id": "TICKET-2", "title": "Add feature", "state": "in_progress"}
94
+ ],
95
+ "count": 2,
96
+ "user": "user@example.com",
97
+ "state_filter": "in_progress"
98
+ }
99
+
100
+ Error Conditions:
101
+ - No default user configured: Returns error with setup instructions
102
+ - Invalid state: Returns error with valid state options
103
+ - Adapter query failure: Returns error with details
104
+
105
+ Usage Notes:
106
+ - Requires default_user to be set in configuration
107
+ - Use config_set_default_user() to configure the user first
108
+ - Limit is capped at 100 to prevent performance issues
109
+
110
+ """
111
+ try:
112
+ # Validate limit
113
+ if limit > 100:
114
+ limit = 100
115
+
116
+ # Load configuration to get default user
117
+ resolver = get_config_resolver()
118
+ config = resolver.load_project_config() or TicketerConfig()
119
+
120
+ if not config.default_user:
121
+ return {
122
+ "status": "error",
123
+ "error": "No default user configured. Use config_set_default_user() to set a default user first.",
124
+ "setup_command": "config_set_default_user",
125
+ }
126
+
127
+ # Validate state if provided
128
+ state_filter = None
129
+ if state is not None:
130
+ try:
131
+ state_filter = TicketState(state.lower())
132
+ except ValueError:
133
+ valid_states = [s.value for s in TicketState]
134
+ return {
135
+ "status": "error",
136
+ "error": f"Invalid state '{state}'. Must be one of: {', '.join(valid_states)}",
137
+ "valid_states": valid_states,
138
+ }
139
+
140
+ # Build filters
141
+ filters: dict[str, Any] = {"assignee": config.default_user}
142
+ if state_filter:
143
+ filters["state"] = state_filter
144
+
145
+ # Query adapter
146
+ adapter = get_adapter()
147
+ tickets = await adapter.list(limit=limit, offset=0, filters=filters)
148
+
149
+ return {
150
+ "status": "completed",
151
+ **_build_adapter_metadata(adapter),
152
+ "tickets": [ticket.model_dump() for ticket in tickets],
153
+ "count": len(tickets),
154
+ "user": config.default_user,
155
+ "state_filter": state if state else "all",
156
+ "limit": limit,
157
+ }
158
+ except Exception as e:
159
+ return {
160
+ "status": "error",
161
+ "error": f"Failed to retrieve tickets: {str(e)}",
162
+ }
163
+
164
+
165
+ @mcp.tool()
166
+ async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
167
+ """Get valid next states for a ticket based on workflow rules.
168
+
169
+ Retrieves the ticket's current state and returns all valid target states
170
+ according to the defined workflow state machine. This helps AI agents and
171
+ users understand which state transitions are allowed.
172
+
173
+ Args:
174
+ ticket_id: Unique identifier of the ticket
175
+
176
+ Returns:
177
+ Dictionary containing:
178
+ - status: "completed" or "error"
179
+ - ticket_id: ID of the queried ticket
180
+ - current_state: Current workflow state
181
+ - available_transitions: List of valid target states
182
+ - transition_descriptions: Human-readable descriptions of each transition
183
+ - error: Error details (if failed)
184
+
185
+ Example:
186
+ >>> result = await get_available_transitions("TICKET-123")
187
+ >>> print(result)
188
+ {
189
+ "status": "completed",
190
+ "ticket_id": "TICKET-123",
191
+ "current_state": "in_progress",
192
+ "available_transitions": ["ready", "waiting", "blocked", "open"],
193
+ "transition_descriptions": {
194
+ "ready": "Mark work as complete and ready for review",
195
+ "waiting": "Pause work while waiting for external dependency",
196
+ "blocked": "Work is blocked by an impediment",
197
+ "open": "Move back to backlog"
198
+ }
199
+ }
200
+
201
+ Error Conditions:
202
+ - Ticket not found: Returns error with ticket ID
203
+ - Adapter query failure: Returns error with details
204
+ - Terminal state (CLOSED): Returns empty transitions list
205
+
206
+ Usage Notes:
207
+ - CLOSED is a terminal state with no valid transitions
208
+ - Use this before ticket_transition() to validate intended state change
209
+ - Transition validation prevents workflow violations
210
+
211
+ """
212
+ try:
213
+ # Get ticket from adapter
214
+ adapter = get_adapter()
215
+ ticket = await adapter.read(ticket_id)
216
+
217
+ if ticket is None:
218
+ return {
219
+ "status": "error",
220
+ "error": f"Ticket {ticket_id} not found",
221
+ }
222
+
223
+ # Get current state
224
+ current_state = ticket.state
225
+
226
+ # Get valid transitions from state machine
227
+ valid_transitions = TicketState.valid_transitions()
228
+ # Handle both TicketState enum and string values
229
+ if isinstance(current_state, str):
230
+ current_state = TicketState(current_state)
231
+ available = valid_transitions.get(current_state, [])
232
+
233
+ # Create human-readable descriptions
234
+ descriptions = {
235
+ TicketState.OPEN: "Move to backlog (not yet started)",
236
+ TicketState.IN_PROGRESS: "Begin active work on ticket",
237
+ TicketState.READY: "Mark as complete and ready for review/testing",
238
+ TicketState.TESTED: "Mark as tested and verified",
239
+ TicketState.DONE: "Mark as complete and accepted",
240
+ TicketState.WAITING: "Pause work while waiting for external dependency",
241
+ TicketState.BLOCKED: "Work is blocked by an impediment",
242
+ TicketState.CLOSED: "Close and archive ticket (final state)",
243
+ }
244
+
245
+ transition_descriptions = {
246
+ state.value: descriptions.get(state, "") for state in available
247
+ }
248
+
249
+ return {
250
+ "status": "completed",
251
+ **_build_adapter_metadata(adapter, ticket_id),
252
+ "current_state": current_state.value,
253
+ "available_transitions": [state.value for state in available],
254
+ "transition_descriptions": transition_descriptions,
255
+ "is_terminal": len(available) == 0,
256
+ }
257
+ except Exception as e:
258
+ return {
259
+ "status": "error",
260
+ "error": f"Failed to get available transitions: {str(e)}",
261
+ }
262
+
263
+
264
+ @mcp.tool()
265
+ async def ticket_transition(
266
+ ticket_id: str,
267
+ to_state: str,
268
+ comment: str | None = None,
269
+ auto_confirm: bool = True,
270
+ ) -> dict[str, Any]:
271
+ """Move ticket through workflow with validation and optional comment.
272
+
273
+ Supports natural language state inputs with semantic matching.
274
+ Transitions a ticket to a new state, validating the transition against the
275
+ defined workflow rules. Optionally adds a comment explaining the transition.
276
+
277
+ Semantic State Matching:
278
+ - Accepts natural language: "working on it" → IN_PROGRESS
279
+ - Handles typos: "reviw" → READY
280
+ - Provides suggestions for ambiguous inputs
281
+ - Confidence-based handling (high/medium/low)
282
+
283
+ Workflow State Machine:
284
+ OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
285
+ IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
286
+ READY → TESTED, IN_PROGRESS, BLOCKED
287
+ TESTED → DONE, IN_PROGRESS
288
+ DONE → CLOSED
289
+ WAITING → OPEN, IN_PROGRESS, CLOSED
290
+ BLOCKED → OPEN, IN_PROGRESS, CLOSED
291
+ CLOSED → (no transitions)
292
+
293
+ Args:
294
+ ticket_id: Unique identifier of the ticket to transition
295
+ to_state: Target state (supports natural language!)
296
+ Examples: "working on it", "needs review", "finished", "review"
297
+ comment: Optional comment explaining the transition reason
298
+ auto_confirm: Auto-apply high confidence matches (default: True)
299
+
300
+ Returns:
301
+ Dictionary containing:
302
+ - status: "completed", "needs_confirmation", or "error"
303
+ - ticket: Updated ticket object with new state (if completed)
304
+ - previous_state: State before transition
305
+ - new_state: State after transition
306
+ - matched_state: Matched state from input (if semantic match used)
307
+ - confidence: Confidence score (0.0-1.0) for semantic matches
308
+ - original_input: Original user input
309
+ - suggestions: Alternative matches (for ambiguous inputs)
310
+ - comment_added: Whether a comment was added (if applicable)
311
+ - error: Error details (if failed)
312
+
313
+ Example:
314
+ >>> # Natural language input
315
+ >>> result = await ticket_transition(
316
+ ... "TICKET-123",
317
+ ... "working on it",
318
+ ... "Started implementation"
319
+ ... )
320
+ >>> print(result)
321
+ {
322
+ "status": "completed",
323
+ "ticket": {"id": "TICKET-123", "state": "in_progress", ...},
324
+ "previous_state": "open",
325
+ "new_state": "in_progress",
326
+ "matched_state": "in_progress",
327
+ "confidence": 0.95,
328
+ "original_input": "working on it",
329
+ "comment_added": True
330
+ }
331
+
332
+ >>> # Ambiguous input returns suggestions
333
+ >>> result = await ticket_transition("TICKET-123", "rev")
334
+ >>> print(result)
335
+ {
336
+ "status": "needs_confirmation",
337
+ "matched_state": "ready",
338
+ "confidence": 0.75,
339
+ "suggestions": [
340
+ {"state": "ready", "confidence": 0.75},
341
+ {"state": "reviewed", "confidence": 0.60}
342
+ ]
343
+ }
344
+
345
+ Error Conditions:
346
+ - Ticket not found: Returns error with ticket ID
347
+ - Invalid transition: Returns error with valid options
348
+ - Invalid state name: Returns error with valid states
349
+ - Adapter update failure: Returns error with details
350
+
351
+ Usage Notes:
352
+ - Use get_available_transitions() first to see valid options
353
+ - Comments are adapter-dependent (some may not support them)
354
+ - Validation prevents workflow violations
355
+ - Terminal state (CLOSED) has no valid transitions
356
+ - High confidence (≥0.90): Auto-applied
357
+ - Medium confidence (0.70-0.89): Needs confirmation (if auto_confirm=False)
358
+ - Low confidence (<0.70): Returns suggestions
359
+
360
+ """
361
+ try:
362
+ # Get ticket from adapter
363
+ adapter = get_adapter()
364
+ ticket = await adapter.read(ticket_id)
365
+
366
+ if ticket is None:
367
+ return {
368
+ "status": "error",
369
+ "error": f"Ticket {ticket_id} not found",
370
+ }
371
+
372
+ # Store current state for response
373
+ current_state = ticket.state
374
+ # Handle both TicketState enum and string values
375
+ if isinstance(current_state, str):
376
+ current_state = TicketState(current_state)
377
+
378
+ # Use semantic matcher to resolve target state
379
+ matcher = get_state_matcher()
380
+ match_result = matcher.match_state(to_state)
381
+
382
+ # Build response with semantic match info
383
+ response: dict[str, Any] = {
384
+ "ticket_id": ticket_id,
385
+ "original_input": to_state,
386
+ "matched_state": match_result.state.value,
387
+ "confidence": match_result.confidence,
388
+ "match_type": match_result.match_type,
389
+ "current_state": current_state.value,
390
+ }
391
+
392
+ # Handle low confidence - provide suggestions
393
+ if match_result.is_low_confidence():
394
+ suggestions = matcher.suggest_states(to_state, top_n=3)
395
+ return {
396
+ **response,
397
+ "status": "ambiguous",
398
+ "message": "Input is ambiguous. Please choose from suggestions.",
399
+ "suggestions": [
400
+ {
401
+ "state": s.state.value,
402
+ "confidence": s.confidence,
403
+ "description": _get_state_description(s.state),
404
+ }
405
+ for s in suggestions
406
+ ],
407
+ }
408
+
409
+ # Handle medium confidence - needs confirmation unless auto_confirm
410
+ if match_result.is_medium_confidence() and not auto_confirm:
411
+ return {
412
+ **response,
413
+ "status": "needs_confirmation",
414
+ "message": f"Matched '{to_state}' to '{match_result.state.value}' with {match_result.confidence:.0%} confidence. Please confirm.",
415
+ "confirm_required": True,
416
+ }
417
+
418
+ target_state = match_result.state
419
+
420
+ # Validate transition using adapter (includes parent/child state constraints)
421
+ is_valid = await adapter.validate_transition(ticket_id, target_state)
422
+ if not is_valid:
423
+ # Check if it's a workflow violation or parent constraint violation
424
+ workflow_valid = current_state.can_transition_to(target_state)
425
+ valid_transitions = TicketState.valid_transitions().get(current_state, [])
426
+ valid_values = [s.value for s in valid_transitions]
427
+
428
+ if workflow_valid:
429
+ # Workflow is valid, so this must be a parent constraint violation
430
+ # Get children to determine max child state
431
+ from ....core.models import Task
432
+
433
+ if isinstance(ticket, Task) and ticket.children:
434
+ try:
435
+ children = await adapter.list_tasks_by_issue(ticket_id)
436
+ if children:
437
+ max_child_state = None
438
+ max_child_level = 0
439
+ for child in children:
440
+ child_state = child.state
441
+ if isinstance(child_state, str):
442
+ try:
443
+ child_state = TicketState(child_state)
444
+ except ValueError:
445
+ continue
446
+ child_level = child_state.completion_level()
447
+ if child_level > max_child_level:
448
+ max_child_level = child_level
449
+ max_child_state = child_state
450
+
451
+ return {
452
+ **response,
453
+ "status": "error",
454
+ "error": f"Cannot transition to '{target_state.value}': parent issue has children in higher completion states",
455
+ "reason": "parent_constraint_violation",
456
+ "max_child_state": (
457
+ max_child_state.value if max_child_state else None
458
+ ),
459
+ "message": f"Cannot transition to {target_state.value}: "
460
+ f"parent issue has children in higher completion states (max child state: {max_child_state.value if max_child_state else 'unknown'}). "
461
+ f"Please update child states first.",
462
+ "valid_transitions": valid_values,
463
+ }
464
+ except Exception:
465
+ # Fallback to generic message if we can't determine child states
466
+ pass
467
+
468
+ # Generic parent constraint violation message
469
+ return {
470
+ **response,
471
+ "status": "error",
472
+ "error": f"Cannot transition to '{target_state.value}': parent/child state constraint violation",
473
+ "reason": "parent_constraint_violation",
474
+ "message": f"Cannot transition to {target_state.value}: "
475
+ f"parent issue has children in higher completion states. Please update child states first.",
476
+ "valid_transitions": valid_values,
477
+ }
478
+ else:
479
+ # Workflow violation
480
+ return {
481
+ **response,
482
+ "status": "error",
483
+ "error": f"Invalid transition from '{current_state.value}' to '{target_state.value}'",
484
+ "reason": "workflow_violation",
485
+ "valid_transitions": valid_values,
486
+ "message": f"Cannot transition from {current_state.value} to {target_state.value}. "
487
+ f"Valid transitions: {', '.join(valid_values) if valid_values else 'none (terminal state)'}",
488
+ }
489
+
490
+ # Update ticket state
491
+ updated = await adapter.update(ticket_id, {"state": target_state})
492
+
493
+ if updated is None:
494
+ return {
495
+ **response,
496
+ "status": "error",
497
+ "error": f"Failed to update ticket {ticket_id}",
498
+ }
499
+
500
+ # Add comment if provided and adapter supports it
501
+ comment_added = False
502
+ if comment and hasattr(adapter, "add_comment"):
503
+ try:
504
+ await adapter.add_comment(ticket_id, comment)
505
+ comment_added = True
506
+ except Exception:
507
+ # Log but don't fail the transition
508
+ comment_added = False
509
+
510
+ return {
511
+ **response,
512
+ **_build_adapter_metadata(adapter, ticket_id),
513
+ "status": "completed",
514
+ "ticket": updated.model_dump(),
515
+ "previous_state": current_state.value,
516
+ "new_state": target_state.value,
517
+ "comment_added": comment_added,
518
+ "message": f"Ticket {ticket_id} transitioned from {current_state.value} to {target_state.value}",
519
+ }
520
+ except Exception as e:
521
+ return {
522
+ "status": "error",
523
+ "error": f"Failed to transition ticket: {str(e)}",
524
+ }
525
+
526
+
527
+ def _get_state_description(state: TicketState) -> str:
528
+ """Get human-readable description of a state.
529
+
530
+ Args:
531
+ state: TicketState to describe
532
+
533
+ Returns:
534
+ Description string
535
+
536
+ """
537
+ descriptions = {
538
+ TicketState.OPEN: "Work not yet started, in backlog",
539
+ TicketState.IN_PROGRESS: "Work is actively being done",
540
+ TicketState.READY: "Work complete, ready for review or testing",
541
+ TicketState.TESTED: "Work has been tested and verified",
542
+ TicketState.DONE: "Work is complete and accepted",
543
+ TicketState.WAITING: "Work paused, waiting for external dependency",
544
+ TicketState.BLOCKED: "Work blocked by an impediment",
545
+ TicketState.CLOSED: "Ticket closed or archived (final state)",
546
+ }
547
+ return descriptions.get(state, "")
@@ -1,5 +1,6 @@
1
1
  """Async queue system for mcp-ticketer."""
2
2
 
3
+ # Import manager last to avoid circular import
3
4
  from .manager import WorkerManager
4
5
  from .queue import Queue, QueueItem, QueueStatus
5
6
  from .worker import Worker