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.
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 +91 -54
  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 -1544
  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 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,29 @@
1
1
  """Base adapter abstract class for ticket systems."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import builtins
4
6
  from abc import ABC, abstractmethod
5
- from typing import Any, Generic, Optional, TypeVar
6
-
7
- from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
9
+
10
+ from .models import (
11
+ Comment,
12
+ Epic,
13
+ Milestone,
14
+ Project,
15
+ ProjectScope,
16
+ ProjectState,
17
+ ProjectStatistics,
18
+ SearchQuery,
19
+ Task,
20
+ TicketState,
21
+ TicketType,
22
+ )
23
+ from .state_matcher import get_state_matcher
24
+
25
+ if TYPE_CHECKING:
26
+ from .models import Attachment
8
27
 
9
28
  # Generic type for tickets
10
29
  T = TypeVar("T", Epic, Task)
@@ -17,17 +36,52 @@ class BaseAdapter(ABC, Generic[T]):
17
36
  """Initialize adapter with configuration.
18
37
 
19
38
  Args:
39
+ ----
20
40
  config: Adapter-specific configuration dictionary
21
41
 
22
42
  """
23
43
  self.config = config
24
44
  self._state_mapping = self._get_state_mapping()
25
45
 
46
+ @property
47
+ def adapter_type(self) -> str:
48
+ """Return lowercase adapter type identifier.
49
+
50
+ This identifier is used in MCP responses to show which adapter
51
+ handled the operation (e.g., "linear", "github", "jira", "asana").
52
+
53
+ Returns:
54
+ -------
55
+ Lowercase adapter type (e.g., "linear", "github")
56
+
57
+ """
58
+ # Extract adapter type from class name
59
+ # LinearAdapter -> linear, GitHubAdapter -> github
60
+ class_name = self.__class__.__name__
61
+ if class_name.endswith("Adapter"):
62
+ adapter_name = class_name[: -len("Adapter")]
63
+ else:
64
+ adapter_name = class_name
65
+
66
+ return adapter_name.lower()
67
+
68
+ @property
69
+ def adapter_display_name(self) -> str:
70
+ """Return human-readable adapter name.
71
+
72
+ Returns:
73
+ -------
74
+ Title-cased adapter name (e.g., "Linear", "Github", "Jira")
75
+
76
+ """
77
+ return self.adapter_type.title()
78
+
26
79
  @abstractmethod
27
80
  def _get_state_mapping(self) -> dict[TicketState, str]:
28
81
  """Get mapping from universal states to system-specific states.
29
82
 
30
83
  Returns:
84
+ -------
31
85
  Dictionary mapping TicketState to system-specific state strings
32
86
 
33
87
  """
@@ -38,6 +92,7 @@ class BaseAdapter(ABC, Generic[T]):
38
92
  """Validate that required credentials are present.
39
93
 
40
94
  Returns:
95
+ -------
41
96
  (is_valid, error_message) - Tuple of validation result and error message
42
97
 
43
98
  """
@@ -48,36 +103,42 @@ class BaseAdapter(ABC, Generic[T]):
48
103
  """Create a new ticket.
49
104
 
50
105
  Args:
106
+ ----
51
107
  ticket: Ticket to create (Epic or Task)
52
108
 
53
109
  Returns:
110
+ -------
54
111
  Created ticket with ID populated
55
112
 
56
113
  """
57
114
  pass
58
115
 
59
116
  @abstractmethod
60
- async def read(self, ticket_id: str) -> Optional[T]:
117
+ async def read(self, ticket_id: str) -> T | None:
61
118
  """Read a ticket by ID.
62
119
 
63
120
  Args:
121
+ ----
64
122
  ticket_id: Unique ticket identifier
65
123
 
66
124
  Returns:
125
+ -------
67
126
  Ticket if found, None otherwise
68
127
 
69
128
  """
70
129
  pass
71
130
 
72
131
  @abstractmethod
73
- async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[T]:
132
+ async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
74
133
  """Update a ticket.
75
134
 
76
135
  Args:
136
+ ----
77
137
  ticket_id: Ticket identifier
78
138
  updates: Fields to update
79
139
 
80
140
  Returns:
141
+ -------
81
142
  Updated ticket if successful, None otherwise
82
143
 
83
144
  """
@@ -88,9 +149,11 @@ class BaseAdapter(ABC, Generic[T]):
88
149
  """Delete a ticket.
89
150
 
90
151
  Args:
152
+ ----
91
153
  ticket_id: Ticket identifier
92
154
 
93
155
  Returns:
156
+ -------
94
157
  True if deleted, False otherwise
95
158
 
96
159
  """
@@ -98,16 +161,18 @@ class BaseAdapter(ABC, Generic[T]):
98
161
 
99
162
  @abstractmethod
100
163
  async def list(
101
- self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
164
+ self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
102
165
  ) -> list[T]:
103
166
  """List tickets with pagination and filters.
104
167
 
105
168
  Args:
169
+ ----
106
170
  limit: Maximum number of tickets
107
171
  offset: Skip this many tickets
108
172
  filters: Optional filter criteria
109
173
 
110
174
  Returns:
175
+ -------
111
176
  List of tickets matching criteria
112
177
 
113
178
  """
@@ -118,9 +183,11 @@ class BaseAdapter(ABC, Generic[T]):
118
183
  """Search tickets using advanced query.
119
184
 
120
185
  Args:
186
+ ----
121
187
  query: Search parameters
122
188
 
123
189
  Returns:
190
+ -------
124
191
  List of tickets matching search criteria
125
192
 
126
193
  """
@@ -129,14 +196,16 @@ class BaseAdapter(ABC, Generic[T]):
129
196
  @abstractmethod
130
197
  async def transition_state(
131
198
  self, ticket_id: str, target_state: TicketState
132
- ) -> Optional[T]:
199
+ ) -> T | None:
133
200
  """Transition ticket to a new state.
134
201
 
135
202
  Args:
203
+ ----
136
204
  ticket_id: Ticket identifier
137
205
  target_state: Target state
138
206
 
139
207
  Returns:
208
+ -------
140
209
  Updated ticket if transition successful, None otherwise
141
210
 
142
211
  """
@@ -147,9 +216,11 @@ class BaseAdapter(ABC, Generic[T]):
147
216
  """Add a comment to a ticket.
148
217
 
149
218
  Args:
219
+ ----
150
220
  comment: Comment to add
151
221
 
152
222
  Returns:
223
+ -------
153
224
  Created comment with ID populated
154
225
 
155
226
  """
@@ -162,11 +233,13 @@ class BaseAdapter(ABC, Generic[T]):
162
233
  """Get comments for a ticket.
163
234
 
164
235
  Args:
236
+ ----
165
237
  ticket_id: Ticket identifier
166
238
  limit: Maximum number of comments
167
239
  offset: Skip this many comments
168
240
 
169
241
  Returns:
242
+ -------
170
243
  List of comments for the ticket
171
244
 
172
245
  """
@@ -176,9 +249,11 @@ class BaseAdapter(ABC, Generic[T]):
176
249
  """Map universal state to system-specific state.
177
250
 
178
251
  Args:
252
+ ----
179
253
  state: Universal ticket state
180
254
 
181
255
  Returns:
256
+ -------
182
257
  System-specific state string
183
258
 
184
259
  """
@@ -188,31 +263,88 @@ class BaseAdapter(ABC, Generic[T]):
188
263
  """Map system-specific state to universal state.
189
264
 
190
265
  Args:
266
+ ----
191
267
  system_state: System-specific state string
192
268
 
193
269
  Returns:
270
+ -------
194
271
  Universal ticket state
195
272
 
196
273
  """
197
274
  reverse_mapping = {v: k for k, v in self._state_mapping.items()}
198
275
  return reverse_mapping.get(system_state, TicketState.OPEN)
199
276
 
277
+ def get_available_states(self) -> list[str]:
278
+ """Get list of adapter-specific available states.
279
+
280
+ Returns adapter-specific state names that can be used for
281
+ semantic state matching. Override in subclasses to provide
282
+ platform-specific state names.
283
+
284
+ Returns:
285
+ -------
286
+ List of adapter-specific state names
287
+
288
+ Example:
289
+ -------
290
+ >>> # Linear adapter override
291
+ >>> def get_available_states(self):
292
+ ... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
293
+
294
+ """
295
+ # Default: return universal state values
296
+ return [state.value for state in TicketState]
297
+
298
+ def resolve_state(self, user_input: str) -> TicketState:
299
+ """Resolve user input to universal state using semantic matcher.
300
+
301
+ Uses the semantic state matcher to interpret natural language
302
+ inputs and resolve them to universal TicketState values.
303
+
304
+ Args:
305
+ ----
306
+ user_input: Natural language state input (e.g., "working on it")
307
+
308
+ Returns:
309
+ -------
310
+ Resolved universal TicketState
311
+
312
+ Example:
313
+ -------
314
+ >>> adapter = get_adapter()
315
+ >>> state = adapter.resolve_state("working on it")
316
+ >>> print(state)
317
+ TicketState.IN_PROGRESS
318
+
319
+ """
320
+ matcher = get_state_matcher()
321
+ adapter_states = self.get_available_states()
322
+ result = matcher.match_state(user_input, adapter_states)
323
+ return result.state
324
+
200
325
  async def validate_transition(
201
326
  self, ticket_id: str, target_state: TicketState
202
327
  ) -> bool:
203
328
  """Validate if state transition is allowed.
204
329
 
330
+ Validates both workflow rules and parent/child state constraints:
331
+ - Parent issues must remain at least as complete as their most complete child
332
+ - Standard workflow transitions must be valid
333
+
205
334
  Args:
335
+ ----
206
336
  ticket_id: Ticket identifier
207
337
  target_state: Target state
208
338
 
209
339
  Returns:
340
+ -------
210
341
  True if transition is valid
211
342
 
212
343
  """
213
344
  ticket = await self.read(ticket_id)
214
345
  if not ticket:
215
346
  return False
347
+
216
348
  # Handle case where state might be stored as string due to use_enum_values=True
217
349
  current_state = ticket.state
218
350
  if isinstance(current_state, str):
@@ -220,21 +352,51 @@ class BaseAdapter(ABC, Generic[T]):
220
352
  current_state = TicketState(current_state)
221
353
  except ValueError:
222
354
  return False
223
- return current_state.can_transition_to(target_state)
355
+
356
+ # Check workflow transition validity
357
+ if not current_state.can_transition_to(target_state):
358
+ return False
359
+
360
+ # Check parent/child state constraint
361
+ # If this ticket has children, ensure target state >= max child state
362
+ if isinstance(ticket, Task):
363
+ # Get all children
364
+ children = await self.list_tasks_by_issue(ticket_id)
365
+ if children:
366
+ # Find max child completion level
367
+ max_child_level = 0
368
+ for child in children:
369
+ child_state = child.state
370
+ if isinstance(child_state, str):
371
+ try:
372
+ child_state = TicketState(child_state)
373
+ except ValueError:
374
+ continue
375
+ max_child_level = max(
376
+ max_child_level, child_state.completion_level()
377
+ )
378
+
379
+ # Target state must be at least as complete as most complete child
380
+ if target_state.completion_level() < max_child_level:
381
+ return False
382
+
383
+ return True
224
384
 
225
385
  # Epic/Issue/Task Hierarchy Methods
226
386
 
227
387
  async def create_epic(
228
- self, title: str, description: Optional[str] = None, **kwargs
229
- ) -> Optional[Epic]:
388
+ self, title: str, description: str | None = None, **kwargs: Any
389
+ ) -> Epic | None:
230
390
  """Create epic (top-level grouping).
231
391
 
232
392
  Args:
393
+ ----
233
394
  title: Epic title
234
395
  description: Epic description
235
396
  **kwargs: Additional adapter-specific fields
236
397
 
237
398
  Returns:
399
+ -------
238
400
  Created epic or None if failed
239
401
 
240
402
  """
@@ -249,13 +411,15 @@ class BaseAdapter(ABC, Generic[T]):
249
411
  return result
250
412
  return None
251
413
 
252
- async def get_epic(self, epic_id: str) -> Optional[Epic]:
414
+ async def get_epic(self, epic_id: str) -> Epic | None:
253
415
  """Get epic by ID.
254
416
 
255
417
  Args:
418
+ ----
256
419
  epic_id: Epic identifier
257
420
 
258
421
  Returns:
422
+ -------
259
423
  Epic if found, None otherwise
260
424
 
261
425
  """
@@ -265,13 +429,15 @@ class BaseAdapter(ABC, Generic[T]):
265
429
  return result
266
430
  return None
267
431
 
268
- async def list_epics(self, **kwargs) -> builtins.list[Epic]:
432
+ async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
269
433
  """List all epics.
270
434
 
271
435
  Args:
436
+ ----
272
437
  **kwargs: Adapter-specific filter parameters
273
438
 
274
439
  Returns:
440
+ -------
275
441
  List of epics
276
442
 
277
443
  """
@@ -284,19 +450,21 @@ class BaseAdapter(ABC, Generic[T]):
284
450
  async def create_issue(
285
451
  self,
286
452
  title: str,
287
- description: Optional[str] = None,
288
- epic_id: Optional[str] = None,
289
- **kwargs,
290
- ) -> Optional[Task]:
453
+ description: str | None = None,
454
+ epic_id: str | None = None,
455
+ **kwargs: Any,
456
+ ) -> Task | None:
291
457
  """Create issue, optionally linked to epic.
292
458
 
293
459
  Args:
460
+ ----
294
461
  title: Issue title
295
462
  description: Issue description
296
463
  epic_id: Optional parent epic ID
297
464
  **kwargs: Additional adapter-specific fields
298
465
 
299
466
  Returns:
467
+ -------
300
468
  Created issue or None if failed
301
469
 
302
470
  """
@@ -313,9 +481,11 @@ class BaseAdapter(ABC, Generic[T]):
313
481
  """List all issues in epic.
314
482
 
315
483
  Args:
484
+ ----
316
485
  epic_id: Epic identifier
317
486
 
318
487
  Returns:
488
+ -------
319
489
  List of issues belonging to epic
320
490
 
321
491
  """
@@ -325,20 +495,23 @@ class BaseAdapter(ABC, Generic[T]):
325
495
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
326
496
 
327
497
  async def create_task(
328
- self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
329
- ) -> Optional[Task]:
498
+ self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
499
+ ) -> Task | None:
330
500
  """Create task as sub-ticket of parent issue.
331
501
 
332
502
  Args:
503
+ ----
333
504
  title: Task title
334
505
  parent_id: Required parent issue ID
335
506
  description: Task description
336
507
  **kwargs: Additional adapter-specific fields
337
508
 
338
509
  Returns:
510
+ -------
339
511
  Created task or None if failed
340
512
 
341
513
  Raises:
514
+ ------
342
515
  ValueError: If parent_id is not provided
343
516
 
344
517
  """
@@ -364,9 +537,11 @@ class BaseAdapter(ABC, Generic[T]):
364
537
  """List all tasks under an issue.
365
538
 
366
539
  Args:
540
+ ----
367
541
  issue_id: Issue identifier
368
542
 
369
543
  Returns:
544
+ -------
370
545
  List of tasks belonging to issue
371
546
 
372
547
  """
@@ -375,6 +550,431 @@ class BaseAdapter(ABC, Generic[T]):
375
550
  results = await self.list(filters=filters)
376
551
  return [r for r in results if isinstance(r, Task) and r.is_task()]
377
552
 
553
+ # Attachment methods
554
+ async def add_attachment(
555
+ self,
556
+ ticket_id: str,
557
+ file_path: str,
558
+ description: str | None = None,
559
+ ) -> Attachment:
560
+ """Attach a file to a ticket.
561
+
562
+ Args:
563
+ ----
564
+ ticket_id: Ticket identifier
565
+ file_path: Local file path to upload
566
+ description: Optional attachment description
567
+
568
+ Returns:
569
+ -------
570
+ Created Attachment with metadata
571
+
572
+ Raises:
573
+ ------
574
+ NotImplementedError: If adapter doesn't support attachments
575
+ FileNotFoundError: If file doesn't exist
576
+ ValueError: If ticket doesn't exist or upload fails
577
+
578
+ """
579
+ raise NotImplementedError(
580
+ f"{self.__class__.__name__} does not support file attachments. "
581
+ "Use comments to reference external files instead."
582
+ )
583
+
584
+ async def get_attachments(self, ticket_id: str) -> list[Attachment]:
585
+ """Get all attachments for a ticket.
586
+
587
+ Args:
588
+ ----
589
+ ticket_id: Ticket identifier
590
+
591
+ Returns:
592
+ -------
593
+ List of attachments (empty if none or not supported)
594
+
595
+ """
596
+ raise NotImplementedError(
597
+ f"{self.__class__.__name__} does not support file attachments."
598
+ )
599
+
600
+ async def delete_attachment(
601
+ self,
602
+ ticket_id: str,
603
+ attachment_id: str,
604
+ ) -> bool:
605
+ """Delete an attachment (optional implementation).
606
+
607
+ Args:
608
+ ----
609
+ ticket_id: Ticket identifier
610
+ attachment_id: Attachment identifier
611
+
612
+ Returns:
613
+ -------
614
+ True if deleted, False otherwise
615
+
616
+ Raises:
617
+ ------
618
+ NotImplementedError: If adapter doesn't support deletion
619
+
620
+ """
621
+ raise NotImplementedError(
622
+ f"{self.__class__.__name__} does not support attachment deletion."
623
+ )
624
+
378
625
  async def close(self) -> None:
379
626
  """Close adapter and cleanup resources."""
380
627
  pass
628
+
629
+ # Milestone Operations (Phase 1 - Abstract methods)
630
+
631
+ @abstractmethod
632
+ async def milestone_create(
633
+ self,
634
+ name: str,
635
+ target_date: datetime | None = None,
636
+ labels: list[str] | None = None,
637
+ description: str = "",
638
+ project_id: str | None = None,
639
+ ) -> Milestone:
640
+ """Create a new milestone.
641
+
642
+ Args:
643
+ ----
644
+ name: Milestone name
645
+ target_date: Target completion date (ISO format: YYYY-MM-DD)
646
+ labels: Labels that define this milestone
647
+ description: Milestone description
648
+ project_id: Associated project ID
649
+
650
+ Returns:
651
+ -------
652
+ Created Milestone object
653
+
654
+ """
655
+ pass
656
+
657
+ @abstractmethod
658
+ async def milestone_get(self, milestone_id: str) -> Milestone | None:
659
+ """Get milestone by ID with progress calculation.
660
+
661
+ Args:
662
+ ----
663
+ milestone_id: Milestone identifier
664
+
665
+ Returns:
666
+ -------
667
+ Milestone object with calculated progress, None if not found
668
+
669
+ """
670
+ pass
671
+
672
+ @abstractmethod
673
+ async def milestone_list(
674
+ self,
675
+ project_id: str | None = None,
676
+ state: str | None = None,
677
+ ) -> builtins.list[Milestone]:
678
+ """List milestones with optional filters.
679
+
680
+ Args:
681
+ ----
682
+ project_id: Filter by project
683
+ state: Filter by state (open, active, completed, closed)
684
+
685
+ Returns:
686
+ -------
687
+ List of Milestone objects
688
+
689
+ """
690
+ pass
691
+
692
+ @abstractmethod
693
+ async def milestone_update(
694
+ self,
695
+ milestone_id: str,
696
+ name: str | None = None,
697
+ target_date: datetime | None = None,
698
+ state: str | None = None,
699
+ labels: list[str] | None = None,
700
+ description: str | None = None,
701
+ ) -> Milestone | None:
702
+ """Update milestone properties.
703
+
704
+ Args:
705
+ ----
706
+ milestone_id: Milestone identifier
707
+ name: New name (optional)
708
+ target_date: New target date (optional)
709
+ state: New state (optional)
710
+ labels: New labels (optional)
711
+ description: New description (optional)
712
+
713
+ Returns:
714
+ -------
715
+ Updated Milestone object, None if not found
716
+
717
+ """
718
+ pass
719
+
720
+ @abstractmethod
721
+ async def milestone_delete(self, milestone_id: str) -> bool:
722
+ """Delete milestone.
723
+
724
+ Args:
725
+ ----
726
+ milestone_id: Milestone identifier
727
+
728
+ Returns:
729
+ -------
730
+ True if deleted successfully, False otherwise
731
+
732
+ """
733
+ pass
734
+
735
+ @abstractmethod
736
+ async def milestone_get_issues(
737
+ self,
738
+ milestone_id: str,
739
+ state: str | None = None,
740
+ ) -> builtins.list[Task]:
741
+ """Get issues associated with milestone.
742
+
743
+ Args:
744
+ ----
745
+ milestone_id: Milestone identifier
746
+ state: Filter by issue state (optional)
747
+
748
+ Returns:
749
+ -------
750
+ List of Task objects (issues)
751
+
752
+ """
753
+ pass
754
+
755
+ # Project Operations (Phase 1 - Abstract methods)
756
+ # These methods are optional - adapters that don't support projects
757
+ # can raise NotImplementedError with a helpful message
758
+
759
+ async def project_list(
760
+ self,
761
+ scope: ProjectScope | None = None,
762
+ state: ProjectState | None = None,
763
+ limit: int = 50,
764
+ offset: int = 0,
765
+ ) -> builtins.list[Project]:
766
+ """List projects with optional filters.
767
+
768
+ Args:
769
+ ----
770
+ scope: Filter by project scope (user, team, org, repo)
771
+ state: Filter by project state
772
+ limit: Maximum results (default: 50)
773
+ offset: Pagination offset (default: 0)
774
+
775
+ Returns:
776
+ -------
777
+ List of Project objects
778
+
779
+ Raises:
780
+ ------
781
+ NotImplementedError: If adapter doesn't support projects
782
+
783
+ """
784
+ raise NotImplementedError(
785
+ f"{self.__class__.__name__} does not support project operations. "
786
+ "Use Epic operations for this adapter."
787
+ )
788
+
789
+ async def project_get(self, project_id: str) -> Project | None:
790
+ """Get project by ID.
791
+
792
+ Args:
793
+ ----
794
+ project_id: Project identifier (platform-specific or unified)
795
+
796
+ Returns:
797
+ -------
798
+ Project object if found, None otherwise
799
+
800
+ Raises:
801
+ ------
802
+ NotImplementedError: If adapter doesn't support projects
803
+
804
+ """
805
+ raise NotImplementedError(
806
+ f"{self.__class__.__name__} does not support project operations. "
807
+ "Use get_epic() for this adapter."
808
+ )
809
+
810
+ async def project_create(
811
+ self,
812
+ name: str,
813
+ description: str | None = None,
814
+ state: ProjectState = ProjectState.PLANNED,
815
+ target_date: datetime | None = None,
816
+ **kwargs: Any,
817
+ ) -> Project:
818
+ """Create new project.
819
+
820
+ Args:
821
+ ----
822
+ name: Project name (required)
823
+ description: Project description
824
+ state: Initial project state (default: PLANNED)
825
+ target_date: Target completion date
826
+ **kwargs: Platform-specific additional fields
827
+
828
+ Returns:
829
+ -------
830
+ Created Project object
831
+
832
+ Raises:
833
+ ------
834
+ NotImplementedError: If adapter doesn't support projects
835
+
836
+ """
837
+ raise NotImplementedError(
838
+ f"{self.__class__.__name__} does not support project operations. "
839
+ "Use create_epic() for this adapter."
840
+ )
841
+
842
+ async def project_update(
843
+ self,
844
+ project_id: str,
845
+ name: str | None = None,
846
+ description: str | None = None,
847
+ state: ProjectState | None = None,
848
+ **kwargs: Any,
849
+ ) -> Project | None:
850
+ """Update project properties.
851
+
852
+ Args:
853
+ ----
854
+ project_id: Project identifier
855
+ name: New name (optional)
856
+ description: New description (optional)
857
+ state: New state (optional)
858
+ **kwargs: Platform-specific fields to update
859
+
860
+ Returns:
861
+ -------
862
+ Updated Project object, None if not found
863
+
864
+ Raises:
865
+ ------
866
+ NotImplementedError: If adapter doesn't support projects
867
+
868
+ """
869
+ raise NotImplementedError(
870
+ f"{self.__class__.__name__} does not support project operations."
871
+ )
872
+
873
+ async def project_delete(self, project_id: str) -> bool:
874
+ """Delete or archive project.
875
+
876
+ Args:
877
+ ----
878
+ project_id: Project identifier
879
+
880
+ Returns:
881
+ -------
882
+ True if deleted successfully, False otherwise
883
+
884
+ Raises:
885
+ ------
886
+ NotImplementedError: If adapter doesn't support projects
887
+
888
+ """
889
+ raise NotImplementedError(
890
+ f"{self.__class__.__name__} does not support project operations."
891
+ )
892
+
893
+ async def project_get_issues(
894
+ self, project_id: str, state: TicketState | None = None
895
+ ) -> builtins.list[Task]:
896
+ """Get all issues in project.
897
+
898
+ Args:
899
+ ----
900
+ project_id: Project identifier
901
+ state: Filter by issue state (optional)
902
+
903
+ Returns:
904
+ -------
905
+ List of Task objects (issues in project)
906
+
907
+ Raises:
908
+ ------
909
+ NotImplementedError: If adapter doesn't support projects
910
+
911
+ """
912
+ raise NotImplementedError(
913
+ f"{self.__class__.__name__} does not support project operations. "
914
+ "Use list_issues_by_epic() for this adapter."
915
+ )
916
+
917
+ async def project_add_issue(self, project_id: str, issue_id: str) -> bool:
918
+ """Add issue to project.
919
+
920
+ Args:
921
+ ----
922
+ project_id: Project identifier
923
+ issue_id: Issue identifier to add
924
+
925
+ Returns:
926
+ -------
927
+ True if added successfully, False otherwise
928
+
929
+ Raises:
930
+ ------
931
+ NotImplementedError: If adapter doesn't support projects
932
+
933
+ """
934
+ raise NotImplementedError(
935
+ f"{self.__class__.__name__} does not support project operations."
936
+ )
937
+
938
+ async def project_remove_issue(self, project_id: str, issue_id: str) -> bool:
939
+ """Remove issue from project.
940
+
941
+ Args:
942
+ ----
943
+ project_id: Project identifier
944
+ issue_id: Issue identifier to remove
945
+
946
+ Returns:
947
+ -------
948
+ True if removed successfully, False otherwise
949
+
950
+ Raises:
951
+ ------
952
+ NotImplementedError: If adapter doesn't support projects
953
+
954
+ """
955
+ raise NotImplementedError(
956
+ f"{self.__class__.__name__} does not support project operations."
957
+ )
958
+
959
+ async def project_get_statistics(self, project_id: str) -> ProjectStatistics:
960
+ """Get project statistics and metrics.
961
+
962
+ Calculates or retrieves statistics including issue counts by state,
963
+ progress percentage, and velocity metrics.
964
+
965
+ Args:
966
+ ----
967
+ project_id: Project identifier
968
+
969
+ Returns:
970
+ -------
971
+ ProjectStatistics object with calculated metrics
972
+
973
+ Raises:
974
+ ------
975
+ NotImplementedError: If adapter doesn't support projects
976
+
977
+ """
978
+ raise NotImplementedError(
979
+ f"{self.__class__.__name__} does not support project statistics."
980
+ )