mcp-ticketer 0.2.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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.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
+ }