mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/__init__.py CHANGED
@@ -1,16 +1,16 @@
1
1
  """MCP Ticketer - Universal ticket management interface."""
2
2
 
3
3
  from .__version__ import (
4
- __author__,
5
- __author_email__,
6
- __copyright__,
7
- __description__,
8
- __license__,
9
- __title__,
10
- __version__,
11
- __version_info__,
12
- get_user_agent,
13
- get_version,
4
+ __author__,
5
+ __author_email__,
6
+ __copyright__,
7
+ __description__,
8
+ __license__,
9
+ __title__,
10
+ __version__,
11
+ __version_info__,
12
+ get_user_agent,
13
+ get_version,
14
14
  )
15
15
 
16
16
  __all__ = [
@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.12.0"
3
+ __version__ = "2.0.1"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
@@ -23,14 +23,14 @@ logger = logging.getLogger(__name__)
23
23
 
24
24
  # Import ai-trackdown-pytools when available
25
25
  try:
26
- from ai_trackdown_pytools import AITrackdown
27
- from ai_trackdown_pytools import Ticket as AITicket
26
+ from ai_trackdown_pytools import AITrackdown # type: ignore[attr-defined]
27
+ from ai_trackdown_pytools import Ticket as AITicket # type: ignore[attr-defined]
28
28
 
29
29
  HAS_AITRACKDOWN = True
30
30
  except ImportError:
31
31
  HAS_AITRACKDOWN = False
32
- AITrackdown = None
33
- AITicket = None
32
+ AITrackdown = None # type: ignore[assignment]
33
+ AITicket = None # type: ignore[assignment]
34
34
 
35
35
 
36
36
  class AITrackdownAdapter(BaseAdapter[Task]):
@@ -40,6 +40,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
40
40
  """Initialize AI-Trackdown adapter.
41
41
 
42
42
  Args:
43
+ ----
43
44
  config: Configuration with 'base_path' for tickets directory
44
45
 
45
46
  """
@@ -64,6 +65,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
64
65
  AITrackdown is file-based and doesn't require credentials.
65
66
 
66
67
  Returns:
68
+ -------
67
69
  (is_valid, error_message) - Always returns (True, "") for AITrackdown
68
70
 
69
71
  """
@@ -128,6 +130,8 @@ class AITrackdownAdapter(BaseAdapter[Task]):
128
130
  parent_issue=ai_ticket.get("parent_issue"),
129
131
  parent_epic=ai_ticket.get("parent_epic"),
130
132
  assignee=ai_ticket.get("assignee"),
133
+ estimated_hours=ai_ticket.get("estimated_hours"),
134
+ actual_hours=ai_ticket.get("actual_hours"),
131
135
  created_at=(
132
136
  datetime.fromisoformat(ai_ticket["created_at"])
133
137
  if "created_at" in ai_ticket
@@ -180,13 +184,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
180
184
  """Convert universal Task to AI-Trackdown ticket."""
181
185
  # Handle enum values that may be stored as strings due to use_enum_values=True
182
186
  # Note: task.state is always a string due to ConfigDict(use_enum_values=True)
183
- state_value = task.state
187
+ state_value: str
184
188
  if isinstance(task.state, TicketState):
185
189
  state_value = self._get_state_mapping()[task.state]
186
190
  elif isinstance(task.state, str):
187
191
  # Already a string - keep as-is (don't convert to kebab-case)
188
192
  # The state is already in snake_case format from the enum value
189
193
  state_value = task.state
194
+ else:
195
+ state_value = str(task.state)
190
196
 
191
197
  return {
192
198
  "id": task.id,
@@ -208,13 +214,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
208
214
  """Convert universal Epic to AI-Trackdown ticket."""
209
215
  # Handle enum values that may be stored as strings due to use_enum_values=True
210
216
  # Note: epic.state is always a string due to ConfigDict(use_enum_values=True)
211
- state_value = epic.state
217
+ state_value: str
212
218
  if isinstance(epic.state, TicketState):
213
219
  state_value = self._get_state_mapping()[epic.state]
214
220
  elif isinstance(epic.state, str):
215
221
  # Already a string - keep as-is (don't convert to kebab-case)
216
222
  # The state is already in snake_case format from the enum value
217
223
  state_value = epic.state
224
+ else:
225
+ state_value = str(epic.state)
218
226
 
219
227
  return {
220
228
  "id": epic.id,
@@ -286,11 +294,13 @@ class AITrackdownAdapter(BaseAdapter[Task]):
286
294
  """Create a new epic.
287
295
 
288
296
  Args:
297
+ ----
289
298
  title: Epic title
290
299
  description: Epic description
291
300
  **kwargs: Additional epic properties
292
301
 
293
302
  Returns:
303
+ -------
294
304
  Created Epic instance
295
305
 
296
306
  """
@@ -307,12 +317,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
307
317
  """Create a new issue.
308
318
 
309
319
  Args:
320
+ ----
310
321
  title: Issue title
311
322
  parent_epic: Parent epic ID
312
323
  description: Issue description
313
324
  **kwargs: Additional issue properties
314
325
 
315
326
  Returns:
327
+ -------
316
328
  Created Task instance (representing an issue)
317
329
 
318
330
  """
@@ -327,12 +339,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
327
339
  """Create a new task under an issue.
328
340
 
329
341
  Args:
342
+ ----
330
343
  title: Task title
331
344
  parent_id: Parent issue ID
332
345
  description: Task description
333
346
  **kwargs: Additional task properties
334
347
 
335
348
  Returns:
349
+ -------
336
350
  Created Task instance
337
351
 
338
352
  """
@@ -362,13 +376,16 @@ class AITrackdownAdapter(BaseAdapter[Task]):
362
376
  """Update a task or epic.
363
377
 
364
378
  Args:
379
+ ----
365
380
  ticket_id: ID of ticket to update
366
381
  updates: Dictionary of updates or Task object with new values
367
382
 
368
383
  Returns:
384
+ -------
369
385
  Updated Task or Epic, or None if ticket not found
370
386
 
371
387
  Raises:
388
+ ------
372
389
  AttributeError: If update fails due to invalid fields
373
390
 
374
391
  """
@@ -555,9 +572,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
555
572
  """Get epic by ID.
556
573
 
557
574
  Args:
575
+ ----
558
576
  epic_id: Epic ID to retrieve
559
577
 
560
578
  Returns:
579
+ -------
561
580
  Epic if found, None otherwise
562
581
 
563
582
  """
@@ -580,10 +599,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
580
599
  """List all epics.
581
600
 
582
601
  Args:
602
+ ----
583
603
  limit: Maximum number of epics to return
584
604
  offset: Number of epics to skip
585
605
 
586
606
  Returns:
607
+ -------
587
608
  List of epics
588
609
 
589
610
  """
@@ -598,9 +619,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
598
619
  """List all issues belonging to an epic.
599
620
 
600
621
  Args:
622
+ ----
601
623
  epic_id: Epic ID to get issues for
602
624
 
603
625
  Returns:
626
+ -------
604
627
  List of issues (tasks with parent_epic set)
605
628
 
606
629
  """
@@ -615,9 +638,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
615
638
  """List all tasks belonging to an issue.
616
639
 
617
640
  Args:
641
+ ----
618
642
  issue_id: Issue ID (parent task) to get child tasks for
619
643
 
620
644
  Returns:
645
+ -------
621
646
  List of tasks
622
647
 
623
648
  """
@@ -633,9 +658,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
633
658
  """Sanitize filename to prevent security issues.
634
659
 
635
660
  Args:
661
+ ----
636
662
  filename: Original filename
637
663
 
638
664
  Returns:
665
+ -------
639
666
  Sanitized filename safe for filesystem
640
667
 
641
668
  """
@@ -655,9 +682,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
655
682
  """Guess MIME type from file extension.
656
683
 
657
684
  Args:
685
+ ----
658
686
  file_path: Path to file
659
687
 
660
688
  Returns:
689
+ -------
661
690
  MIME type string
662
691
 
663
692
  """
@@ -670,9 +699,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
670
699
  """Calculate SHA256 checksum of file.
671
700
 
672
701
  Args:
702
+ ----
673
703
  file_path: Path to file
674
704
 
675
705
  Returns:
706
+ -------
676
707
  Hexadecimal checksum string
677
708
 
678
709
  """
@@ -695,14 +726,17 @@ class AITrackdownAdapter(BaseAdapter[Task]):
695
726
  """Attach a file to a ticket (local filesystem storage).
696
727
 
697
728
  Args:
729
+ ----
698
730
  ticket_id: Ticket identifier
699
731
  file_path: Local file path to attach
700
732
  description: Optional attachment description
701
733
 
702
734
  Returns:
735
+ -------
703
736
  Attachment metadata
704
737
 
705
738
  Raises:
739
+ ------
706
740
  ValueError: If ticket doesn't exist
707
741
  FileNotFoundError: If file doesn't exist
708
742
 
@@ -767,9 +801,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
767
801
  """Get all attachments for a ticket with path traversal protection.
768
802
 
769
803
  Args:
804
+ ----
770
805
  ticket_id: Ticket identifier
771
806
 
772
807
  Returns:
808
+ -------
773
809
  List of attachments (empty if none)
774
810
 
775
811
  """
@@ -820,10 +856,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
820
856
  """Delete an attachment and its metadata with path traversal protection.
821
857
 
822
858
  Args:
859
+ ----
823
860
  ticket_id: Ticket identifier
824
861
  attachment_id: Attachment identifier
825
862
 
826
863
  Returns:
864
+ -------
827
865
  True if deleted, False if not found
828
866
 
829
867
  """
@@ -861,6 +899,347 @@ class AITrackdownAdapter(BaseAdapter[Task]):
861
899
 
862
900
  return deleted
863
901
 
902
+ async def update_epic(self, epic_id: str, updates: dict[str, Any]) -> Epic | None:
903
+ """Update an epic (project) in AITrackdown.
904
+
905
+ Args:
906
+ ----
907
+ epic_id: Epic identifier (filename without .json)
908
+ updates: Dictionary of fields to update. Supported fields:
909
+ - title: Epic title
910
+ - description: Epic description
911
+ - state: TicketState value
912
+ - priority: Priority value
913
+ - tags: List of tags
914
+ - target_date: Target completion date
915
+ - metadata: User metadata dictionary
916
+
917
+ Returns:
918
+ -------
919
+ Updated Epic object or None if epic not found
920
+
921
+ Raises:
922
+ ------
923
+ ValueError: If epic_id is invalid or epic not found
924
+
925
+ Note:
926
+ ----
927
+ AITrackdown stores epics as JSON files in {storage_path}/tickets/
928
+ Updates are applied as partial updates (only specified fields changed)
929
+
930
+ """
931
+ # Validate epic_id
932
+ if not epic_id:
933
+ raise ValueError("epic_id is required")
934
+
935
+ # Read existing epic
936
+ existing = await self.read(epic_id)
937
+ if not existing:
938
+ logger.warning("Epic %s not found for update", epic_id)
939
+ return None
940
+
941
+ # Ensure it's an epic, not a task
942
+ if not isinstance(existing, Epic):
943
+ logger.warning("Ticket %s is not an epic", epic_id)
944
+ return None
945
+
946
+ # Apply updates to the existing epic
947
+ for key, value in updates.items():
948
+ if hasattr(existing, key) and value is not None:
949
+ setattr(existing, key, value)
950
+
951
+ # Update timestamp
952
+ existing.updated_at = datetime.now()
953
+
954
+ # Write back to file
955
+ ai_ticket = self._epic_to_ai_ticket(existing)
956
+ self._write_ticket_file(epic_id, ai_ticket)
957
+
958
+ logger.info("Updated epic %s with fields: %s", epic_id, list(updates.keys()))
959
+ return existing
960
+
961
+ async def list_labels(self, limit: int = 100) -> builtins.list[dict[str, Any]]:
962
+ """List all tags (labels) used across tickets.
963
+
964
+ Args:
965
+ ----
966
+ limit: Maximum number of labels to return (default: 100)
967
+
968
+ Returns:
969
+ -------
970
+ List of label dictionaries sorted by usage count (descending).
971
+ Each dictionary contains:
972
+ - id: Tag name (same as name in AITrackdown)
973
+ - name: Tag name
974
+ - count: Number of tickets using this tag
975
+
976
+ Note:
977
+ ----
978
+ AITrackdown uses 'tags' terminology. This method scans
979
+ all task and epic files to extract unique tags.
980
+
981
+ """
982
+ # Initialize tag counter
983
+ tag_counts: dict[str, int] = {}
984
+
985
+ # Scan all ticket JSON files
986
+ if self.tickets_dir.exists():
987
+ for ticket_file in self.tickets_dir.glob("*.json"):
988
+ try:
989
+ with open(ticket_file) as f:
990
+ ticket_data = json.load(f)
991
+ tags = ticket_data.get("tags", [])
992
+ for tag in tags:
993
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
994
+ except (json.JSONDecodeError, OSError) as e:
995
+ logger.warning("Failed to read ticket file %s: %s", ticket_file, e)
996
+ continue
997
+
998
+ # Sort by usage count (descending)
999
+ sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
1000
+
1001
+ # Return top N tags with standardized format
1002
+ return [
1003
+ {"id": tag, "name": tag, "count": count}
1004
+ for tag, count in sorted_tags[:limit]
1005
+ ]
1006
+
1007
+ async def create_issue_label(
1008
+ self, name: str, color: str | None = None
1009
+ ) -> dict[str, Any]:
1010
+ """Create/register a label (tag) in AITrackdown.
1011
+
1012
+ Args:
1013
+ ----
1014
+ name: Label name (alphanumeric, hyphens, underscores allowed)
1015
+ color: Optional color (not used in file-based storage)
1016
+
1017
+ Returns:
1018
+ -------
1019
+ Label dictionary with:
1020
+ - id: Label name
1021
+ - name: Label name
1022
+ - color: Color value (if provided)
1023
+ - created: True (always, as tags are created on use)
1024
+
1025
+ Raises:
1026
+ ------
1027
+ ValueError: If label name is invalid
1028
+
1029
+ Note:
1030
+ ----
1031
+ AITrackdown creates tags implicitly when used on tickets.
1032
+ This method validates the tag name and returns success.
1033
+ Tags are stored as arrays in ticket JSON files.
1034
+
1035
+ """
1036
+ # Validate tag name
1037
+ if not name:
1038
+ raise ValueError("Label name cannot be empty")
1039
+
1040
+ # Check for valid characters (alphanumeric, hyphens, underscores, spaces)
1041
+ import re
1042
+
1043
+ if not re.match(r"^[a-zA-Z0-9_\- ]+$", name):
1044
+ raise ValueError(
1045
+ "Label name must contain only alphanumeric characters, hyphens, underscores, or spaces"
1046
+ )
1047
+
1048
+ # Return success response
1049
+ logger.info("Label '%s' registered (created implicitly on use)", name)
1050
+ return {
1051
+ "id": name,
1052
+ "name": name,
1053
+ "color": color,
1054
+ "created": True,
1055
+ }
1056
+
1057
+ async def list_project_labels(
1058
+ self, epic_id: str, limit: int = 100
1059
+ ) -> builtins.list[dict[str, Any]]:
1060
+ """List labels (tags) used in a specific epic and its tasks.
1061
+
1062
+ Args:
1063
+ ----
1064
+ epic_id: Epic identifier
1065
+ limit: Maximum number of labels to return (default: 100)
1066
+
1067
+ Returns:
1068
+ -------
1069
+ List of label dictionaries used in the epic, sorted by usage count.
1070
+ Each dictionary contains:
1071
+ - id: Tag name
1072
+ - name: Tag name
1073
+ - count: Number of tickets using this tag within the epic
1074
+
1075
+ Raises:
1076
+ ------
1077
+ ValueError: If epic not found
1078
+
1079
+ Note:
1080
+ ----
1081
+ Scans the epic and all tasks with parent_epic == epic_id.
1082
+
1083
+ """
1084
+ # Validate epic exists
1085
+ epic = await self.get_epic(epic_id)
1086
+ if not epic:
1087
+ raise ValueError(f"Epic {epic_id} not found")
1088
+
1089
+ # Initialize tag counter
1090
+ tag_counts: dict[str, int] = {}
1091
+
1092
+ # Add tags from the epic itself
1093
+ if epic.tags:
1094
+ for tag in epic.tags:
1095
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
1096
+
1097
+ # Find all tasks with parent_epic == epic_id
1098
+ all_tasks = await self.list_issues_by_epic(epic_id)
1099
+ for task in all_tasks:
1100
+ if task.tags:
1101
+ for tag in task.tags:
1102
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
1103
+
1104
+ # Sort by usage count (descending)
1105
+ sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
1106
+
1107
+ # Return top N tags
1108
+ return [
1109
+ {"id": tag, "name": tag, "count": count}
1110
+ for tag, count in sorted_tags[:limit]
1111
+ ]
1112
+
1113
+ async def list_cycles(self, limit: int = 50) -> builtins.list[dict[str, Any]]:
1114
+ """List cycles (sprints) - Not supported in file-based AITrackdown.
1115
+
1116
+ Args:
1117
+ ----
1118
+ limit: Maximum number of cycles to return (unused)
1119
+
1120
+ Returns:
1121
+ -------
1122
+ Empty list (cycles not supported)
1123
+
1124
+ Note:
1125
+ ----
1126
+ AITrackdown is a simple file-based system without
1127
+ cycle/sprint management. Returns empty list.
1128
+
1129
+ """
1130
+ logger.info("list_cycles called but cycles not supported in AITrackdown")
1131
+ return []
1132
+
1133
+ async def get_issue_status(self, ticket_id: str) -> dict[str, Any] | None:
1134
+ """Get status details for a ticket.
1135
+
1136
+ Args:
1137
+ ----
1138
+ ticket_id: Ticket identifier
1139
+
1140
+ Returns:
1141
+ -------
1142
+ Status dictionary with:
1143
+ - id: Ticket ID
1144
+ - state: Current state
1145
+ - priority: Current priority
1146
+ - updated_at: Last update timestamp
1147
+ - created_at: Creation timestamp
1148
+ - title: Ticket title
1149
+ - assignee: Assignee (if Task, None for Epic)
1150
+ Returns None if ticket not found
1151
+
1152
+ Raises:
1153
+ ------
1154
+ ValueError: If ticket_id is invalid
1155
+
1156
+ """
1157
+ if not ticket_id:
1158
+ raise ValueError("ticket_id is required")
1159
+
1160
+ # Read ticket
1161
+ ticket = await self.read(ticket_id)
1162
+ if not ticket:
1163
+ logger.warning("Ticket %s not found", ticket_id)
1164
+ return None
1165
+
1166
+ # Return comprehensive status object
1167
+ status = {
1168
+ "id": ticket.id,
1169
+ "state": ticket.state,
1170
+ "priority": ticket.priority,
1171
+ "updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
1172
+ "created_at": ticket.created_at.isoformat() if ticket.created_at else None,
1173
+ "title": ticket.title,
1174
+ }
1175
+
1176
+ # Add assignee only if ticket is a Task (Epic doesn't have assignee)
1177
+ if hasattr(ticket, "assignee"):
1178
+ status["assignee"] = ticket.assignee
1179
+
1180
+ return status
1181
+
1182
+ async def list_issue_statuses(self) -> builtins.list[dict[str, Any]]:
1183
+ """List available ticket statuses.
1184
+
1185
+ Returns:
1186
+ -------
1187
+ List of status dictionaries with:
1188
+ - id: State identifier
1189
+ - name: Human-readable state name
1190
+ - description: State description
1191
+
1192
+ Note:
1193
+ ----
1194
+ AITrackdown uses standard TicketState enum values:
1195
+ open, in_progress, ready, tested, done, closed, waiting, blocked
1196
+
1197
+ """
1198
+ # Return hardcoded list of TicketState values
1199
+ statuses = [
1200
+ {
1201
+ "id": "open",
1202
+ "name": "Open",
1203
+ "description": "Ticket is created and ready to be worked on",
1204
+ },
1205
+ {
1206
+ "id": "in_progress",
1207
+ "name": "In Progress",
1208
+ "description": "Ticket is actively being worked on",
1209
+ },
1210
+ {
1211
+ "id": "ready",
1212
+ "name": "Ready",
1213
+ "description": "Ticket is ready for review or testing",
1214
+ },
1215
+ {
1216
+ "id": "tested",
1217
+ "name": "Tested",
1218
+ "description": "Ticket has been tested and verified",
1219
+ },
1220
+ {
1221
+ "id": "done",
1222
+ "name": "Done",
1223
+ "description": "Ticket work is completed",
1224
+ },
1225
+ {
1226
+ "id": "closed",
1227
+ "name": "Closed",
1228
+ "description": "Ticket is closed and archived",
1229
+ },
1230
+ {
1231
+ "id": "waiting",
1232
+ "name": "Waiting",
1233
+ "description": "Ticket is waiting for external dependency",
1234
+ },
1235
+ {
1236
+ "id": "blocked",
1237
+ "name": "Blocked",
1238
+ "description": "Ticket is blocked by an issue or dependency",
1239
+ },
1240
+ ]
1241
+ return statuses
1242
+
864
1243
 
865
1244
  # Register the adapter
866
1245
  AdapterRegistry.register("aitrackdown", AITrackdownAdapter)