mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- 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/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- 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 +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""User-specific ticket management tools.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing tickets from a user's perspective,
|
|
4
|
+
including transitioning tickets through workflow states with validation.
|
|
5
|
+
|
|
6
|
+
Design Decision: Workflow State Validation
|
|
7
|
+
------------------------------------------
|
|
8
|
+
State transitions are validated using TicketState.can_transition_to() to ensure
|
|
9
|
+
tickets follow the defined workflow. This prevents invalid state changes that
|
|
10
|
+
could break integrations or confuse team members.
|
|
11
|
+
|
|
12
|
+
Valid workflow transitions:
|
|
13
|
+
- OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
|
|
14
|
+
- IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
|
|
15
|
+
- READY → TESTED, IN_PROGRESS, BLOCKED
|
|
16
|
+
- TESTED → DONE, IN_PROGRESS
|
|
17
|
+
- DONE → CLOSED
|
|
18
|
+
- WAITING/BLOCKED → OPEN, IN_PROGRESS, CLOSED
|
|
19
|
+
- CLOSED → (no transitions, terminal state)
|
|
20
|
+
|
|
21
|
+
Performance Considerations:
|
|
22
|
+
- State transition validation is O(1) lookup in predefined state machine
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from ....core.adapter import BaseAdapter
|
|
28
|
+
from ....core.models import TicketState
|
|
29
|
+
from ....core.state_matcher import get_state_matcher
|
|
30
|
+
from ..server_sdk import get_adapter, mcp
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_adapter_metadata(
|
|
34
|
+
adapter: BaseAdapter,
|
|
35
|
+
ticket_id: str | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
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
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mcp.tool()
|
|
48
|
+
async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
|
|
49
|
+
"""Get valid next states for ticket based on workflow state machine.
|
|
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
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
# Get ticket from adapter
|
|
57
|
+
adapter = get_adapter()
|
|
58
|
+
ticket = await adapter.read(ticket_id)
|
|
59
|
+
|
|
60
|
+
if ticket is None:
|
|
61
|
+
return {
|
|
62
|
+
"status": "error",
|
|
63
|
+
"error": f"Ticket {ticket_id} not found",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Get current state
|
|
67
|
+
current_state = ticket.state
|
|
68
|
+
|
|
69
|
+
# Get valid transitions from state machine
|
|
70
|
+
valid_transitions = TicketState.valid_transitions()
|
|
71
|
+
# Handle both TicketState enum and string values
|
|
72
|
+
if isinstance(current_state, str):
|
|
73
|
+
current_state = TicketState(current_state)
|
|
74
|
+
available = valid_transitions.get(current_state, [])
|
|
75
|
+
|
|
76
|
+
# Create human-readable descriptions
|
|
77
|
+
descriptions = {
|
|
78
|
+
TicketState.OPEN: "Move to backlog (not yet started)",
|
|
79
|
+
TicketState.IN_PROGRESS: "Begin active work on ticket",
|
|
80
|
+
TicketState.READY: "Mark as complete and ready for review/testing",
|
|
81
|
+
TicketState.TESTED: "Mark as tested and verified",
|
|
82
|
+
TicketState.DONE: "Mark as complete and accepted",
|
|
83
|
+
TicketState.WAITING: "Pause work while waiting for external dependency",
|
|
84
|
+
TicketState.BLOCKED: "Work is blocked by an impediment",
|
|
85
|
+
TicketState.CLOSED: "Close and archive ticket (final state)",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
transition_descriptions = {
|
|
89
|
+
state.value: descriptions.get(state, "") for state in available
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"status": "completed",
|
|
94
|
+
**_build_adapter_metadata(adapter, ticket_id),
|
|
95
|
+
"current_state": current_state.value,
|
|
96
|
+
"available_transitions": [state.value for state in available],
|
|
97
|
+
"transition_descriptions": transition_descriptions,
|
|
98
|
+
"is_terminal": len(available) == 0,
|
|
99
|
+
}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {
|
|
102
|
+
"status": "error",
|
|
103
|
+
"error": f"Failed to get available transitions: {str(e)}",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@mcp.tool()
|
|
108
|
+
async def ticket_transition(
|
|
109
|
+
ticket_id: str,
|
|
110
|
+
to_state: str,
|
|
111
|
+
comment: str | None = None,
|
|
112
|
+
auto_confirm: bool = True,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Move ticket through workflow with validation and semantic matching (natural language support).
|
|
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
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# Get ticket from adapter
|
|
122
|
+
adapter = get_adapter()
|
|
123
|
+
ticket = await adapter.read(ticket_id)
|
|
124
|
+
|
|
125
|
+
if ticket is None:
|
|
126
|
+
return {
|
|
127
|
+
"status": "error",
|
|
128
|
+
"error": f"Ticket {ticket_id} not found",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Store current state for response
|
|
132
|
+
current_state = ticket.state
|
|
133
|
+
# Handle both TicketState enum and string values
|
|
134
|
+
if isinstance(current_state, str):
|
|
135
|
+
current_state = TicketState(current_state)
|
|
136
|
+
|
|
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)
|
|
154
|
+
return {
|
|
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,
|
|
175
|
+
}
|
|
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
|
+
|
|
249
|
+
# Update ticket state
|
|
250
|
+
updated = await adapter.update(ticket_id, {"state": target_state})
|
|
251
|
+
|
|
252
|
+
if updated is None:
|
|
253
|
+
return {
|
|
254
|
+
**response,
|
|
255
|
+
"status": "error",
|
|
256
|
+
"error": f"Failed to update ticket {ticket_id}",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Add comment if provided and adapter supports it
|
|
260
|
+
comment_added = False
|
|
261
|
+
if comment and hasattr(adapter, "add_comment"):
|
|
262
|
+
try:
|
|
263
|
+
await adapter.add_comment(ticket_id, comment)
|
|
264
|
+
comment_added = True
|
|
265
|
+
except Exception:
|
|
266
|
+
# Log but don't fail the transition
|
|
267
|
+
comment_added = False
|
|
268
|
+
|
|
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),
|
|
324
|
+
"status": "completed",
|
|
325
|
+
"ticket": updated.model_dump(),
|
|
326
|
+
"previous_state": current_state.value,
|
|
327
|
+
"new_state": target_state.value,
|
|
328
|
+
"comment_added": comment_added,
|
|
329
|
+
"message": f"Ticket {ticket_id} transitioned from {current_state.value} to {target_state.value}",
|
|
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
|
|
337
|
+
except Exception as e:
|
|
338
|
+
return {
|
|
339
|
+
"status": "error",
|
|
340
|
+
"error": f"Failed to transition ticket: {str(e)}",
|
|
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, "")
|