mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/mappers.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from typing import Any, Generic,
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
7
|
|
|
8
8
|
from .models import Priority, TicketState
|
|
9
9
|
|
|
@@ -27,11 +27,11 @@ class BiDirectionalDict(Generic[T, U]):
|
|
|
27
27
|
self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
|
|
28
28
|
self._cache: dict[str, Any] = {}
|
|
29
29
|
|
|
30
|
-
def get_forward(self, key: T, default:
|
|
30
|
+
def get_forward(self, key: T, default: U | None = None) -> U | None:
|
|
31
31
|
"""Get value by forward key."""
|
|
32
32
|
return self._forward.get(key, default)
|
|
33
33
|
|
|
34
|
-
def get_reverse(self, key: U, default:
|
|
34
|
+
def get_reverse(self, key: U, default: T | None = None) -> T | None:
|
|
35
35
|
"""Get value by reverse key."""
|
|
36
36
|
return self._reverse.get(key, default)
|
|
37
37
|
|
|
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
|
|
|
83
83
|
"""Universal state mapping utility."""
|
|
84
84
|
|
|
85
85
|
def __init__(
|
|
86
|
-
self, adapter_type: str, custom_mappings:
|
|
86
|
+
self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
87
87
|
):
|
|
88
88
|
"""Initialize state mapper.
|
|
89
89
|
|
|
@@ -95,16 +95,16 @@ class StateMapper(BaseMapper):
|
|
|
95
95
|
super().__init__()
|
|
96
96
|
self.adapter_type = adapter_type
|
|
97
97
|
self.custom_mappings = custom_mappings or {}
|
|
98
|
-
self._mapping:
|
|
98
|
+
self._mapping: BiDirectionalDict | None = None
|
|
99
99
|
|
|
100
100
|
@lru_cache(maxsize=1)
|
|
101
|
-
def get_mapping(self) -> BiDirectionalDict:
|
|
101
|
+
def get_mapping(self) -> BiDirectionalDict[TicketState, str]:
|
|
102
102
|
"""Get cached bidirectional state mapping."""
|
|
103
103
|
if self._mapping is not None:
|
|
104
104
|
return self._mapping
|
|
105
105
|
|
|
106
106
|
# Default mappings by adapter type
|
|
107
|
-
default_mappings = {
|
|
107
|
+
default_mappings: dict[str, dict[TicketState, str]] = {
|
|
108
108
|
"github": {
|
|
109
109
|
TicketState.OPEN: "open",
|
|
110
110
|
TicketState.IN_PROGRESS: "open", # Uses labels
|
|
@@ -147,13 +147,16 @@ class StateMapper(BaseMapper):
|
|
|
147
147
|
},
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
mapping = default_mappings.get(self.adapter_type, {})
|
|
150
|
+
mapping: dict[TicketState, str] = default_mappings.get(self.adapter_type, {})
|
|
151
151
|
|
|
152
|
-
# Apply custom mappings
|
|
152
|
+
# Apply custom mappings (cast to proper type)
|
|
153
153
|
if self.custom_mappings:
|
|
154
|
-
|
|
154
|
+
# custom_mappings might have str keys, need to convert to TicketState
|
|
155
|
+
for key, value in self.custom_mappings.items():
|
|
156
|
+
if isinstance(key, TicketState):
|
|
157
|
+
mapping[key] = value
|
|
155
158
|
|
|
156
|
-
self._mapping = BiDirectionalDict(mapping)
|
|
159
|
+
self._mapping = BiDirectionalDict[TicketState, str](mapping)
|
|
157
160
|
return self._mapping
|
|
158
161
|
|
|
159
162
|
def to_system_state(self, adapter_state: str) -> TicketState:
|
|
@@ -168,7 +171,9 @@ class StateMapper(BaseMapper):
|
|
|
168
171
|
"""
|
|
169
172
|
cache_key = f"to_system_{adapter_state}"
|
|
170
173
|
if cache_key in self._cache:
|
|
171
|
-
|
|
174
|
+
cached = self._cache[cache_key]
|
|
175
|
+
if isinstance(cached, TicketState):
|
|
176
|
+
return cached
|
|
172
177
|
|
|
173
178
|
mapping = self.get_mapping()
|
|
174
179
|
result = mapping.get_reverse(adapter_state)
|
|
@@ -205,7 +210,9 @@ class StateMapper(BaseMapper):
|
|
|
205
210
|
"""
|
|
206
211
|
cache_key = f"from_system_{system_state.value}"
|
|
207
212
|
if cache_key in self._cache:
|
|
208
|
-
|
|
213
|
+
cached = self._cache[cache_key]
|
|
214
|
+
if isinstance(cached, str):
|
|
215
|
+
return cached
|
|
209
216
|
|
|
210
217
|
mapping = self.get_mapping()
|
|
211
218
|
result = mapping.get_forward(system_state)
|
|
@@ -229,7 +236,7 @@ class StateMapper(BaseMapper):
|
|
|
229
236
|
"""Check if adapter uses labels for extended states."""
|
|
230
237
|
return self.adapter_type in ["github", "linear"]
|
|
231
238
|
|
|
232
|
-
def get_state_label(self, state: TicketState) ->
|
|
239
|
+
def get_state_label(self, state: TicketState) -> str | None:
|
|
233
240
|
"""Get label name for extended states that require labels.
|
|
234
241
|
|
|
235
242
|
Args:
|
|
@@ -258,7 +265,7 @@ class PriorityMapper(BaseMapper):
|
|
|
258
265
|
"""Universal priority mapping utility."""
|
|
259
266
|
|
|
260
267
|
def __init__(
|
|
261
|
-
self, adapter_type: str, custom_mappings:
|
|
268
|
+
self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
262
269
|
):
|
|
263
270
|
"""Initialize priority mapper.
|
|
264
271
|
|
|
@@ -270,16 +277,16 @@ class PriorityMapper(BaseMapper):
|
|
|
270
277
|
super().__init__()
|
|
271
278
|
self.adapter_type = adapter_type
|
|
272
279
|
self.custom_mappings = custom_mappings or {}
|
|
273
|
-
self._mapping:
|
|
280
|
+
self._mapping: BiDirectionalDict | None = None
|
|
274
281
|
|
|
275
282
|
@lru_cache(maxsize=1)
|
|
276
|
-
def get_mapping(self) -> BiDirectionalDict:
|
|
283
|
+
def get_mapping(self) -> BiDirectionalDict[Priority, Any]:
|
|
277
284
|
"""Get cached bidirectional priority mapping."""
|
|
278
285
|
if self._mapping is not None:
|
|
279
286
|
return self._mapping
|
|
280
287
|
|
|
281
288
|
# Default mappings by adapter type
|
|
282
|
-
default_mappings = {
|
|
289
|
+
default_mappings: dict[str, dict[Priority, Any]] = {
|
|
283
290
|
"github": {
|
|
284
291
|
Priority.CRITICAL: "P0",
|
|
285
292
|
Priority.HIGH: "P1",
|
|
@@ -306,13 +313,16 @@ class PriorityMapper(BaseMapper):
|
|
|
306
313
|
},
|
|
307
314
|
}
|
|
308
315
|
|
|
309
|
-
mapping = default_mappings.get(self.adapter_type, {})
|
|
316
|
+
mapping: dict[Priority, Any] = default_mappings.get(self.adapter_type, {})
|
|
310
317
|
|
|
311
|
-
# Apply custom mappings
|
|
318
|
+
# Apply custom mappings (cast to proper type)
|
|
312
319
|
if self.custom_mappings:
|
|
313
|
-
|
|
320
|
+
# custom_mappings might have str keys, need to convert to Priority
|
|
321
|
+
for key, value in self.custom_mappings.items():
|
|
322
|
+
if isinstance(key, Priority):
|
|
323
|
+
mapping[key] = value
|
|
314
324
|
|
|
315
|
-
self._mapping = BiDirectionalDict(mapping)
|
|
325
|
+
self._mapping = BiDirectionalDict[Priority, Any](mapping)
|
|
316
326
|
return self._mapping
|
|
317
327
|
|
|
318
328
|
def to_system_priority(self, adapter_priority: Any) -> Priority:
|
|
@@ -327,7 +337,9 @@ class PriorityMapper(BaseMapper):
|
|
|
327
337
|
"""
|
|
328
338
|
cache_key = f"to_system_{adapter_priority}"
|
|
329
339
|
if cache_key in self._cache:
|
|
330
|
-
|
|
340
|
+
cached = self._cache[cache_key]
|
|
341
|
+
if isinstance(cached, Priority):
|
|
342
|
+
return cached
|
|
331
343
|
|
|
332
344
|
mapping = self.get_mapping()
|
|
333
345
|
result = mapping.get_reverse(adapter_priority)
|
|
@@ -365,7 +377,7 @@ class PriorityMapper(BaseMapper):
|
|
|
365
377
|
]:
|
|
366
378
|
result = Priority.LOW
|
|
367
379
|
break
|
|
368
|
-
elif isinstance(adapter_priority,
|
|
380
|
+
elif isinstance(adapter_priority, int | float):
|
|
369
381
|
# Handle numeric priorities (Linear-style)
|
|
370
382
|
if adapter_priority <= 1:
|
|
371
383
|
result = Priority.CRITICAL
|
|
@@ -483,7 +495,7 @@ class MapperRegistry:
|
|
|
483
495
|
|
|
484
496
|
@classmethod
|
|
485
497
|
def get_state_mapper(
|
|
486
|
-
cls, adapter_type: str, custom_mappings:
|
|
498
|
+
cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
487
499
|
) -> StateMapper:
|
|
488
500
|
"""Get or create state mapper for adapter type.
|
|
489
501
|
|
|
@@ -502,7 +514,7 @@ class MapperRegistry:
|
|
|
502
514
|
|
|
503
515
|
@classmethod
|
|
504
516
|
def get_priority_mapper(
|
|
505
|
-
cls, adapter_type: str, custom_mappings:
|
|
517
|
+
cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
506
518
|
) -> PriorityMapper:
|
|
507
519
|
"""Get or create priority mapper for adapter type.
|
|
508
520
|
|
|
@@ -524,10 +536,10 @@ class MapperRegistry:
|
|
|
524
536
|
@classmethod
|
|
525
537
|
def clear_cache(cls) -> None:
|
|
526
538
|
"""Clear all mapper caches."""
|
|
527
|
-
for
|
|
528
|
-
|
|
529
|
-
for
|
|
530
|
-
|
|
539
|
+
for state_mapper in cls._state_mappers.values():
|
|
540
|
+
state_mapper.clear_cache()
|
|
541
|
+
for priority_mapper in cls._priority_mappers.values():
|
|
542
|
+
priority_mapper.clear_cache()
|
|
531
543
|
|
|
532
544
|
@classmethod
|
|
533
545
|
def reset(cls) -> None:
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Universal Ticket models using Pydantic.
|
|
2
|
+
|
|
3
|
+
This module defines the core data models for the MCP Ticketer system, providing
|
|
4
|
+
a unified interface across different ticket management platforms (Linear, JIRA,
|
|
5
|
+
GitHub, etc.).
|
|
6
|
+
|
|
7
|
+
The models follow a hierarchical structure:
|
|
8
|
+
- Epic: Strategic level containers (Projects in Linear, Epics in JIRA)
|
|
9
|
+
- Issue: Standard work items (Issues in GitHub, Stories in JIRA)
|
|
10
|
+
- Task: Sub-work items (Sub-issues in Linear, Sub-tasks in JIRA)
|
|
11
|
+
|
|
12
|
+
All models use Pydantic v2 for validation and serialization, ensuring type safety
|
|
13
|
+
and consistent data handling across adapters.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from mcp_ticketer.core.models import Task, Priority, TicketState
|
|
17
|
+
>>> task = Task(
|
|
18
|
+
... title="Fix authentication bug",
|
|
19
|
+
... priority=Priority.HIGH,
|
|
20
|
+
... state=TicketState.IN_PROGRESS
|
|
21
|
+
... )
|
|
22
|
+
>>> print(task.model_dump_json())
|
|
23
|
+
|
|
24
|
+
"""
|
|
2
25
|
|
|
3
26
|
from datetime import datetime
|
|
4
27
|
from enum import Enum
|
|
5
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
6
29
|
|
|
7
30
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
31
|
|
|
9
32
|
|
|
10
33
|
class Priority(str, Enum):
|
|
11
|
-
"""Universal priority levels.
|
|
34
|
+
"""Universal priority levels for tickets.
|
|
35
|
+
|
|
36
|
+
These priority levels are mapped to platform-specific priorities:
|
|
37
|
+
- Linear: 1 (Critical), 2 (High), 3 (Medium), 4 (Low)
|
|
38
|
+
- JIRA: Highest, High, Medium, Low
|
|
39
|
+
- GitHub: P0/critical, P1/high, P2/medium, P3/low labels
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
LOW: Low priority, non-urgent work
|
|
43
|
+
MEDIUM: Standard priority, default for most work
|
|
44
|
+
HIGH: High priority, should be addressed soon
|
|
45
|
+
CRITICAL: Critical priority, urgent work requiring immediate attention
|
|
46
|
+
|
|
47
|
+
"""
|
|
12
48
|
|
|
13
49
|
LOW = "low"
|
|
14
50
|
MEDIUM = "medium"
|
|
@@ -17,7 +53,23 @@ class Priority(str, Enum):
|
|
|
17
53
|
|
|
18
54
|
|
|
19
55
|
class TicketType(str, Enum):
|
|
20
|
-
"""Ticket type hierarchy.
|
|
56
|
+
"""Ticket type hierarchy for organizing work.
|
|
57
|
+
|
|
58
|
+
Defines the three-level hierarchy used across all platforms:
|
|
59
|
+
|
|
60
|
+
Platform Mappings:
|
|
61
|
+
- Linear: Project (Epic) → Issue (Issue) → Sub-issue (Task)
|
|
62
|
+
- JIRA: Epic (Epic) → Story/Task (Issue) → Sub-task (Task)
|
|
63
|
+
- GitHub: Milestone (Epic) → Issue (Issue) → Checklist item (Task)
|
|
64
|
+
- Aitrackdown: Epic file → Issue file → Task reference
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
EPIC: Strategic level containers for large features or initiatives
|
|
68
|
+
ISSUE: Standard work items, the primary unit of work
|
|
69
|
+
TASK: Sub-work items, smaller pieces of an issue
|
|
70
|
+
SUBTASK: Alias for TASK for backward compatibility
|
|
71
|
+
|
|
72
|
+
"""
|
|
21
73
|
|
|
22
74
|
EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
|
|
23
75
|
ISSUE = "issue" # Work item level (standard issues/tasks)
|
|
@@ -26,7 +78,34 @@ class TicketType(str, Enum):
|
|
|
26
78
|
|
|
27
79
|
|
|
28
80
|
class TicketState(str, Enum):
|
|
29
|
-
"""Universal ticket states with state machine
|
|
81
|
+
"""Universal ticket states with workflow state machine.
|
|
82
|
+
|
|
83
|
+
Implements a standardized workflow that maps to different platform states:
|
|
84
|
+
|
|
85
|
+
State Flow:
|
|
86
|
+
OPEN → IN_PROGRESS → READY → TESTED → DONE → CLOSED
|
|
87
|
+
↓ ↓ ↓
|
|
88
|
+
CLOSED WAITING BLOCKED
|
|
89
|
+
↓ ↓
|
|
90
|
+
IN_PROGRESS ← IN_PROGRESS
|
|
91
|
+
|
|
92
|
+
Platform Mappings:
|
|
93
|
+
- Linear: Backlog (OPEN), Started (IN_PROGRESS), Completed (DONE), Canceled (CLOSED)
|
|
94
|
+
- JIRA: To Do (OPEN), In Progress (IN_PROGRESS), Done (DONE), etc.
|
|
95
|
+
- GitHub: open (OPEN), closed (CLOSED) + labels for extended states
|
|
96
|
+
- Aitrackdown: File-based state tracking
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
OPEN: Initial state, work not yet started
|
|
100
|
+
IN_PROGRESS: Work is actively being done
|
|
101
|
+
READY: Work is complete and ready for review/testing
|
|
102
|
+
TESTED: Work has been tested and verified
|
|
103
|
+
DONE: Work is complete and accepted
|
|
104
|
+
WAITING: Work is paused waiting for external dependency
|
|
105
|
+
BLOCKED: Work is blocked by an impediment
|
|
106
|
+
CLOSED: Final state, work is closed/archived
|
|
107
|
+
|
|
108
|
+
"""
|
|
30
109
|
|
|
31
110
|
OPEN = "open"
|
|
32
111
|
IN_PROGRESS = "in_progress"
|
|
@@ -39,7 +118,15 @@ class TicketState(str, Enum):
|
|
|
39
118
|
|
|
40
119
|
@classmethod
|
|
41
120
|
def valid_transitions(cls) -> dict[str, list[str]]:
|
|
42
|
-
"""Define valid state transitions.
|
|
121
|
+
"""Define valid state transitions for workflow enforcement.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary mapping each state to list of valid target states
|
|
125
|
+
|
|
126
|
+
Note:
|
|
127
|
+
CLOSED is a terminal state with no valid transitions
|
|
128
|
+
|
|
129
|
+
"""
|
|
43
130
|
return {
|
|
44
131
|
cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
|
|
45
132
|
cls.IN_PROGRESS: [cls.READY, cls.WAITING, cls.BLOCKED, cls.OPEN],
|
|
@@ -52,23 +139,100 @@ class TicketState(str, Enum):
|
|
|
52
139
|
}
|
|
53
140
|
|
|
54
141
|
def can_transition_to(self, target: "TicketState") -> bool:
|
|
55
|
-
"""Check if transition to target state is valid.
|
|
142
|
+
"""Check if transition to target state is valid.
|
|
143
|
+
|
|
144
|
+
Validates state transitions according to the defined workflow rules.
|
|
145
|
+
This prevents invalid state changes and ensures workflow integrity.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
target: The state to transition to
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if the transition is valid, False otherwise
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> state = TicketState.OPEN
|
|
155
|
+
>>> state.can_transition_to(TicketState.IN_PROGRESS)
|
|
156
|
+
True
|
|
157
|
+
>>> state.can_transition_to(TicketState.DONE)
|
|
158
|
+
False
|
|
159
|
+
|
|
160
|
+
"""
|
|
56
161
|
return target.value in self.valid_transitions().get(self, [])
|
|
57
162
|
|
|
163
|
+
def completion_level(self) -> int:
|
|
164
|
+
"""Get numeric completion level for state ordering.
|
|
165
|
+
|
|
166
|
+
Higher numbers indicate more complete states. Used for parent/child
|
|
167
|
+
state constraints where parents must be at least as complete as
|
|
168
|
+
their most complete child.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Completion level (0-7)
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
>>> TicketState.OPEN.completion_level()
|
|
175
|
+
0
|
|
176
|
+
>>> TicketState.DONE.completion_level()
|
|
177
|
+
6
|
|
178
|
+
>>> TicketState.DONE.completion_level() > TicketState.IN_PROGRESS.completion_level()
|
|
179
|
+
True
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
levels = {
|
|
183
|
+
TicketState.OPEN: 0, # Not started
|
|
184
|
+
TicketState.BLOCKED: 1, # Blocked
|
|
185
|
+
TicketState.WAITING: 2, # Waiting
|
|
186
|
+
TicketState.IN_PROGRESS: 3, # In progress
|
|
187
|
+
TicketState.READY: 4, # Ready for review
|
|
188
|
+
TicketState.TESTED: 5, # Tested
|
|
189
|
+
TicketState.DONE: 6, # Done
|
|
190
|
+
TicketState.CLOSED: 7, # Closed (terminal)
|
|
191
|
+
}
|
|
192
|
+
return levels.get(self, 0)
|
|
193
|
+
|
|
58
194
|
|
|
59
195
|
class BaseTicket(BaseModel):
|
|
60
|
-
"""Base model for all ticket types.
|
|
196
|
+
"""Base model for all ticket types with universal field mapping.
|
|
197
|
+
|
|
198
|
+
Provides common fields and functionality shared across all ticket types
|
|
199
|
+
(Epic, Task, Comment). Uses Pydantic v2 for validation and serialization.
|
|
200
|
+
|
|
201
|
+
The metadata field allows adapters to store platform-specific information
|
|
202
|
+
while maintaining the universal interface.
|
|
203
|
+
|
|
204
|
+
Attributes:
|
|
205
|
+
id: Unique identifier assigned by the platform
|
|
206
|
+
title: Human-readable title (required, min 1 character)
|
|
207
|
+
description: Optional detailed description or body text
|
|
208
|
+
state: Current workflow state (defaults to OPEN)
|
|
209
|
+
priority: Priority level (defaults to MEDIUM)
|
|
210
|
+
tags: List of tags/labels for categorization
|
|
211
|
+
created_at: Timestamp when ticket was created
|
|
212
|
+
updated_at: Timestamp when ticket was last modified
|
|
213
|
+
metadata: Platform-specific data and field mappings
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> ticket = BaseTicket(
|
|
217
|
+
... title="Fix login issue",
|
|
218
|
+
... description="Users cannot log in with SSO",
|
|
219
|
+
... priority=Priority.HIGH,
|
|
220
|
+
... tags=["bug", "authentication"]
|
|
221
|
+
... )
|
|
222
|
+
>>> ticket.state = TicketState.IN_PROGRESS
|
|
223
|
+
|
|
224
|
+
"""
|
|
61
225
|
|
|
62
226
|
model_config = ConfigDict(use_enum_values=True)
|
|
63
227
|
|
|
64
|
-
id:
|
|
228
|
+
id: str | None = Field(None, description="Unique identifier")
|
|
65
229
|
title: str = Field(..., min_length=1, description="Ticket title")
|
|
66
|
-
description:
|
|
230
|
+
description: str | None = Field(None, description="Detailed description")
|
|
67
231
|
state: TicketState = Field(TicketState.OPEN, description="Current state")
|
|
68
232
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
|
69
233
|
tags: list[str] = Field(default_factory=list, description="Tags/labels")
|
|
70
|
-
created_at:
|
|
71
|
-
updated_at:
|
|
234
|
+
created_at: datetime | None = Field(None, description="Creation timestamp")
|
|
235
|
+
updated_at: datetime | None = Field(None, description="Last update timestamp")
|
|
72
236
|
|
|
73
237
|
# Metadata for field mapping to different systems
|
|
74
238
|
metadata: dict[str, Any] = Field(
|
|
@@ -77,7 +241,33 @@ class BaseTicket(BaseModel):
|
|
|
77
241
|
|
|
78
242
|
|
|
79
243
|
class Epic(BaseTicket):
|
|
80
|
-
"""Epic - highest level container for work
|
|
244
|
+
"""Epic - highest level container for strategic work initiatives.
|
|
245
|
+
|
|
246
|
+
Epics represent large features, projects, or initiatives that contain
|
|
247
|
+
multiple related issues. They map to different concepts across platforms:
|
|
248
|
+
|
|
249
|
+
Platform Mappings:
|
|
250
|
+
- Linear: Projects (with issues as children)
|
|
251
|
+
- JIRA: Epics (with stories/tasks as children)
|
|
252
|
+
- GitHub: Milestones (with issues as children)
|
|
253
|
+
- Aitrackdown: Epic files (with issue references)
|
|
254
|
+
|
|
255
|
+
Epics sit at the top of the hierarchy and cannot have parent epics.
|
|
256
|
+
They can contain multiple child issues, which in turn can contain tasks.
|
|
257
|
+
|
|
258
|
+
Attributes:
|
|
259
|
+
ticket_type: Always TicketType.EPIC (frozen field)
|
|
260
|
+
child_issues: List of issue IDs that belong to this epic
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
>>> epic = Epic(
|
|
264
|
+
... title="User Authentication System",
|
|
265
|
+
... description="Complete overhaul of authentication",
|
|
266
|
+
... priority=Priority.HIGH
|
|
267
|
+
... )
|
|
268
|
+
>>> epic.child_issues = ["ISSUE-123", "ISSUE-124"]
|
|
269
|
+
|
|
270
|
+
"""
|
|
81
271
|
|
|
82
272
|
ticket_type: TicketType = Field(
|
|
83
273
|
default=TicketType.EPIC, frozen=True, description="Always EPIC type"
|
|
@@ -89,8 +279,11 @@ class Epic(BaseTicket):
|
|
|
89
279
|
def validate_hierarchy(self) -> list[str]:
|
|
90
280
|
"""Validate epic hierarchy rules.
|
|
91
281
|
|
|
282
|
+
Epics are at the top of the hierarchy and have no parent constraints.
|
|
283
|
+
This method is provided for consistency with other ticket types.
|
|
284
|
+
|
|
92
285
|
Returns:
|
|
93
|
-
|
|
286
|
+
Empty list (epics have no hierarchy constraints)
|
|
94
287
|
|
|
95
288
|
"""
|
|
96
289
|
# Epics don't have parents in our hierarchy
|
|
@@ -98,19 +291,47 @@ class Epic(BaseTicket):
|
|
|
98
291
|
|
|
99
292
|
|
|
100
293
|
class Task(BaseTicket):
|
|
101
|
-
"""Task - individual work item (can be ISSUE or TASK type).
|
|
294
|
+
"""Task - individual work item (can be ISSUE or TASK type).
|
|
295
|
+
|
|
296
|
+
Note: The `project` field is a synonym for `parent_epic` to provide
|
|
297
|
+
flexibility in CLI and API usage. Both fields map to the same underlying
|
|
298
|
+
value (the parent epic/project ID).
|
|
299
|
+
"""
|
|
102
300
|
|
|
103
301
|
ticket_type: TicketType = Field(
|
|
104
302
|
default=TicketType.ISSUE, description="Ticket type in hierarchy"
|
|
105
303
|
)
|
|
106
|
-
parent_issue:
|
|
107
|
-
parent_epic:
|
|
108
|
-
|
|
304
|
+
parent_issue: str | None = Field(None, description="Parent issue ID (for tasks)")
|
|
305
|
+
parent_epic: str | None = Field(
|
|
306
|
+
None,
|
|
307
|
+
description="Parent epic/project ID (for issues). Synonym: 'project'",
|
|
308
|
+
)
|
|
309
|
+
assignee: str | None = Field(None, description="Assigned user")
|
|
109
310
|
children: list[str] = Field(default_factory=list, description="Child task IDs")
|
|
110
311
|
|
|
111
312
|
# Additional fields common across systems
|
|
112
|
-
estimated_hours:
|
|
113
|
-
actual_hours:
|
|
313
|
+
estimated_hours: float | None = Field(None, description="Time estimate")
|
|
314
|
+
actual_hours: float | None = Field(None, description="Actual time spent")
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def project(self) -> str | None:
|
|
318
|
+
"""Synonym for parent_epic.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Parent epic/project ID
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
return self.parent_epic
|
|
325
|
+
|
|
326
|
+
@project.setter
|
|
327
|
+
def project(self, value: str | None) -> None:
|
|
328
|
+
"""Set parent_epic via project synonym.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
value: Parent epic/project ID
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
self.parent_epic = value
|
|
114
335
|
|
|
115
336
|
def is_epic(self) -> bool:
|
|
116
337
|
"""Check if this is an epic (should use Epic class instead)."""
|
|
@@ -155,23 +376,54 @@ class Comment(BaseModel):
|
|
|
155
376
|
|
|
156
377
|
model_config = ConfigDict(use_enum_values=True)
|
|
157
378
|
|
|
158
|
-
id:
|
|
379
|
+
id: str | None = Field(None, description="Comment ID")
|
|
159
380
|
ticket_id: str = Field(..., description="Parent ticket ID")
|
|
160
|
-
author:
|
|
381
|
+
author: str | None = Field(None, description="Comment author")
|
|
161
382
|
content: str = Field(..., min_length=1, description="Comment text")
|
|
162
|
-
created_at:
|
|
383
|
+
created_at: datetime | None = Field(None, description="Creation timestamp")
|
|
163
384
|
metadata: dict[str, Any] = Field(
|
|
164
385
|
default_factory=dict, description="System-specific metadata"
|
|
165
386
|
)
|
|
166
387
|
|
|
167
388
|
|
|
389
|
+
class Attachment(BaseModel):
|
|
390
|
+
"""File attachment metadata for tickets.
|
|
391
|
+
|
|
392
|
+
Represents a file attached to a ticket across all adapters.
|
|
393
|
+
Each adapter maps its native attachment format to this model.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
397
|
+
|
|
398
|
+
id: str | None = Field(None, description="Attachment unique identifier")
|
|
399
|
+
ticket_id: str = Field(..., description="Parent ticket identifier")
|
|
400
|
+
filename: str = Field(..., description="Original filename")
|
|
401
|
+
url: str | None = Field(None, description="Download URL or file path")
|
|
402
|
+
content_type: str | None = Field(
|
|
403
|
+
None, description="MIME type (e.g., 'application/pdf', 'image/png')"
|
|
404
|
+
)
|
|
405
|
+
size_bytes: int | None = Field(None, description="File size in bytes")
|
|
406
|
+
created_at: datetime | None = Field(None, description="Upload timestamp")
|
|
407
|
+
created_by: str | None = Field(None, description="User who uploaded the attachment")
|
|
408
|
+
description: str | None = Field(None, description="Attachment description or notes")
|
|
409
|
+
metadata: dict[str, Any] = Field(
|
|
410
|
+
default_factory=dict, description="Adapter-specific attachment metadata"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def __str__(self) -> str:
|
|
414
|
+
"""Return string representation showing filename and size."""
|
|
415
|
+
size_str = f" ({self.size_bytes} bytes)" if self.size_bytes else ""
|
|
416
|
+
return f"Attachment({self.filename}{size_str})"
|
|
417
|
+
|
|
418
|
+
|
|
168
419
|
class SearchQuery(BaseModel):
|
|
169
420
|
"""Search query parameters."""
|
|
170
421
|
|
|
171
|
-
query:
|
|
172
|
-
state:
|
|
173
|
-
priority:
|
|
174
|
-
tags:
|
|
175
|
-
assignee:
|
|
422
|
+
query: str | None = Field(None, description="Text search query")
|
|
423
|
+
state: TicketState | None = Field(None, description="Filter by state")
|
|
424
|
+
priority: Priority | None = Field(None, description="Filter by priority")
|
|
425
|
+
tags: list[str] | None = Field(None, description="Filter by tags")
|
|
426
|
+
assignee: str | None = Field(None, description="Filter by assignee")
|
|
427
|
+
project: str | None = Field(None, description="Filter by project/epic ID or name")
|
|
176
428
|
limit: int = Field(10, gt=0, le=100, description="Maximum results")
|
|
177
429
|
offset: int = Field(0, ge=0, description="Result offset for pagination")
|