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