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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -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.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- 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/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- 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 +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- 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/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- 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/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 +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
"""Hierarchy management tools for Epic/Issue/Task structure.
|
|
2
|
+
|
|
3
|
+
This module implements tools for managing the three-level ticket hierarchy:
|
|
4
|
+
- Epic: Strategic level containers
|
|
5
|
+
- Issue: Standard work items
|
|
6
|
+
- Task: Sub-work items
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ....core.adapter import BaseAdapter
|
|
14
|
+
from ....core.models import Epic, Priority, Task, TicketType
|
|
15
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
16
|
+
from ..server_sdk import get_adapter, mcp
|
|
17
|
+
from .ticket_tools import detect_and_apply_labels
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_adapter_metadata(
|
|
21
|
+
adapter: BaseAdapter,
|
|
22
|
+
ticket_id: str | None = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
"""Build adapter metadata for MCP responses.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
adapter: The adapter that handled the operation
|
|
28
|
+
ticket_id: Optional ticket ID to include in metadata
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary with adapter metadata fields
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
metadata = {
|
|
35
|
+
"adapter": adapter.adapter_type,
|
|
36
|
+
"adapter_name": adapter.adapter_display_name,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ticket_id:
|
|
40
|
+
metadata["ticket_id"] = ticket_id
|
|
41
|
+
|
|
42
|
+
return metadata
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
async def epic_create(
|
|
47
|
+
title: str,
|
|
48
|
+
description: str = "",
|
|
49
|
+
target_date: str | None = None,
|
|
50
|
+
lead_id: str | None = None,
|
|
51
|
+
child_issues: list[str] | None = None,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Create a new epic (strategic level container).
|
|
54
|
+
|
|
55
|
+
Adapter Support: All adapters support epic creation
|
|
56
|
+
- Linear: Creates project with timeline (via create())
|
|
57
|
+
- JIRA: Creates epic in configured project (dedicated create_epic())
|
|
58
|
+
- GitHub: Creates milestone (via create_milestone())
|
|
59
|
+
- Asana: Creates project (dedicated create_epic())
|
|
60
|
+
- AiTrackDown: Creates epic in local storage (dedicated create_epic())
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
title: Epic title (required)
|
|
64
|
+
description: Detailed description of the epic
|
|
65
|
+
target_date: Target completion date in ISO format (YYYY-MM-DD)
|
|
66
|
+
lead_id: User ID or email of the epic lead
|
|
67
|
+
child_issues: List of existing issue IDs to link to this epic
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Created epic details including ID and metadata, or error information
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
adapter = get_adapter()
|
|
75
|
+
|
|
76
|
+
# Parse target date if provided
|
|
77
|
+
target_datetime = None
|
|
78
|
+
if target_date:
|
|
79
|
+
try:
|
|
80
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
81
|
+
except ValueError:
|
|
82
|
+
return {
|
|
83
|
+
"status": "error",
|
|
84
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Create epic object
|
|
88
|
+
epic = Epic(
|
|
89
|
+
title=title,
|
|
90
|
+
description=description or "",
|
|
91
|
+
due_date=target_datetime,
|
|
92
|
+
assignee=lead_id,
|
|
93
|
+
child_issues=child_issues or [],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Create via adapter
|
|
97
|
+
created = await adapter.create(epic)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"status": "completed",
|
|
101
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
102
|
+
"epic": created.model_dump(),
|
|
103
|
+
}
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {
|
|
106
|
+
"status": "error",
|
|
107
|
+
"error": f"Failed to create epic: {str(e)}",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
async def epic_get(epic_id: str) -> dict[str, Any]:
|
|
113
|
+
"""Read an epic by its ID.
|
|
114
|
+
|
|
115
|
+
This tool retrieves detailed information about a specific epic/project/milestone.
|
|
116
|
+
|
|
117
|
+
Adapter Support: All adapters support reading epics
|
|
118
|
+
- Linear: Reads project details (via read())
|
|
119
|
+
- JIRA: Reads epic with dedicated get_epic() method
|
|
120
|
+
- GitHub: Reads milestone (via read())
|
|
121
|
+
- Asana: Reads project with dedicated get_epic() method
|
|
122
|
+
- AiTrackDown: Reads epic with dedicated get_epic() method
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
epic_id: Unique identifier of the epic to retrieve
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Epic details if found, or error information
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
adapter = get_adapter()
|
|
133
|
+
|
|
134
|
+
# Use adapter's get_epic method if available (optimized for some adapters)
|
|
135
|
+
if hasattr(adapter, "get_epic"):
|
|
136
|
+
epic = await adapter.get_epic(epic_id)
|
|
137
|
+
else:
|
|
138
|
+
# Fallback to generic read method
|
|
139
|
+
epic = await adapter.read(epic_id)
|
|
140
|
+
|
|
141
|
+
if epic is None:
|
|
142
|
+
return {
|
|
143
|
+
"status": "error",
|
|
144
|
+
"error": f"Epic {epic_id} not found",
|
|
145
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"status": "completed",
|
|
150
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
151
|
+
"epic": epic.model_dump(),
|
|
152
|
+
}
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return {
|
|
155
|
+
"status": "error",
|
|
156
|
+
"error": f"Failed to get epic: {str(e)}",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
async def epic_list(
|
|
162
|
+
limit: int = 10,
|
|
163
|
+
offset: int = 0,
|
|
164
|
+
state: str | None = None,
|
|
165
|
+
include_completed: bool = False,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""List all epics with pagination and optional filtering.
|
|
168
|
+
|
|
169
|
+
Adapter Support: All adapters support listing epics
|
|
170
|
+
- Linear: Optimized list_epics() with state filter and include_completed
|
|
171
|
+
- JIRA: Optimized list_epics() with state filtering (mapped to JIRA status)
|
|
172
|
+
- GitHub: Generic list() method (state filter not supported)
|
|
173
|
+
- Asana: Optimized list_epics() method (state filter not supported)
|
|
174
|
+
- AiTrackDown: Optimized list_epics() with basic pagination
|
|
175
|
+
|
|
176
|
+
Adapter-Specific Parameters:
|
|
177
|
+
- state: Supported by Linear (e.g., "planned", "started", "completed") and JIRA (e.g., "To Do", "In Progress", "Done")
|
|
178
|
+
- include_completed: Linear-specific parameter to include/exclude completed projects
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
limit: Maximum number of epics to return (default: 10)
|
|
182
|
+
offset: Number of epics to skip for pagination (default: 0)
|
|
183
|
+
state: Optional state filter - adapter-specific behavior
|
|
184
|
+
include_completed: Include completed epics (Linear-specific, default: False)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of epics with adapter information, or error information
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
adapter = get_adapter()
|
|
192
|
+
|
|
193
|
+
# Check if adapter has optimized list_epics method
|
|
194
|
+
if hasattr(adapter, "list_epics"):
|
|
195
|
+
# Build kwargs for adapter-specific parameters
|
|
196
|
+
kwargs: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
197
|
+
|
|
198
|
+
# Add state filter if supported
|
|
199
|
+
if state is not None:
|
|
200
|
+
kwargs["state"] = state
|
|
201
|
+
|
|
202
|
+
# Add include_completed for Linear adapter
|
|
203
|
+
adapter_type = adapter.adapter_type.lower()
|
|
204
|
+
if adapter_type == "linear" and include_completed:
|
|
205
|
+
kwargs["include_completed"] = include_completed
|
|
206
|
+
|
|
207
|
+
epics = await adapter.list_epics(**kwargs)
|
|
208
|
+
else:
|
|
209
|
+
# Fallback to generic list method with epic filter
|
|
210
|
+
filters = {"ticket_type": TicketType.EPIC}
|
|
211
|
+
if state is not None:
|
|
212
|
+
filters["state"] = state
|
|
213
|
+
epics = await adapter.list(limit=limit, offset=offset, filters=filters)
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"status": "completed",
|
|
217
|
+
**_build_adapter_metadata(adapter),
|
|
218
|
+
"epics": [epic.model_dump() for epic in epics],
|
|
219
|
+
"count": len(epics),
|
|
220
|
+
"limit": limit,
|
|
221
|
+
"offset": offset,
|
|
222
|
+
"filters_applied": {
|
|
223
|
+
"state": state,
|
|
224
|
+
"include_completed": include_completed,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return {
|
|
229
|
+
"status": "error",
|
|
230
|
+
"error": f"Failed to list epics: {str(e)}",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@mcp.tool()
|
|
235
|
+
async def epic_issues(epic_id: str) -> dict[str, Any]:
|
|
236
|
+
"""Get all issues belonging to an epic.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
epic_id: Unique identifier of the epic
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of issues in the epic, or error information
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
adapter = get_adapter()
|
|
247
|
+
|
|
248
|
+
# Read the epic to get child issue IDs
|
|
249
|
+
epic = await adapter.read(epic_id)
|
|
250
|
+
if epic is None:
|
|
251
|
+
return {
|
|
252
|
+
"status": "error",
|
|
253
|
+
"error": f"Epic {epic_id} not found",
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# If epic has no child_issues attribute, use empty list
|
|
257
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
258
|
+
|
|
259
|
+
# Fetch each child issue
|
|
260
|
+
issues = []
|
|
261
|
+
for issue_id in child_issue_ids:
|
|
262
|
+
issue = await adapter.read(issue_id)
|
|
263
|
+
if issue:
|
|
264
|
+
issues.append(issue.model_dump())
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"status": "completed",
|
|
268
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
269
|
+
"issues": issues,
|
|
270
|
+
"count": len(issues),
|
|
271
|
+
}
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return {
|
|
274
|
+
"status": "error",
|
|
275
|
+
"error": f"Failed to get epic issues: {str(e)}",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
async def issue_create(
|
|
281
|
+
title: str,
|
|
282
|
+
description: str = "",
|
|
283
|
+
epic_id: str | None = None,
|
|
284
|
+
assignee: str | None = None,
|
|
285
|
+
priority: str = "medium",
|
|
286
|
+
tags: list[str] | None = None,
|
|
287
|
+
auto_detect_labels: bool = True,
|
|
288
|
+
) -> dict[str, Any]:
|
|
289
|
+
"""Create a new issue (standard work item) with automatic label detection.
|
|
290
|
+
|
|
291
|
+
This tool automatically scans available labels/tags and intelligently
|
|
292
|
+
applies relevant ones based on the issue title and description.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
title: Issue title (required)
|
|
296
|
+
description: Detailed description of the issue
|
|
297
|
+
epic_id: Parent epic ID to link this issue to
|
|
298
|
+
assignee: User ID or email to assign the issue to
|
|
299
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
300
|
+
tags: List of tags to categorize the issue (auto-detection adds to these)
|
|
301
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Created issue details including ID and metadata, or error information
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
adapter = get_adapter()
|
|
309
|
+
|
|
310
|
+
# Validate and convert priority
|
|
311
|
+
try:
|
|
312
|
+
priority_enum = Priority(priority.lower())
|
|
313
|
+
except ValueError:
|
|
314
|
+
return {
|
|
315
|
+
"status": "error",
|
|
316
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Use default_user if no assignee specified
|
|
320
|
+
final_assignee = assignee
|
|
321
|
+
if final_assignee is None:
|
|
322
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
323
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
324
|
+
if config.default_user:
|
|
325
|
+
final_assignee = config.default_user
|
|
326
|
+
|
|
327
|
+
# Use default_project if no epic_id specified
|
|
328
|
+
final_epic_id = epic_id
|
|
329
|
+
if final_epic_id is None:
|
|
330
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
331
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
332
|
+
# Try default_project first, fall back to default_epic
|
|
333
|
+
if config.default_project:
|
|
334
|
+
final_epic_id = config.default_project
|
|
335
|
+
elif config.default_epic:
|
|
336
|
+
final_epic_id = config.default_epic
|
|
337
|
+
|
|
338
|
+
# Auto-detect labels if enabled
|
|
339
|
+
final_tags = tags
|
|
340
|
+
if auto_detect_labels:
|
|
341
|
+
final_tags = await detect_and_apply_labels(
|
|
342
|
+
adapter, title, description or "", tags
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Create issue (Task with ISSUE type)
|
|
346
|
+
issue = Task(
|
|
347
|
+
title=title,
|
|
348
|
+
description=description or "",
|
|
349
|
+
ticket_type=TicketType.ISSUE,
|
|
350
|
+
parent_epic=final_epic_id,
|
|
351
|
+
assignee=final_assignee,
|
|
352
|
+
priority=priority_enum,
|
|
353
|
+
tags=final_tags or [],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Create via adapter
|
|
357
|
+
created = await adapter.create(issue)
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
"status": "completed",
|
|
361
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
362
|
+
"issue": created.model_dump(),
|
|
363
|
+
"labels_applied": created.tags or [],
|
|
364
|
+
"auto_detected": auto_detect_labels,
|
|
365
|
+
}
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return {
|
|
368
|
+
"status": "error",
|
|
369
|
+
"error": f"Failed to create issue: {str(e)}",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@mcp.tool()
|
|
374
|
+
async def issue_get_parent(issue_id: str) -> dict[str, Any]:
|
|
375
|
+
"""Get the parent issue of a sub-issue.
|
|
376
|
+
|
|
377
|
+
This tool retrieves the parent issue details for a given sub-issue ID.
|
|
378
|
+
Returns None if the issue has no parent (i.e., it's a top-level issue).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
issue_id: Unique identifier of the sub-issue (e.g., "ENG-842", UUID)
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Dictionary containing:
|
|
385
|
+
- status: "completed" or "error"
|
|
386
|
+
- parent: Parent issue details (dict) if exists, None if no parent
|
|
387
|
+
- adapter: Adapter type that handled the operation
|
|
388
|
+
- adapter_name: Human-readable adapter name
|
|
389
|
+
- error: Error message (if failed)
|
|
390
|
+
|
|
391
|
+
Example response (has parent):
|
|
392
|
+
{
|
|
393
|
+
"status": "completed",
|
|
394
|
+
"parent": {
|
|
395
|
+
"id": "abc-123",
|
|
396
|
+
"identifier": "ENG-840",
|
|
397
|
+
"title": "Implement hierarchy features",
|
|
398
|
+
"state": "in_progress",
|
|
399
|
+
...
|
|
400
|
+
},
|
|
401
|
+
"adapter": "linear",
|
|
402
|
+
"adapter_name": "Linear"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Example response (no parent):
|
|
406
|
+
{
|
|
407
|
+
"status": "completed",
|
|
408
|
+
"parent": None,
|
|
409
|
+
"adapter": "linear",
|
|
410
|
+
"adapter_name": "Linear"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
adapter = get_adapter()
|
|
416
|
+
|
|
417
|
+
# Read the issue to check if it has a parent
|
|
418
|
+
issue = await adapter.read(issue_id)
|
|
419
|
+
if issue is None:
|
|
420
|
+
return {
|
|
421
|
+
"status": "error",
|
|
422
|
+
"error": f"Issue {issue_id} not found",
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Check for parent_issue attribute (sub-issues have this set)
|
|
426
|
+
parent_issue_id = getattr(issue, "parent_issue", None)
|
|
427
|
+
|
|
428
|
+
if not parent_issue_id:
|
|
429
|
+
# No parent - this is a top-level issue
|
|
430
|
+
return {
|
|
431
|
+
"status": "completed",
|
|
432
|
+
**_build_adapter_metadata(adapter, issue_id),
|
|
433
|
+
"parent": None,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Fetch parent issue details
|
|
437
|
+
parent_issue = await adapter.read(parent_issue_id)
|
|
438
|
+
if parent_issue is None:
|
|
439
|
+
return {
|
|
440
|
+
"status": "error",
|
|
441
|
+
"error": f"Parent issue {parent_issue_id} not found",
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
"status": "completed",
|
|
446
|
+
**_build_adapter_metadata(adapter, issue_id),
|
|
447
|
+
"parent": parent_issue.model_dump(),
|
|
448
|
+
}
|
|
449
|
+
except Exception as e:
|
|
450
|
+
return {
|
|
451
|
+
"status": "error",
|
|
452
|
+
"error": f"Failed to get parent issue: {str(e)}",
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@mcp.tool()
|
|
457
|
+
async def issue_tasks(
|
|
458
|
+
issue_id: str,
|
|
459
|
+
state: str | None = None,
|
|
460
|
+
assignee: str | None = None,
|
|
461
|
+
priority: str | None = None,
|
|
462
|
+
) -> dict[str, Any]:
|
|
463
|
+
"""Get all tasks (sub-items) belonging to an issue with optional filtering.
|
|
464
|
+
|
|
465
|
+
This tool retrieves child tasks/sub-issues for a given issue ID, with support
|
|
466
|
+
for filtering by state, assignee, and priority. All filter parameters are optional.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
issue_id: Unique identifier of the issue
|
|
470
|
+
state: Optional state filter - must be one of: open, in_progress, ready,
|
|
471
|
+
tested, done, closed, waiting, blocked
|
|
472
|
+
assignee: Optional user ID or email to filter by assignee
|
|
473
|
+
priority: Optional priority filter - must be one of: low, medium, high, critical
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Dictionary containing:
|
|
477
|
+
- status: "completed" or "error"
|
|
478
|
+
- tasks: List of task objects matching filters
|
|
479
|
+
- count: Number of tasks returned
|
|
480
|
+
- filters_applied: Dict showing which filters were used
|
|
481
|
+
- adapter: Adapter type that handled the operation
|
|
482
|
+
- error: Error message (if failed)
|
|
483
|
+
|
|
484
|
+
Example:
|
|
485
|
+
# Get all tasks for issue
|
|
486
|
+
result = issue_tasks("ENG-840")
|
|
487
|
+
|
|
488
|
+
# Get only in-progress tasks assigned to user
|
|
489
|
+
result = issue_tasks("ENG-840", state="in_progress", assignee="user@example.com")
|
|
490
|
+
|
|
491
|
+
# Get high priority tasks
|
|
492
|
+
result = issue_tasks("ENG-840", priority="high")
|
|
493
|
+
|
|
494
|
+
"""
|
|
495
|
+
try:
|
|
496
|
+
adapter = get_adapter()
|
|
497
|
+
|
|
498
|
+
# Validate filter parameters
|
|
499
|
+
filters_applied = {}
|
|
500
|
+
|
|
501
|
+
# Validate state if provided
|
|
502
|
+
if state is not None:
|
|
503
|
+
try:
|
|
504
|
+
from ....core.models import TicketState
|
|
505
|
+
|
|
506
|
+
state_enum = TicketState(state.lower())
|
|
507
|
+
filters_applied["state"] = state_enum.value
|
|
508
|
+
except ValueError:
|
|
509
|
+
return {
|
|
510
|
+
"status": "error",
|
|
511
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Validate priority if provided
|
|
515
|
+
if priority is not None:
|
|
516
|
+
try:
|
|
517
|
+
from ....core.models import Priority
|
|
518
|
+
|
|
519
|
+
priority_enum = Priority(priority.lower())
|
|
520
|
+
filters_applied["priority"] = priority_enum.value
|
|
521
|
+
except ValueError:
|
|
522
|
+
return {
|
|
523
|
+
"status": "error",
|
|
524
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if assignee is not None:
|
|
528
|
+
filters_applied["assignee"] = assignee
|
|
529
|
+
|
|
530
|
+
# Read the issue to get child task IDs
|
|
531
|
+
issue = await adapter.read(issue_id)
|
|
532
|
+
if issue is None:
|
|
533
|
+
return {
|
|
534
|
+
"status": "error",
|
|
535
|
+
"error": f"Issue {issue_id} not found",
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
# Get child task IDs
|
|
539
|
+
child_task_ids = getattr(issue, "children", [])
|
|
540
|
+
|
|
541
|
+
# Fetch each child task
|
|
542
|
+
tasks = []
|
|
543
|
+
for task_id in child_task_ids:
|
|
544
|
+
task = await adapter.read(task_id)
|
|
545
|
+
if task:
|
|
546
|
+
# Apply filters
|
|
547
|
+
should_include = True
|
|
548
|
+
|
|
549
|
+
# Filter by state
|
|
550
|
+
if state is not None:
|
|
551
|
+
task_state = getattr(task, "state", None)
|
|
552
|
+
# Handle case where state might be stored as string
|
|
553
|
+
if isinstance(task_state, str):
|
|
554
|
+
should_include = should_include and (
|
|
555
|
+
task_state.lower() == state.lower()
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
should_include = should_include and (task_state == state_enum)
|
|
559
|
+
|
|
560
|
+
# Filter by priority
|
|
561
|
+
if priority is not None:
|
|
562
|
+
task_priority = getattr(task, "priority", None)
|
|
563
|
+
# Handle case where priority might be stored as string
|
|
564
|
+
if isinstance(task_priority, str):
|
|
565
|
+
should_include = should_include and (
|
|
566
|
+
task_priority.lower() == priority.lower()
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
should_include = should_include and (
|
|
570
|
+
task_priority == priority_enum
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Filter by assignee
|
|
574
|
+
if assignee is not None:
|
|
575
|
+
task_assignee = getattr(task, "assignee", None)
|
|
576
|
+
# Case-insensitive comparison for emails/usernames
|
|
577
|
+
should_include = should_include and (
|
|
578
|
+
task_assignee is not None
|
|
579
|
+
and assignee.lower() in str(task_assignee).lower()
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if should_include:
|
|
583
|
+
tasks.append(task.model_dump())
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
"status": "completed",
|
|
587
|
+
**_build_adapter_metadata(adapter, issue_id),
|
|
588
|
+
"tasks": tasks,
|
|
589
|
+
"count": len(tasks),
|
|
590
|
+
"filters_applied": filters_applied,
|
|
591
|
+
}
|
|
592
|
+
except Exception as e:
|
|
593
|
+
return {
|
|
594
|
+
"status": "error",
|
|
595
|
+
"error": f"Failed to get issue tasks: {str(e)}",
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@mcp.tool()
|
|
600
|
+
async def task_create(
|
|
601
|
+
title: str,
|
|
602
|
+
description: str = "",
|
|
603
|
+
issue_id: str | None = None,
|
|
604
|
+
assignee: str | None = None,
|
|
605
|
+
priority: str = "medium",
|
|
606
|
+
tags: list[str] | None = None,
|
|
607
|
+
auto_detect_labels: bool = True,
|
|
608
|
+
) -> dict[str, Any]:
|
|
609
|
+
"""Create a new task (sub-work item) with automatic label detection.
|
|
610
|
+
|
|
611
|
+
This tool automatically scans available labels/tags and intelligently
|
|
612
|
+
applies relevant ones based on the task title and description.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
title: Task title (required)
|
|
616
|
+
description: Detailed description of the task
|
|
617
|
+
issue_id: Parent issue ID to link this task to
|
|
618
|
+
assignee: User ID or email to assign the task to
|
|
619
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
620
|
+
tags: List of tags to categorize the task (auto-detection adds to these)
|
|
621
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
Created task details including ID and metadata, or error information
|
|
625
|
+
|
|
626
|
+
"""
|
|
627
|
+
try:
|
|
628
|
+
adapter = get_adapter()
|
|
629
|
+
|
|
630
|
+
# Validate and convert priority
|
|
631
|
+
try:
|
|
632
|
+
priority_enum = Priority(priority.lower())
|
|
633
|
+
except ValueError:
|
|
634
|
+
return {
|
|
635
|
+
"status": "error",
|
|
636
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
# Use default_user if no assignee specified
|
|
640
|
+
final_assignee = assignee
|
|
641
|
+
if final_assignee is None:
|
|
642
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
643
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
644
|
+
if config.default_user:
|
|
645
|
+
final_assignee = config.default_user
|
|
646
|
+
|
|
647
|
+
# Auto-detect labels if enabled
|
|
648
|
+
final_tags = tags
|
|
649
|
+
if auto_detect_labels:
|
|
650
|
+
final_tags = await detect_and_apply_labels(
|
|
651
|
+
adapter, title, description or "", tags
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Create task (Task with TASK type)
|
|
655
|
+
task = Task(
|
|
656
|
+
title=title,
|
|
657
|
+
description=description or "",
|
|
658
|
+
ticket_type=TicketType.TASK,
|
|
659
|
+
parent_issue=issue_id,
|
|
660
|
+
assignee=final_assignee,
|
|
661
|
+
priority=priority_enum,
|
|
662
|
+
tags=final_tags or [],
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Create via adapter
|
|
666
|
+
created = await adapter.create(task)
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
"status": "completed",
|
|
670
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
671
|
+
"task": created.model_dump(),
|
|
672
|
+
"labels_applied": created.tags or [],
|
|
673
|
+
"auto_detected": auto_detect_labels,
|
|
674
|
+
}
|
|
675
|
+
except Exception as e:
|
|
676
|
+
return {
|
|
677
|
+
"status": "error",
|
|
678
|
+
"error": f"Failed to create task: {str(e)}",
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@mcp.tool()
|
|
683
|
+
async def epic_update(
|
|
684
|
+
epic_id: str,
|
|
685
|
+
title: str | None = None,
|
|
686
|
+
description: str | None = None,
|
|
687
|
+
state: str | None = None,
|
|
688
|
+
target_date: str | None = None,
|
|
689
|
+
) -> dict[str, Any]:
|
|
690
|
+
"""Update an existing epic's metadata and description.
|
|
691
|
+
|
|
692
|
+
Adapter Support: All adapters support epic updates with dedicated update_epic() method
|
|
693
|
+
- Linear: ✓ Updates project fields (title, description, state, target_date, etc.)
|
|
694
|
+
- JIRA: ✓ Updates epic fields (title, description, status, due date)
|
|
695
|
+
- GitHub: ✓ Updates milestone (title, description, state, due_on)
|
|
696
|
+
- Asana: ✓ Updates project metadata (name, notes, due_on, color, etc.)
|
|
697
|
+
- AiTrackDown: ✓ Updates epic in local storage
|
|
698
|
+
|
|
699
|
+
Supported Update Fields:
|
|
700
|
+
- title: Epic/project/milestone title (all adapters)
|
|
701
|
+
- description: Detailed description (all adapters)
|
|
702
|
+
- state: Epic state - adapter-specific values (Linear, JIRA, GitHub)
|
|
703
|
+
- target_date: Due date in ISO format YYYY-MM-DD (all adapters)
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
epic_id: Epic identifier (required)
|
|
707
|
+
title: New title for the epic
|
|
708
|
+
description: New description for the epic
|
|
709
|
+
state: New state (open, in_progress, done, closed)
|
|
710
|
+
target_date: Target completion date in ISO format (YYYY-MM-DD)
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
Updated epic details, or error information
|
|
714
|
+
|
|
715
|
+
"""
|
|
716
|
+
try:
|
|
717
|
+
adapter = get_adapter()
|
|
718
|
+
|
|
719
|
+
# Check if adapter supports epic updates
|
|
720
|
+
if not hasattr(adapter, "update_epic"):
|
|
721
|
+
adapter_name = adapter.adapter_display_name
|
|
722
|
+
return {
|
|
723
|
+
"status": "error",
|
|
724
|
+
"error": f"Epic updates not supported by {adapter_name} adapter",
|
|
725
|
+
"epic_id": epic_id,
|
|
726
|
+
"note": "This adapter should implement update_epic() method",
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
# Build updates dictionary
|
|
730
|
+
updates = {}
|
|
731
|
+
if title is not None:
|
|
732
|
+
updates["title"] = title
|
|
733
|
+
if description is not None:
|
|
734
|
+
updates["description"] = description
|
|
735
|
+
if state is not None:
|
|
736
|
+
updates["state"] = state
|
|
737
|
+
if target_date is not None:
|
|
738
|
+
# Parse target date if provided
|
|
739
|
+
try:
|
|
740
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
741
|
+
updates["target_date"] = target_datetime
|
|
742
|
+
except ValueError:
|
|
743
|
+
return {
|
|
744
|
+
"status": "error",
|
|
745
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if not updates:
|
|
749
|
+
return {
|
|
750
|
+
"status": "error",
|
|
751
|
+
"error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
# Update via adapter
|
|
755
|
+
updated = await adapter.update_epic(epic_id, updates)
|
|
756
|
+
|
|
757
|
+
if updated is None:
|
|
758
|
+
return {
|
|
759
|
+
"status": "error",
|
|
760
|
+
"error": f"Epic {epic_id} not found or update failed",
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
"status": "completed",
|
|
765
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
766
|
+
"epic": updated.model_dump(),
|
|
767
|
+
}
|
|
768
|
+
except AttributeError as e:
|
|
769
|
+
return {
|
|
770
|
+
"status": "error",
|
|
771
|
+
"error": f"Epic update method not available: {str(e)}",
|
|
772
|
+
"epic_id": epic_id,
|
|
773
|
+
}
|
|
774
|
+
except Exception as e:
|
|
775
|
+
return {
|
|
776
|
+
"status": "error",
|
|
777
|
+
"error": f"Failed to update epic: {str(e)}",
|
|
778
|
+
"epic_id": epic_id,
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@mcp.tool()
|
|
783
|
+
async def epic_delete(epic_id: str) -> dict[str, Any]:
|
|
784
|
+
"""Delete an epic/project/milestone by ID.
|
|
785
|
+
|
|
786
|
+
Adapter Support:
|
|
787
|
+
- GitHub: ✓ Deletes milestone (delete_epic() - permanent deletion)
|
|
788
|
+
- Asana: ✓ Archives project (delete_epic() - can be restored from archive)
|
|
789
|
+
- Linear: ✗ Linear API doesn't support project deletion
|
|
790
|
+
- JIRA: ✗ JIRA API doesn't support epic deletion
|
|
791
|
+
- AiTrackDown: ✗ Epic deletion not implemented yet
|
|
792
|
+
|
|
793
|
+
Important Notes:
|
|
794
|
+
- GitHub: Deletion is permanent and cannot be undone
|
|
795
|
+
- Asana: Project is archived, not deleted, and can be restored
|
|
796
|
+
- For unsupported adapters, the tool returns an error with details
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
epic_id: Unique identifier of the epic to delete
|
|
800
|
+
|
|
801
|
+
Returns:
|
|
802
|
+
Success/failure status with adapter information
|
|
803
|
+
|
|
804
|
+
"""
|
|
805
|
+
try:
|
|
806
|
+
adapter = get_adapter()
|
|
807
|
+
|
|
808
|
+
# Check if adapter supports epic deletion
|
|
809
|
+
if not hasattr(adapter, "delete_epic"):
|
|
810
|
+
adapter_name = adapter.adapter_display_name
|
|
811
|
+
return {
|
|
812
|
+
"status": "error",
|
|
813
|
+
"error": f"Epic deletion not supported by {adapter_name} adapter",
|
|
814
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
815
|
+
"supported_adapters": ["GitHub", "Asana"],
|
|
816
|
+
"note": f"{adapter_name} does not provide API support for deleting epics/projects",
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
# Call adapter's delete_epic method
|
|
820
|
+
success = await adapter.delete_epic(epic_id)
|
|
821
|
+
|
|
822
|
+
if not success:
|
|
823
|
+
return {
|
|
824
|
+
"status": "error",
|
|
825
|
+
"error": f"Failed to delete epic {epic_id}",
|
|
826
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
"status": "completed",
|
|
831
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
832
|
+
"message": f"Epic {epic_id} deleted successfully",
|
|
833
|
+
"deleted": True,
|
|
834
|
+
}
|
|
835
|
+
except AttributeError:
|
|
836
|
+
adapter_name = adapter.adapter_display_name
|
|
837
|
+
return {
|
|
838
|
+
"status": "error",
|
|
839
|
+
"error": f"Epic deletion not supported by {adapter_name} adapter",
|
|
840
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
841
|
+
"supported_adapters": ["GitHub", "Asana"],
|
|
842
|
+
}
|
|
843
|
+
except Exception as e:
|
|
844
|
+
return {
|
|
845
|
+
"status": "error",
|
|
846
|
+
"error": f"Failed to delete epic: {str(e)}",
|
|
847
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@mcp.tool()
|
|
852
|
+
async def hierarchy_tree(
|
|
853
|
+
epic_id: str,
|
|
854
|
+
max_depth: int = 3,
|
|
855
|
+
) -> dict[str, Any]:
|
|
856
|
+
"""Get complete hierarchy tree for an epic.
|
|
857
|
+
|
|
858
|
+
Retrieves the full hierarchy tree starting from an epic, including all
|
|
859
|
+
child issues and their tasks up to the specified depth.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
epic_id: Unique identifier of the root epic
|
|
863
|
+
max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
Complete hierarchy tree structure, or error information
|
|
867
|
+
|
|
868
|
+
"""
|
|
869
|
+
try:
|
|
870
|
+
adapter = get_adapter()
|
|
871
|
+
|
|
872
|
+
# Read the epic
|
|
873
|
+
epic = await adapter.read(epic_id)
|
|
874
|
+
if epic is None:
|
|
875
|
+
return {
|
|
876
|
+
"status": "error",
|
|
877
|
+
"error": f"Epic {epic_id} not found",
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
# Build tree structure
|
|
881
|
+
tree = {
|
|
882
|
+
"epic": epic.model_dump(),
|
|
883
|
+
"issues": [],
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if max_depth < 2:
|
|
887
|
+
return {
|
|
888
|
+
"status": "completed",
|
|
889
|
+
"tree": tree,
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
# Get child issues
|
|
893
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
894
|
+
for issue_id in child_issue_ids:
|
|
895
|
+
issue = await adapter.read(issue_id)
|
|
896
|
+
if issue:
|
|
897
|
+
issue_data = {
|
|
898
|
+
"issue": issue.model_dump(),
|
|
899
|
+
"tasks": [],
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if max_depth >= 3:
|
|
903
|
+
# Get child tasks
|
|
904
|
+
child_task_ids = getattr(issue, "children", [])
|
|
905
|
+
for task_id in child_task_ids:
|
|
906
|
+
task = await adapter.read(task_id)
|
|
907
|
+
if task:
|
|
908
|
+
issue_data["tasks"].append(task.model_dump())
|
|
909
|
+
|
|
910
|
+
tree["issues"].append(issue_data)
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
"status": "completed",
|
|
914
|
+
**_build_adapter_metadata(adapter, epic_id),
|
|
915
|
+
"tree": tree,
|
|
916
|
+
}
|
|
917
|
+
except Exception as e:
|
|
918
|
+
return {
|
|
919
|
+
"status": "error",
|
|
920
|
+
"error": f"Failed to build hierarchy tree: {str(e)}",
|
|
921
|
+
}
|