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.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {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 listing assigned tickets and transitioning tickets through workflow
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.project_config import ConfigResolver, TicketerConfig
29
+ from ....core.state_matcher import get_state_matcher
33
30
  from ..server_sdk import get_adapter, mcp
34
31
 
35
32
 
36
- def get_config_resolver() -> ConfigResolver:
37
- """Get configuration resolver for current project.
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
- """Get tickets assigned to the configured default user.
52
-
53
- Retrieves tickets assigned to the user specified in default_user configuration.
54
- Requires default_user to be set via config_set_default_user().
55
-
56
- Args:
57
- state: Optional state filter - must be one of: open, in_progress, ready,
58
- tested, done, closed, waiting, blocked
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 a ticket based on workflow rules.
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
- "ticket_id": ticket_id,
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 optional comment.
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
- # Validate transition
339
- if not current_state.can_transition_to(target_state):
340
- valid_transitions = TicketState.valid_transitions().get(current_state, [])
341
- valid_values = [s.value for s in valid_transitions]
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
- "status": "error",
344
- "error": f"Invalid transition from '{current_state.value}' to '{target_state.value}'",
345
- "current_state": current_state.value,
346
- "valid_transitions": valid_values,
347
- "message": f"Cannot transition from {current_state.value} to {target_state.value}. "
348
- f"Valid transitions: {', '.join(valid_values) if valid_values else 'none (terminal state)'}",
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
- return {
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, "")
@@ -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] = []
@@ -0,0 +1,5 @@
1
+ """Utility modules for mcp-ticketer."""
2
+
3
+ from .token_utils import estimate_json_tokens, estimate_tokens, paginate_response
4
+
5
+ __all__ = ["estimate_tokens", "estimate_json_tokens", "paginate_response"]