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,942 @@
|
|
|
1
|
+
"""Hierarchy management tools for Epic/Issue/Task structure (v2.0.0).
|
|
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
|
+
Version 2.0.0 changes:
|
|
9
|
+
- Removed all deprecated functions (epic_create, epic_get, epic_list, etc.)
|
|
10
|
+
- Single `hierarchy()` function provides all hierarchy operations
|
|
11
|
+
- All deprecated function logic has been inlined into the unified interface
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from ....core.adapter import BaseAdapter
|
|
19
|
+
from ....core.models import Epic, Priority, Task, TicketType
|
|
20
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
21
|
+
from ..server_sdk import get_adapter, mcp
|
|
22
|
+
from .ticket_tools import detect_and_apply_labels
|
|
23
|
+
|
|
24
|
+
# Sentinel value to distinguish between "parameter not provided" and "explicitly None"
|
|
25
|
+
_UNSET = object()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_adapter_metadata(
|
|
29
|
+
adapter: BaseAdapter,
|
|
30
|
+
ticket_id: str | None = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Build adapter metadata for MCP responses.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
adapter: The adapter that handled the operation
|
|
36
|
+
ticket_id: Optional ticket ID to include in metadata
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary with adapter metadata fields
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
metadata = {
|
|
43
|
+
"adapter": adapter.adapter_type,
|
|
44
|
+
"adapter_name": adapter.adapter_display_name,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ticket_id:
|
|
48
|
+
metadata["ticket_id"] = ticket_id
|
|
49
|
+
|
|
50
|
+
return metadata
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
async def hierarchy(
|
|
55
|
+
entity_type: Literal["epic", "issue", "task"],
|
|
56
|
+
action: Literal[
|
|
57
|
+
"create",
|
|
58
|
+
"get",
|
|
59
|
+
"list",
|
|
60
|
+
"update",
|
|
61
|
+
"delete",
|
|
62
|
+
"get_children",
|
|
63
|
+
"get_parent",
|
|
64
|
+
"get_tree",
|
|
65
|
+
],
|
|
66
|
+
# Entity identification
|
|
67
|
+
entity_id: str | None = None,
|
|
68
|
+
epic_id: str | None = None,
|
|
69
|
+
issue_id: str | None = None,
|
|
70
|
+
# Creation/Update parameters
|
|
71
|
+
title: str | None = None,
|
|
72
|
+
description: str = "",
|
|
73
|
+
# Epic-specific
|
|
74
|
+
target_date: str | None = None,
|
|
75
|
+
lead_id: str | None = None,
|
|
76
|
+
child_issues: list[str] | None = None,
|
|
77
|
+
# List parameters
|
|
78
|
+
project_id: str | None = None,
|
|
79
|
+
state: str | None = None,
|
|
80
|
+
limit: int = 10,
|
|
81
|
+
offset: int = 0,
|
|
82
|
+
include_completed: bool = False,
|
|
83
|
+
# Tree parameters
|
|
84
|
+
max_depth: int = 3,
|
|
85
|
+
# Task/Issue parameters
|
|
86
|
+
assignee: str | None = None,
|
|
87
|
+
priority: str = "medium",
|
|
88
|
+
tags: list[str] | None = None,
|
|
89
|
+
auto_detect_labels: bool = True,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""Unified hierarchy management tool for epics, issues, and tasks.
|
|
92
|
+
|
|
93
|
+
Consolidates 11 separate hierarchy tools into a single interface for
|
|
94
|
+
all CRUD operations and hierarchical relationships across the three-tier
|
|
95
|
+
structure: Epic → Issue → Task.
|
|
96
|
+
|
|
97
|
+
This tool replaces:
|
|
98
|
+
- epic_create, epic_get, epic_list, epic_update, epic_delete, epic_issues
|
|
99
|
+
- issue_create, issue_get_parent, issue_tasks
|
|
100
|
+
- task_create
|
|
101
|
+
- hierarchy_tree
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
entity_type: Type of entity - "epic", "issue", or "task"
|
|
105
|
+
action: Operation to perform - create, get, list, update, delete,
|
|
106
|
+
get_children, get_parent, or get_tree
|
|
107
|
+
entity_id: ID for get/update/delete operations
|
|
108
|
+
epic_id: Parent epic ID (for issues/tasks/get_children)
|
|
109
|
+
issue_id: Parent issue ID (for tasks/get_parent/get_children)
|
|
110
|
+
title: Title for create/update operations
|
|
111
|
+
description: Description for create/update operations
|
|
112
|
+
target_date: Target date for epics (ISO YYYY-MM-DD format)
|
|
113
|
+
lead_id: Lead user ID for epics
|
|
114
|
+
child_issues: List of child issue IDs for epics
|
|
115
|
+
project_id: Project filter for list operations
|
|
116
|
+
state: State filter for list operations
|
|
117
|
+
limit: Maximum results for list operations (default: 10)
|
|
118
|
+
offset: Pagination offset for list operations (default: 0)
|
|
119
|
+
include_completed: Include completed items in epic lists (default: False)
|
|
120
|
+
max_depth: Maximum depth for tree operations (1-3, default: 3)
|
|
121
|
+
assignee: Assigned user for issues/tasks
|
|
122
|
+
priority: Priority level - low, medium, high, critical (default: medium)
|
|
123
|
+
tags: Tags/labels for issues/tasks
|
|
124
|
+
auto_detect_labels: Auto-detect labels from title/description (default: True)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Operation results in standard format with status, data, and metadata
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If action/entity_type combination is invalid
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
# Create epic
|
|
134
|
+
await hierarchy(
|
|
135
|
+
entity_type="epic",
|
|
136
|
+
action="create",
|
|
137
|
+
title="Q4 Features",
|
|
138
|
+
description="New features for Q4",
|
|
139
|
+
target_date="2025-12-31"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Get epic details
|
|
143
|
+
await hierarchy(
|
|
144
|
+
entity_type="epic",
|
|
145
|
+
action="get",
|
|
146
|
+
entity_id="EPIC-123"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# List epics in project
|
|
150
|
+
await hierarchy(
|
|
151
|
+
entity_type="epic",
|
|
152
|
+
action="list",
|
|
153
|
+
project_id="PROJECT-1",
|
|
154
|
+
limit=20
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Get epic's child issues
|
|
158
|
+
await hierarchy(
|
|
159
|
+
entity_type="epic",
|
|
160
|
+
action="get_children",
|
|
161
|
+
entity_id="EPIC-123"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Create issue under epic
|
|
165
|
+
await hierarchy(
|
|
166
|
+
entity_type="issue",
|
|
167
|
+
action="create",
|
|
168
|
+
title="User authentication",
|
|
169
|
+
description="Implement OAuth2 flow",
|
|
170
|
+
epic_id="EPIC-123",
|
|
171
|
+
priority="high"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Get issue's parent
|
|
175
|
+
await hierarchy(
|
|
176
|
+
entity_type="issue",
|
|
177
|
+
action="get_parent",
|
|
178
|
+
entity_id="ISSUE-456"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Get issue's child tasks
|
|
182
|
+
await hierarchy(
|
|
183
|
+
entity_type="issue",
|
|
184
|
+
action="get_children",
|
|
185
|
+
entity_id="ISSUE-456",
|
|
186
|
+
state="open"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Create task under issue
|
|
190
|
+
await hierarchy(
|
|
191
|
+
entity_type="task",
|
|
192
|
+
action="create",
|
|
193
|
+
title="Write tests",
|
|
194
|
+
issue_id="ISSUE-456",
|
|
195
|
+
priority="medium"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Get full hierarchy tree
|
|
199
|
+
await hierarchy(
|
|
200
|
+
entity_type="epic",
|
|
201
|
+
action="get_tree",
|
|
202
|
+
entity_id="EPIC-123",
|
|
203
|
+
max_depth=3
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Update epic
|
|
207
|
+
await hierarchy(
|
|
208
|
+
entity_type="epic",
|
|
209
|
+
action="update",
|
|
210
|
+
entity_id="EPIC-123",
|
|
211
|
+
title="Updated Title",
|
|
212
|
+
state="in_progress"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Delete epic
|
|
216
|
+
await hierarchy(
|
|
217
|
+
entity_type="epic",
|
|
218
|
+
action="delete",
|
|
219
|
+
entity_id="EPIC-123"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
Migration from old tools:
|
|
223
|
+
epic_create(...) → hierarchy(entity_type="epic", action="create", ...)
|
|
224
|
+
epic_get(epic_id) → hierarchy(entity_type="epic", action="get", entity_id=epic_id)
|
|
225
|
+
epic_list(...) → hierarchy(entity_type="epic", action="list", ...)
|
|
226
|
+
epic_update(...) → hierarchy(entity_type="epic", action="update", ...)
|
|
227
|
+
epic_delete(epic_id) → hierarchy(entity_type="epic", action="delete", entity_id=epic_id)
|
|
228
|
+
epic_issues(epic_id) → hierarchy(entity_type="epic", action="get_children", entity_id=epic_id)
|
|
229
|
+
issue_create(...) → hierarchy(entity_type="issue", action="create", ...)
|
|
230
|
+
issue_get_parent(issue_id) → hierarchy(entity_type="issue", action="get_parent", entity_id=issue_id)
|
|
231
|
+
issue_tasks(issue_id) → hierarchy(entity_type="issue", action="get_children", entity_id=issue_id)
|
|
232
|
+
task_create(...) → hierarchy(entity_type="task", action="create", ...)
|
|
233
|
+
hierarchy_tree(epic_id) → hierarchy(entity_type="epic", action="get_tree", entity_id=epic_id)
|
|
234
|
+
|
|
235
|
+
See: docs/mcp-api-reference.md for detailed response formats
|
|
236
|
+
"""
|
|
237
|
+
# Normalize entity_type and action to lowercase for case-insensitive matching
|
|
238
|
+
entity_type_lower = entity_type.lower()
|
|
239
|
+
action_lower = action.lower()
|
|
240
|
+
|
|
241
|
+
# Route to appropriate handler based on entity_type + action
|
|
242
|
+
try:
|
|
243
|
+
adapter = get_adapter()
|
|
244
|
+
|
|
245
|
+
if entity_type_lower == "epic":
|
|
246
|
+
if action_lower == "create":
|
|
247
|
+
# Inline implementation of epic_create
|
|
248
|
+
try:
|
|
249
|
+
# Parse target date if provided
|
|
250
|
+
target_datetime = None
|
|
251
|
+
if target_date:
|
|
252
|
+
try:
|
|
253
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
254
|
+
except ValueError:
|
|
255
|
+
return {
|
|
256
|
+
"status": "error",
|
|
257
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Create epic object
|
|
261
|
+
epic = Epic(
|
|
262
|
+
title=title or "",
|
|
263
|
+
description=description or "",
|
|
264
|
+
due_date=target_datetime,
|
|
265
|
+
assignee=lead_id,
|
|
266
|
+
child_issues=child_issues or [],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Create via adapter
|
|
270
|
+
created = await adapter.create(epic)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"status": "completed",
|
|
274
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
275
|
+
"epic": created.model_dump(),
|
|
276
|
+
}
|
|
277
|
+
except Exception as e:
|
|
278
|
+
return {
|
|
279
|
+
"status": "error",
|
|
280
|
+
"error": f"Failed to create epic: {str(e)}",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
elif action_lower == "get":
|
|
284
|
+
# Inline implementation of epic_get
|
|
285
|
+
if not entity_id and not epic_id:
|
|
286
|
+
return {
|
|
287
|
+
"status": "error",
|
|
288
|
+
"error": "entity_id or epic_id required for get operation",
|
|
289
|
+
}
|
|
290
|
+
try:
|
|
291
|
+
final_epic_id = entity_id or epic_id or ""
|
|
292
|
+
|
|
293
|
+
# Use adapter's get_epic method if available (optimized for some adapters)
|
|
294
|
+
if hasattr(adapter, "get_epic"):
|
|
295
|
+
epic = await adapter.get_epic(final_epic_id)
|
|
296
|
+
else:
|
|
297
|
+
# Fallback to generic read method
|
|
298
|
+
epic = await adapter.read(final_epic_id)
|
|
299
|
+
|
|
300
|
+
if epic is None:
|
|
301
|
+
return {
|
|
302
|
+
"status": "error",
|
|
303
|
+
"error": f"Epic {final_epic_id} not found",
|
|
304
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"status": "completed",
|
|
309
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
310
|
+
"epic": epic.model_dump(),
|
|
311
|
+
}
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return {
|
|
314
|
+
"status": "error",
|
|
315
|
+
"error": f"Failed to get epic: {str(e)}",
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
elif action_lower == "list":
|
|
319
|
+
# Inline implementation of epic_list
|
|
320
|
+
try:
|
|
321
|
+
# Validate project context (Required for list operations)
|
|
322
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
323
|
+
config = resolver.load_project_config()
|
|
324
|
+
final_project = project_id or (
|
|
325
|
+
config.default_project if config else None
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if not final_project:
|
|
329
|
+
return {
|
|
330
|
+
"status": "error",
|
|
331
|
+
"error": "project_id required. Provide project_id parameter or configure default_project.",
|
|
332
|
+
"help": "Use config_set_default_project(project_id='YOUR-PROJECT') to set default project",
|
|
333
|
+
"check_config": "Use config_get() to view current configuration",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Check if adapter has optimized list_epics method
|
|
337
|
+
if hasattr(adapter, "list_epics"):
|
|
338
|
+
# Build kwargs for adapter-specific parameters with required project scoping
|
|
339
|
+
kwargs: dict[str, Any] = {
|
|
340
|
+
"limit": limit,
|
|
341
|
+
"offset": offset,
|
|
342
|
+
"project": final_project,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Add state filter if supported
|
|
346
|
+
if state is not None:
|
|
347
|
+
kwargs["state"] = state
|
|
348
|
+
|
|
349
|
+
# Add include_completed for Linear adapter
|
|
350
|
+
adapter_type = adapter.adapter_type.lower()
|
|
351
|
+
if adapter_type == "linear" and include_completed:
|
|
352
|
+
kwargs["include_completed"] = include_completed
|
|
353
|
+
|
|
354
|
+
epics = await adapter.list_epics(**kwargs)
|
|
355
|
+
else:
|
|
356
|
+
# Fallback to generic list method with epic filter and project scoping
|
|
357
|
+
filters = {
|
|
358
|
+
"ticket_type": TicketType.EPIC,
|
|
359
|
+
"project": final_project,
|
|
360
|
+
}
|
|
361
|
+
if state is not None:
|
|
362
|
+
filters["state"] = state
|
|
363
|
+
epics = await adapter.list(
|
|
364
|
+
limit=limit, offset=offset, filters=filters
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"status": "completed",
|
|
369
|
+
**_build_adapter_metadata(adapter),
|
|
370
|
+
"epics": [epic.model_dump() for epic in epics],
|
|
371
|
+
"count": len(epics),
|
|
372
|
+
"limit": limit,
|
|
373
|
+
"offset": offset,
|
|
374
|
+
"filters_applied": {
|
|
375
|
+
"state": state,
|
|
376
|
+
"include_completed": include_completed,
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
except Exception as e:
|
|
380
|
+
return {
|
|
381
|
+
"status": "error",
|
|
382
|
+
"error": f"Failed to list epics: {str(e)}",
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
elif action_lower == "update":
|
|
386
|
+
# Inline implementation of epic_update
|
|
387
|
+
if not entity_id and not epic_id:
|
|
388
|
+
return {
|
|
389
|
+
"status": "error",
|
|
390
|
+
"error": "entity_id or epic_id required for update operation",
|
|
391
|
+
}
|
|
392
|
+
try:
|
|
393
|
+
final_epic_id = entity_id or epic_id or ""
|
|
394
|
+
|
|
395
|
+
# Check if adapter supports epic updates
|
|
396
|
+
if not hasattr(adapter, "update_epic"):
|
|
397
|
+
adapter_name = adapter.adapter_display_name
|
|
398
|
+
return {
|
|
399
|
+
"status": "error",
|
|
400
|
+
"error": f"Epic updates not supported by {adapter_name} adapter",
|
|
401
|
+
"epic_id": final_epic_id,
|
|
402
|
+
"note": "This adapter should implement update_epic() method",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Build updates dictionary
|
|
406
|
+
updates = {}
|
|
407
|
+
if title is not None:
|
|
408
|
+
updates["title"] = title
|
|
409
|
+
if description is not None:
|
|
410
|
+
updates["description"] = description
|
|
411
|
+
if state is not None:
|
|
412
|
+
updates["state"] = state
|
|
413
|
+
if target_date is not None:
|
|
414
|
+
# Parse target date if provided
|
|
415
|
+
try:
|
|
416
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
417
|
+
updates["target_date"] = target_datetime
|
|
418
|
+
except ValueError:
|
|
419
|
+
return {
|
|
420
|
+
"status": "error",
|
|
421
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if not updates:
|
|
425
|
+
return {
|
|
426
|
+
"status": "error",
|
|
427
|
+
"error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Update via adapter
|
|
431
|
+
updated = await adapter.update_epic(final_epic_id, updates)
|
|
432
|
+
|
|
433
|
+
if updated is None:
|
|
434
|
+
return {
|
|
435
|
+
"status": "error",
|
|
436
|
+
"error": f"Epic {final_epic_id} not found or update failed",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
"status": "completed",
|
|
441
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
442
|
+
"epic": updated.model_dump(),
|
|
443
|
+
}
|
|
444
|
+
except AttributeError as e:
|
|
445
|
+
return {
|
|
446
|
+
"status": "error",
|
|
447
|
+
"error": f"Epic update method not available: {str(e)}",
|
|
448
|
+
"epic_id": final_epic_id,
|
|
449
|
+
}
|
|
450
|
+
except Exception as e:
|
|
451
|
+
return {
|
|
452
|
+
"status": "error",
|
|
453
|
+
"error": f"Failed to update epic: {str(e)}",
|
|
454
|
+
"epic_id": final_epic_id,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
elif action_lower == "delete":
|
|
458
|
+
# Inline implementation of epic_delete
|
|
459
|
+
if not entity_id and not epic_id:
|
|
460
|
+
return {
|
|
461
|
+
"status": "error",
|
|
462
|
+
"error": "entity_id or epic_id required for delete operation",
|
|
463
|
+
}
|
|
464
|
+
try:
|
|
465
|
+
final_epic_id = entity_id or epic_id or ""
|
|
466
|
+
|
|
467
|
+
# Check if adapter supports epic deletion
|
|
468
|
+
if not hasattr(adapter, "delete_epic"):
|
|
469
|
+
adapter_name = adapter.adapter_display_name
|
|
470
|
+
return {
|
|
471
|
+
"status": "error",
|
|
472
|
+
"error": f"Epic deletion not supported by {adapter_name} adapter",
|
|
473
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
474
|
+
"supported_adapters": ["GitHub", "Asana"],
|
|
475
|
+
"note": f"{adapter_name} does not provide API support for deleting epics/projects",
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Call adapter's delete_epic method
|
|
479
|
+
success = await adapter.delete_epic(final_epic_id)
|
|
480
|
+
|
|
481
|
+
if not success:
|
|
482
|
+
return {
|
|
483
|
+
"status": "error",
|
|
484
|
+
"error": f"Failed to delete epic {final_epic_id}",
|
|
485
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
"status": "completed",
|
|
490
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
491
|
+
"message": f"Epic {final_epic_id} deleted successfully",
|
|
492
|
+
"deleted": True,
|
|
493
|
+
}
|
|
494
|
+
except AttributeError:
|
|
495
|
+
adapter_name = adapter.adapter_display_name
|
|
496
|
+
return {
|
|
497
|
+
"status": "error",
|
|
498
|
+
"error": f"Epic deletion not supported by {adapter_name} adapter",
|
|
499
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
500
|
+
"supported_adapters": ["GitHub", "Asana"],
|
|
501
|
+
}
|
|
502
|
+
except Exception as e:
|
|
503
|
+
return {
|
|
504
|
+
"status": "error",
|
|
505
|
+
"error": f"Failed to delete epic: {str(e)}",
|
|
506
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
elif action_lower == "get_children":
|
|
510
|
+
# Inline implementation of epic_issues
|
|
511
|
+
if not entity_id and not epic_id:
|
|
512
|
+
return {
|
|
513
|
+
"status": "error",
|
|
514
|
+
"error": "entity_id or epic_id required for get_children operation",
|
|
515
|
+
}
|
|
516
|
+
try:
|
|
517
|
+
final_epic_id = entity_id or epic_id or ""
|
|
518
|
+
|
|
519
|
+
# Read the epic to get child issue IDs
|
|
520
|
+
epic = await adapter.read(final_epic_id)
|
|
521
|
+
if epic is None:
|
|
522
|
+
return {
|
|
523
|
+
"status": "error",
|
|
524
|
+
"error": f"Epic {final_epic_id} not found",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# If epic has no child_issues attribute, use empty list
|
|
528
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
529
|
+
|
|
530
|
+
# Fetch each child issue
|
|
531
|
+
issues = []
|
|
532
|
+
for issue_id in child_issue_ids:
|
|
533
|
+
issue = await adapter.read(issue_id)
|
|
534
|
+
if issue:
|
|
535
|
+
issues.append(issue.model_dump())
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"status": "completed",
|
|
539
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
540
|
+
"issues": issues,
|
|
541
|
+
"count": len(issues),
|
|
542
|
+
}
|
|
543
|
+
except Exception as e:
|
|
544
|
+
return {
|
|
545
|
+
"status": "error",
|
|
546
|
+
"error": f"Failed to get epic issues: {str(e)}",
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
elif action_lower == "get_tree":
|
|
550
|
+
# Inline implementation of hierarchy_tree
|
|
551
|
+
if not entity_id and not epic_id:
|
|
552
|
+
return {
|
|
553
|
+
"status": "error",
|
|
554
|
+
"error": "entity_id or epic_id required for get_tree operation",
|
|
555
|
+
}
|
|
556
|
+
try:
|
|
557
|
+
final_epic_id = entity_id or epic_id or ""
|
|
558
|
+
|
|
559
|
+
# Read the epic
|
|
560
|
+
epic = await adapter.read(final_epic_id)
|
|
561
|
+
if epic is None:
|
|
562
|
+
return {
|
|
563
|
+
"status": "error",
|
|
564
|
+
"error": f"Epic {final_epic_id} not found",
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Build tree structure
|
|
568
|
+
tree = {
|
|
569
|
+
"epic": epic.model_dump(),
|
|
570
|
+
"issues": [],
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if max_depth < 2:
|
|
574
|
+
return {
|
|
575
|
+
"status": "completed",
|
|
576
|
+
"tree": tree,
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
# Get child issues
|
|
580
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
581
|
+
for issue_id in child_issue_ids:
|
|
582
|
+
issue = await adapter.read(issue_id)
|
|
583
|
+
if issue:
|
|
584
|
+
issue_data = {
|
|
585
|
+
"issue": issue.model_dump(),
|
|
586
|
+
"tasks": [],
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if max_depth >= 3:
|
|
590
|
+
# Get child tasks
|
|
591
|
+
child_task_ids = getattr(issue, "children", [])
|
|
592
|
+
for task_id in child_task_ids:
|
|
593
|
+
task = await adapter.read(task_id)
|
|
594
|
+
if task:
|
|
595
|
+
issue_data["tasks"].append(task.model_dump())
|
|
596
|
+
|
|
597
|
+
tree["issues"].append(issue_data)
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
"status": "completed",
|
|
601
|
+
**_build_adapter_metadata(adapter, final_epic_id),
|
|
602
|
+
"tree": tree,
|
|
603
|
+
}
|
|
604
|
+
except Exception as e:
|
|
605
|
+
return {
|
|
606
|
+
"status": "error",
|
|
607
|
+
"error": f"Failed to build hierarchy tree: {str(e)}",
|
|
608
|
+
}
|
|
609
|
+
else:
|
|
610
|
+
valid_actions = [
|
|
611
|
+
"create",
|
|
612
|
+
"get",
|
|
613
|
+
"list",
|
|
614
|
+
"update",
|
|
615
|
+
"delete",
|
|
616
|
+
"get_children",
|
|
617
|
+
"get_tree",
|
|
618
|
+
]
|
|
619
|
+
return {
|
|
620
|
+
"status": "error",
|
|
621
|
+
"error": f"Invalid action '{action}' for entity_type 'epic'",
|
|
622
|
+
"valid_actions": valid_actions,
|
|
623
|
+
"hint": f"Use hierarchy(entity_type='epic', action=<one of {valid_actions}>, ...)",
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
elif entity_type_lower == "issue":
|
|
627
|
+
if action_lower == "create":
|
|
628
|
+
# Inline implementation of issue_create
|
|
629
|
+
try:
|
|
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
|
+
# Load configuration
|
|
640
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
641
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
642
|
+
|
|
643
|
+
# Use default_user if no assignee specified
|
|
644
|
+
final_assignee = assignee
|
|
645
|
+
if final_assignee is None and config.default_user:
|
|
646
|
+
final_assignee = config.default_user
|
|
647
|
+
|
|
648
|
+
# Determine final_epic_id based on priority order:
|
|
649
|
+
# Priority 1: Explicit epic_id argument (including explicit None for opt-out)
|
|
650
|
+
# Priority 2: Config default (default_epic or default_project)
|
|
651
|
+
final_epic_id: str | None = None
|
|
652
|
+
|
|
653
|
+
# Handle epic_id with sentinel for explicit None
|
|
654
|
+
effective_epic_id = _UNSET if epic_id is None else epic_id
|
|
655
|
+
|
|
656
|
+
if effective_epic_id is not _UNSET:
|
|
657
|
+
# Priority 1: Explicit value provided (including None for opt-out)
|
|
658
|
+
final_epic_id = effective_epic_id
|
|
659
|
+
elif config.default_project or config.default_epic:
|
|
660
|
+
# Priority 2: Use configured default
|
|
661
|
+
final_epic_id = config.default_project or config.default_epic
|
|
662
|
+
|
|
663
|
+
# Auto-detect labels if enabled
|
|
664
|
+
final_tags = tags
|
|
665
|
+
if auto_detect_labels:
|
|
666
|
+
final_tags = await detect_and_apply_labels(
|
|
667
|
+
adapter, title or "", description or "", tags
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Create issue (Task with ISSUE type)
|
|
671
|
+
issue = Task(
|
|
672
|
+
title=title or "",
|
|
673
|
+
description=description or "",
|
|
674
|
+
ticket_type=TicketType.ISSUE,
|
|
675
|
+
parent_epic=final_epic_id,
|
|
676
|
+
assignee=final_assignee,
|
|
677
|
+
priority=priority_enum,
|
|
678
|
+
tags=final_tags or [],
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Create via adapter
|
|
682
|
+
created = await adapter.create(issue)
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
"status": "completed",
|
|
686
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
687
|
+
"issue": created.model_dump(),
|
|
688
|
+
"labels_applied": created.tags or [],
|
|
689
|
+
"auto_detected": auto_detect_labels,
|
|
690
|
+
}
|
|
691
|
+
except Exception as e:
|
|
692
|
+
return {
|
|
693
|
+
"status": "error",
|
|
694
|
+
"error": f"Failed to create issue: {str(e)}",
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
elif action_lower == "get_parent":
|
|
698
|
+
# Inline implementation of issue_get_parent
|
|
699
|
+
if not entity_id and not issue_id:
|
|
700
|
+
return {
|
|
701
|
+
"status": "error",
|
|
702
|
+
"error": "entity_id or issue_id required for get_parent operation",
|
|
703
|
+
}
|
|
704
|
+
try:
|
|
705
|
+
final_issue_id = entity_id or issue_id or ""
|
|
706
|
+
|
|
707
|
+
# Read the issue to check if it has a parent
|
|
708
|
+
issue = await adapter.read(final_issue_id)
|
|
709
|
+
if issue is None:
|
|
710
|
+
return {
|
|
711
|
+
"status": "error",
|
|
712
|
+
"error": f"Issue {final_issue_id} not found",
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
# Check for parent_issue attribute (sub-issues have this set)
|
|
716
|
+
parent_issue_id = getattr(issue, "parent_issue", None)
|
|
717
|
+
|
|
718
|
+
if not parent_issue_id:
|
|
719
|
+
# No parent - this is a top-level issue
|
|
720
|
+
return {
|
|
721
|
+
"status": "completed",
|
|
722
|
+
**_build_adapter_metadata(adapter, final_issue_id),
|
|
723
|
+
"parent": None,
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Fetch parent issue details
|
|
727
|
+
parent_issue = await adapter.read(parent_issue_id)
|
|
728
|
+
if parent_issue is None:
|
|
729
|
+
return {
|
|
730
|
+
"status": "error",
|
|
731
|
+
"error": f"Parent issue {parent_issue_id} not found",
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
"status": "completed",
|
|
736
|
+
**_build_adapter_metadata(adapter, final_issue_id),
|
|
737
|
+
"parent": parent_issue.model_dump(),
|
|
738
|
+
}
|
|
739
|
+
except Exception as e:
|
|
740
|
+
return {
|
|
741
|
+
"status": "error",
|
|
742
|
+
"error": f"Failed to get parent issue: {str(e)}",
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
elif action_lower == "get_children":
|
|
746
|
+
# Inline implementation of issue_tasks
|
|
747
|
+
if not entity_id and not issue_id:
|
|
748
|
+
return {
|
|
749
|
+
"status": "error",
|
|
750
|
+
"error": "entity_id or issue_id required for get_children operation",
|
|
751
|
+
}
|
|
752
|
+
try:
|
|
753
|
+
final_issue_id = entity_id or issue_id or ""
|
|
754
|
+
|
|
755
|
+
# Validate filter parameters
|
|
756
|
+
filters_applied = {}
|
|
757
|
+
|
|
758
|
+
# Validate state if provided
|
|
759
|
+
if state is not None:
|
|
760
|
+
try:
|
|
761
|
+
from ....core.models import TicketState
|
|
762
|
+
|
|
763
|
+
state_enum = TicketState(state.lower())
|
|
764
|
+
filters_applied["state"] = state_enum.value
|
|
765
|
+
except ValueError:
|
|
766
|
+
return {
|
|
767
|
+
"status": "error",
|
|
768
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# Validate priority if provided
|
|
772
|
+
if priority is not None:
|
|
773
|
+
try:
|
|
774
|
+
priority_enum = Priority(priority.lower())
|
|
775
|
+
filters_applied["priority"] = priority_enum.value
|
|
776
|
+
except ValueError:
|
|
777
|
+
return {
|
|
778
|
+
"status": "error",
|
|
779
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if assignee is not None:
|
|
783
|
+
filters_applied["assignee"] = assignee
|
|
784
|
+
|
|
785
|
+
# Read the issue to get child task IDs
|
|
786
|
+
issue = await adapter.read(final_issue_id)
|
|
787
|
+
if issue is None:
|
|
788
|
+
return {
|
|
789
|
+
"status": "error",
|
|
790
|
+
"error": f"Issue {final_issue_id} not found",
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# Get child task IDs
|
|
794
|
+
child_task_ids = getattr(issue, "children", [])
|
|
795
|
+
|
|
796
|
+
# Fetch each child task
|
|
797
|
+
tasks = []
|
|
798
|
+
for task_id in child_task_ids:
|
|
799
|
+
task = await adapter.read(task_id)
|
|
800
|
+
if task:
|
|
801
|
+
# Apply filters
|
|
802
|
+
should_include = True
|
|
803
|
+
|
|
804
|
+
# Filter by state
|
|
805
|
+
if state is not None:
|
|
806
|
+
task_state = getattr(task, "state", None)
|
|
807
|
+
# Handle case where state might be stored as string
|
|
808
|
+
if isinstance(task_state, str):
|
|
809
|
+
should_include = should_include and (
|
|
810
|
+
task_state.lower() == state.lower()
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
should_include = should_include and (
|
|
814
|
+
task_state == state_enum
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# Filter by priority
|
|
818
|
+
if priority is not None:
|
|
819
|
+
task_priority = getattr(task, "priority", None)
|
|
820
|
+
# Handle case where priority might be stored as string
|
|
821
|
+
if isinstance(task_priority, str):
|
|
822
|
+
should_include = should_include and (
|
|
823
|
+
task_priority.lower() == priority.lower()
|
|
824
|
+
)
|
|
825
|
+
else:
|
|
826
|
+
should_include = should_include and (
|
|
827
|
+
task_priority == priority_enum
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Filter by assignee
|
|
831
|
+
if assignee is not None:
|
|
832
|
+
task_assignee = getattr(task, "assignee", None)
|
|
833
|
+
# Case-insensitive comparison for emails/usernames
|
|
834
|
+
should_include = should_include and (
|
|
835
|
+
task_assignee is not None
|
|
836
|
+
and assignee.lower() in str(task_assignee).lower()
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
if should_include:
|
|
840
|
+
tasks.append(task.model_dump())
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
"status": "completed",
|
|
844
|
+
**_build_adapter_metadata(adapter, final_issue_id),
|
|
845
|
+
"tasks": tasks,
|
|
846
|
+
"count": len(tasks),
|
|
847
|
+
"filters_applied": filters_applied,
|
|
848
|
+
}
|
|
849
|
+
except Exception as e:
|
|
850
|
+
return {
|
|
851
|
+
"status": "error",
|
|
852
|
+
"error": f"Failed to get issue tasks: {str(e)}",
|
|
853
|
+
}
|
|
854
|
+
else:
|
|
855
|
+
valid_actions = ["create", "get_parent", "get_children"]
|
|
856
|
+
return {
|
|
857
|
+
"status": "error",
|
|
858
|
+
"error": f"Invalid action '{action}' for entity_type 'issue'",
|
|
859
|
+
"valid_actions": valid_actions,
|
|
860
|
+
"hint": f"Use hierarchy(entity_type='issue', action=<one of {valid_actions}>, ...)",
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
elif entity_type_lower == "task":
|
|
864
|
+
if action_lower == "create":
|
|
865
|
+
# Inline implementation of task_create
|
|
866
|
+
try:
|
|
867
|
+
# Validate and convert priority
|
|
868
|
+
try:
|
|
869
|
+
priority_enum = Priority(priority.lower())
|
|
870
|
+
except ValueError:
|
|
871
|
+
return {
|
|
872
|
+
"status": "error",
|
|
873
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
# Use default_user if no assignee specified
|
|
877
|
+
final_assignee = assignee
|
|
878
|
+
if final_assignee is None:
|
|
879
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
880
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
881
|
+
if config.default_user:
|
|
882
|
+
final_assignee = config.default_user
|
|
883
|
+
|
|
884
|
+
# Auto-detect labels if enabled
|
|
885
|
+
final_tags = tags
|
|
886
|
+
if auto_detect_labels:
|
|
887
|
+
final_tags = await detect_and_apply_labels(
|
|
888
|
+
adapter, title or "", description or "", tags
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# Create task (Task with TASK type)
|
|
892
|
+
task = Task(
|
|
893
|
+
title=title or "",
|
|
894
|
+
description=description or "",
|
|
895
|
+
ticket_type=TicketType.TASK,
|
|
896
|
+
parent_issue=issue_id,
|
|
897
|
+
assignee=final_assignee,
|
|
898
|
+
priority=priority_enum,
|
|
899
|
+
tags=final_tags or [],
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# Create via adapter
|
|
903
|
+
created = await adapter.create(task)
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
"status": "completed",
|
|
907
|
+
**_build_adapter_metadata(adapter, created.id),
|
|
908
|
+
"task": created.model_dump(),
|
|
909
|
+
"labels_applied": created.tags or [],
|
|
910
|
+
"auto_detected": auto_detect_labels,
|
|
911
|
+
}
|
|
912
|
+
except Exception as e:
|
|
913
|
+
return {
|
|
914
|
+
"status": "error",
|
|
915
|
+
"error": f"Failed to create task: {str(e)}",
|
|
916
|
+
}
|
|
917
|
+
else:
|
|
918
|
+
valid_actions = ["create"]
|
|
919
|
+
return {
|
|
920
|
+
"status": "error",
|
|
921
|
+
"error": f"Invalid action '{action}' for entity_type 'task'",
|
|
922
|
+
"valid_actions": valid_actions,
|
|
923
|
+
"hint": "Use hierarchy(entity_type='task', action='create', ...)",
|
|
924
|
+
"note": "Tasks support only create operation. Use ticket_read/ticket_update for other operations.",
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
else:
|
|
928
|
+
valid_types = ["epic", "issue", "task"]
|
|
929
|
+
return {
|
|
930
|
+
"status": "error",
|
|
931
|
+
"error": f"Invalid entity_type: {entity_type}",
|
|
932
|
+
"valid_entity_types": valid_types,
|
|
933
|
+
"hint": f"Use hierarchy(entity_type=<one of {valid_types}>, action=..., ...)",
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
except Exception as e:
|
|
937
|
+
return {
|
|
938
|
+
"status": "error",
|
|
939
|
+
"error": f"Hierarchy operation failed: {str(e)}",
|
|
940
|
+
"entity_type": entity_type,
|
|
941
|
+
"action": action,
|
|
942
|
+
}
|