mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +66 -49
  5. mcp_ticketer/adapters/github.py +192 -125
  6. mcp_ticketer/adapters/hybrid.py +99 -53
  7. mcp_ticketer/adapters/jira.py +161 -151
  8. mcp_ticketer/adapters/linear.py +396 -246
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +15 -16
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +283 -298
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +11 -13
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +121 -66
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +46 -39
  21. mcp_ticketer/core/config.py +128 -92
  22. mcp_ticketer/core/env_discovery.py +69 -37
  23. mcp_ticketer/core/http_client.py +57 -40
  24. mcp_ticketer/core/mappers.py +98 -54
  25. mcp_ticketer/core/models.py +38 -24
  26. mcp_ticketer/core/project_config.py +145 -80
  27. mcp_ticketer/core/registry.py +16 -16
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +199 -145
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +30 -26
  33. mcp_ticketer/queue/queue.py +147 -85
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +55 -40
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,32 @@
1
1
  """Linear adapter implementation using native GraphQL API with full feature support."""
2
2
 
3
- import os
4
3
  import asyncio
5
- from typing import List, Optional, Dict, Any, Union
6
- from datetime import datetime, date
4
+ import builtins
5
+ import os
6
+ from datetime import date, datetime
7
7
  from enum import Enum
8
+ from typing import Any, Optional, Union
8
9
 
9
- from gql import gql, Client
10
- from gql.transport.httpx import HTTPXAsyncTransport
10
+ from gql import Client, gql
11
11
  from gql.transport.exceptions import TransportQueryError
12
- import httpx
12
+ from gql.transport.httpx import HTTPXAsyncTransport
13
13
 
14
14
  from ..core.adapter import BaseAdapter
15
- from ..core.models import Epic, Task, Comment, SearchQuery, TicketState, Priority, TicketType
15
+ from ..core.models import (
16
+ Comment,
17
+ Epic,
18
+ Priority,
19
+ SearchQuery,
20
+ Task,
21
+ TicketState,
22
+ TicketType,
23
+ )
16
24
  from ..core.registry import AdapterRegistry
17
25
 
18
26
 
19
27
  class LinearStateType(str, Enum):
20
28
  """Linear workflow state types."""
29
+
21
30
  BACKLOG = "backlog"
22
31
  UNSTARTED = "unstarted"
23
32
  STARTED = "started"
@@ -38,9 +47,9 @@ class LinearPriorityMapping:
38
47
  FROM_LINEAR = {
39
48
  0: Priority.CRITICAL, # Urgent
40
49
  1: Priority.CRITICAL, # High
41
- 2: Priority.HIGH, # Medium
42
- 3: Priority.MEDIUM, # Low
43
- 4: Priority.LOW, # No priority
50
+ 2: Priority.HIGH, # Medium
51
+ 3: Priority.MEDIUM, # Low
52
+ 4: Priority.LOW, # No priority
44
53
  }
45
54
 
46
55
 
@@ -242,35 +251,35 @@ ISSUE_FULL_FRAGMENT = """
242
251
 
243
252
  # Combine all fragments
244
253
  ALL_FRAGMENTS = (
245
- USER_FRAGMENT +
246
- WORKFLOW_STATE_FRAGMENT +
247
- TEAM_FRAGMENT +
248
- CYCLE_FRAGMENT +
249
- PROJECT_FRAGMENT +
250
- LABEL_FRAGMENT +
251
- ATTACHMENT_FRAGMENT +
252
- COMMENT_FRAGMENT +
253
- ISSUE_COMPACT_FRAGMENT +
254
- ISSUE_FULL_FRAGMENT
254
+ USER_FRAGMENT
255
+ + WORKFLOW_STATE_FRAGMENT
256
+ + TEAM_FRAGMENT
257
+ + CYCLE_FRAGMENT
258
+ + PROJECT_FRAGMENT
259
+ + LABEL_FRAGMENT
260
+ + ATTACHMENT_FRAGMENT
261
+ + COMMENT_FRAGMENT
262
+ + ISSUE_COMPACT_FRAGMENT
263
+ + ISSUE_FULL_FRAGMENT
255
264
  )
256
265
 
257
266
  # Fragments needed for issue list/search (without comments)
258
267
  ISSUE_LIST_FRAGMENTS = (
259
- USER_FRAGMENT +
260
- WORKFLOW_STATE_FRAGMENT +
261
- TEAM_FRAGMENT +
262
- CYCLE_FRAGMENT +
263
- PROJECT_FRAGMENT +
264
- LABEL_FRAGMENT +
265
- ATTACHMENT_FRAGMENT +
266
- ISSUE_COMPACT_FRAGMENT
268
+ USER_FRAGMENT
269
+ + WORKFLOW_STATE_FRAGMENT
270
+ + TEAM_FRAGMENT
271
+ + CYCLE_FRAGMENT
272
+ + PROJECT_FRAGMENT
273
+ + LABEL_FRAGMENT
274
+ + ATTACHMENT_FRAGMENT
275
+ + ISSUE_COMPACT_FRAGMENT
267
276
  )
268
277
 
269
278
 
270
279
  class LinearAdapter(BaseAdapter[Task]):
271
280
  """Adapter for Linear issue tracking system using native GraphQL API."""
272
281
 
273
- def __init__(self, config: Dict[str, Any]):
282
+ def __init__(self, config: dict[str, Any]):
274
283
  """Initialize Linear adapter.
275
284
 
276
285
  Args:
@@ -282,13 +291,16 @@ class LinearAdapter(BaseAdapter[Task]):
282
291
  - api_url: Optional Linear API URL
283
292
 
284
293
  Note: Either team_key or team_id is required. If both are provided, team_id takes precedence.
294
+
285
295
  """
286
296
  super().__init__(config)
287
297
 
288
298
  # Get API key from config or environment
289
299
  self.api_key = config.get("api_key") or os.getenv("LINEAR_API_KEY")
290
300
  if not self.api_key:
291
- raise ValueError("Linear API key required (config.api_key or LINEAR_API_KEY env var)")
301
+ raise ValueError(
302
+ "Linear API key required (config.api_key or LINEAR_API_KEY env var)"
303
+ )
292
304
 
293
305
  self.workspace = config.get("workspace") # Optional, for documentation
294
306
 
@@ -304,9 +316,9 @@ class LinearAdapter(BaseAdapter[Task]):
304
316
 
305
317
  # Caches for frequently used data
306
318
  self._team_id: Optional[str] = None
307
- self._workflow_states: Optional[Dict[str, Dict[str, Any]]] = None
308
- self._labels: Optional[Dict[str, str]] = None # name -> id
309
- self._users: Optional[Dict[str, str]] = None # email -> id
319
+ self._workflow_states: Optional[dict[str, dict[str, Any]]] = None
320
+ self._labels: Optional[dict[str, str]] = None # name -> id
321
+ self._users: Optional[dict[str, str]] = None # email -> id
310
322
 
311
323
  # Initialize state mapping
312
324
  self._state_mapping = self._get_state_mapping()
@@ -323,6 +335,7 @@ class LinearAdapter(BaseAdapter[Task]):
323
335
 
324
336
  Returns:
325
337
  Client: Fresh GraphQL client instance
338
+
326
339
  """
327
340
  transport = HTTPXAsyncTransport(
328
341
  url=self.api_url,
@@ -372,7 +385,8 @@ class LinearAdapter(BaseAdapter[Task]):
372
385
  # If team_id (UUID) is provided, use it directly (preferred)
373
386
  if self.team_id_config:
374
387
  # Validate that this team ID exists
375
- query = gql("""
388
+ query = gql(
389
+ """
376
390
  query GetTeamById($id: String!) {
377
391
  team(id: $id) {
378
392
  id
@@ -380,11 +394,14 @@ class LinearAdapter(BaseAdapter[Task]):
380
394
  key
381
395
  }
382
396
  }
383
- """)
397
+ """
398
+ )
384
399
 
385
400
  client = self._create_client()
386
401
  async with client as session:
387
- result = await session.execute(query, variable_values={"id": self.team_id_config})
402
+ result = await session.execute(
403
+ query, variable_values={"id": self.team_id_config}
404
+ )
388
405
 
389
406
  if not result.get("team"):
390
407
  raise ValueError(f"Team with ID '{self.team_id_config}' not found")
@@ -392,7 +409,8 @@ class LinearAdapter(BaseAdapter[Task]):
392
409
  return result["team"]["id"]
393
410
 
394
411
  # Otherwise, fetch team ID by key
395
- query = gql("""
412
+ query = gql(
413
+ """
396
414
  query GetTeamByKey($key: String!) {
397
415
  teams(filter: { key: { eq: $key } }) {
398
416
  nodes {
@@ -402,20 +420,26 @@ class LinearAdapter(BaseAdapter[Task]):
402
420
  }
403
421
  }
404
422
  }
405
- """)
423
+ """
424
+ )
406
425
 
407
426
  client = self._create_client()
408
427
  async with client as session:
409
- result = await session.execute(query, variable_values={"key": self.team_key})
428
+ result = await session.execute(
429
+ query, variable_values={"key": self.team_key}
430
+ )
410
431
 
411
432
  if not result["teams"]["nodes"]:
412
433
  raise ValueError(f"Team with key '{self.team_key}' not found")
413
434
 
414
435
  return result["teams"]["nodes"][0]["id"]
415
436
 
416
- async def _fetch_workflow_states_data(self, team_id: str) -> Dict[str, Dict[str, Any]]:
437
+ async def _fetch_workflow_states_data(
438
+ self, team_id: str
439
+ ) -> dict[str, dict[str, Any]]:
417
440
  """Fetch workflow states data."""
418
- query = gql("""
441
+ query = gql(
442
+ """
419
443
  query WorkflowStates($teamId: ID!) {
420
444
  workflowStates(filter: { team: { id: { eq: $teamId } } }) {
421
445
  nodes {
@@ -427,7 +451,8 @@ class LinearAdapter(BaseAdapter[Task]):
427
451
  }
428
452
  }
429
453
  }
430
- """)
454
+ """
455
+ )
431
456
 
432
457
  client = self._create_client()
433
458
  async with client as session:
@@ -443,9 +468,10 @@ class LinearAdapter(BaseAdapter[Task]):
443
468
 
444
469
  return workflow_states
445
470
 
446
- async def _fetch_labels_data(self, team_id: str) -> Dict[str, str]:
471
+ async def _fetch_labels_data(self, team_id: str) -> dict[str, str]:
447
472
  """Fetch labels data."""
448
- query = gql("""
473
+ query = gql(
474
+ """
449
475
  query GetLabels($teamId: ID!) {
450
476
  issueLabels(filter: { team: { id: { eq: $teamId } } }) {
451
477
  nodes {
@@ -454,7 +480,8 @@ class LinearAdapter(BaseAdapter[Task]):
454
480
  }
455
481
  }
456
482
  }
457
- """)
483
+ """
484
+ )
458
485
 
459
486
  client = self._create_client()
460
487
  async with client as session:
@@ -472,7 +499,7 @@ class LinearAdapter(BaseAdapter[Task]):
472
499
  await self._ensure_initialized()
473
500
  return self._team_id
474
501
 
475
- async def _get_workflow_states(self) -> Dict[str, Dict[str, Any]]:
502
+ async def _get_workflow_states(self) -> dict[str, dict[str, Any]]:
476
503
  """Get cached workflow states from Linear."""
477
504
  await self._ensure_initialized()
478
505
  return self._workflow_states
@@ -487,7 +514,8 @@ class LinearAdapter(BaseAdapter[Task]):
487
514
 
488
515
  # Try to find existing label (may have been added since initialization)
489
516
  team_id = self._team_id
490
- search_query = gql("""
517
+ search_query = gql(
518
+ """
491
519
  query GetLabel($name: String!, $teamId: ID!) {
492
520
  issueLabels(filter: { name: { eq: $name }, team: { id: { eq: $teamId } } }) {
493
521
  nodes {
@@ -496,13 +524,13 @@ class LinearAdapter(BaseAdapter[Task]):
496
524
  }
497
525
  }
498
526
  }
499
- """)
527
+ """
528
+ )
500
529
 
501
530
  client = self._create_client()
502
531
  async with client as session:
503
532
  result = await session.execute(
504
- search_query,
505
- variable_values={"name": name, "teamId": team_id}
533
+ search_query, variable_values={"name": name, "teamId": team_id}
506
534
  )
507
535
 
508
536
  if result["issueLabels"]["nodes"]:
@@ -511,7 +539,8 @@ class LinearAdapter(BaseAdapter[Task]):
511
539
  return label_id
512
540
 
513
541
  # Create new label
514
- create_query = gql("""
542
+ create_query = gql(
543
+ """
515
544
  mutation CreateLabel($input: IssueLabelCreateInput!) {
516
545
  issueLabelCreate(input: $input) {
517
546
  issueLabel {
@@ -520,7 +549,8 @@ class LinearAdapter(BaseAdapter[Task]):
520
549
  }
521
550
  }
522
551
  }
523
- """)
552
+ """
553
+ )
524
554
 
525
555
  label_input = {
526
556
  "name": name,
@@ -532,8 +562,7 @@ class LinearAdapter(BaseAdapter[Task]):
532
562
  client = self._create_client()
533
563
  async with client as session:
534
564
  result = await session.execute(
535
- create_query,
536
- variable_values={"input": label_input}
565
+ create_query, variable_values={"input": label_input}
537
566
  )
538
567
 
539
568
  label_id = result["issueLabelCreate"]["issueLabel"]["id"]
@@ -548,7 +577,8 @@ class LinearAdapter(BaseAdapter[Task]):
548
577
  if email in self._users:
549
578
  return self._users[email]
550
579
 
551
- query = gql("""
580
+ query = gql(
581
+ """
552
582
  query GetUser($email: String!) {
553
583
  users(filter: { email: { eq: $email } }) {
554
584
  nodes {
@@ -557,7 +587,8 @@ class LinearAdapter(BaseAdapter[Task]):
557
587
  }
558
588
  }
559
589
  }
560
- """)
590
+ """
591
+ )
561
592
 
562
593
  client = self._create_client()
563
594
  async with client as session:
@@ -575,14 +606,21 @@ class LinearAdapter(BaseAdapter[Task]):
575
606
 
576
607
  Returns:
577
608
  (is_valid, error_message) - Tuple of validation result and error message
609
+
578
610
  """
579
611
  if not self.api_key:
580
- return False, "LINEAR_API_KEY is required but not found. Set it in .env.local or environment."
612
+ return (
613
+ False,
614
+ "LINEAR_API_KEY is required but not found. Set it in .env.local or environment.",
615
+ )
581
616
  if not self.team_key and not self.team_id_config:
582
- return False, "Either Linear team_key or team_id is required in configuration. Set it in .mcp-ticketer/config.json"
617
+ return (
618
+ False,
619
+ "Either Linear team_key or team_id is required in configuration. Set it in .mcp-ticketer/config.json",
620
+ )
583
621
  return True, ""
584
622
 
585
- def _get_state_mapping(self) -> Dict[TicketState, str]:
623
+ def _get_state_mapping(self) -> dict[TicketState, str]:
586
624
  """Get mapping from universal states to Linear state types.
587
625
 
588
626
  Required by BaseAdapter abstract method.
@@ -605,7 +643,9 @@ class LinearAdapter(BaseAdapter[Task]):
605
643
  state = TicketState(state)
606
644
  return self._state_mapping.get(state, LinearStateType.BACKLOG)
607
645
 
608
- def _map_linear_state(self, state_data: Dict[str, Any], labels: List[str]) -> TicketState:
646
+ def _map_linear_state(
647
+ self, state_data: dict[str, Any], labels: list[str]
648
+ ) -> TicketState:
609
649
  """Map Linear state and labels to universal state."""
610
650
  state_type = state_data.get("type", "").lower()
611
651
 
@@ -630,7 +670,7 @@ class LinearAdapter(BaseAdapter[Task]):
630
670
  }
631
671
  return state_mapping.get(state_type, TicketState.OPEN)
632
672
 
633
- def _task_from_linear_issue(self, issue: Dict[str, Any]) -> Task:
673
+ def _task_from_linear_issue(self, issue: dict[str, Any]) -> Task:
634
674
  """Convert Linear issue to universal Task."""
635
675
  # Extract labels
636
676
  tags = []
@@ -639,7 +679,9 @@ class LinearAdapter(BaseAdapter[Task]):
639
679
 
640
680
  # Map priority
641
681
  linear_priority = issue.get("priority", 4)
642
- priority = LinearPriorityMapping.FROM_LINEAR.get(linear_priority, Priority.MEDIUM)
682
+ priority = LinearPriorityMapping.FROM_LINEAR.get(
683
+ linear_priority, Priority.MEDIUM
684
+ )
643
685
 
644
686
  # Map state
645
687
  state = self._map_linear_state(issue.get("state", {}), tags)
@@ -654,10 +696,20 @@ class LinearAdapter(BaseAdapter[Task]):
654
696
  "state_name": issue.get("state", {}).get("name"),
655
697
  "team_id": issue.get("team", {}).get("id"),
656
698
  "team_name": issue.get("team", {}).get("name"),
657
- "cycle_id": issue.get("cycle", {}).get("id") if issue.get("cycle") else None,
658
- "cycle_name": issue.get("cycle", {}).get("name") if issue.get("cycle") else None,
659
- "project_id": issue.get("project", {}).get("id") if issue.get("project") else None,
660
- "project_name": issue.get("project", {}).get("name") if issue.get("project") else None,
699
+ "cycle_id": (
700
+ issue.get("cycle", {}).get("id") if issue.get("cycle") else None
701
+ ),
702
+ "cycle_name": (
703
+ issue.get("cycle", {}).get("name") if issue.get("cycle") else None
704
+ ),
705
+ "project_id": (
706
+ issue.get("project", {}).get("id") if issue.get("project") else None
707
+ ),
708
+ "project_name": (
709
+ issue.get("project", {}).get("name")
710
+ if issue.get("project")
711
+ else None
712
+ ),
661
713
  "priority_label": issue.get("priorityLabel"),
662
714
  "estimate": issue.get("estimate"),
663
715
  "due_date": issue.get("dueDate"),
@@ -715,17 +767,27 @@ class LinearAdapter(BaseAdapter[Task]):
715
767
  ticket_type=ticket_type,
716
768
  parent_issue=parent_issue_id,
717
769
  parent_epic=parent_epic_id,
718
- assignee=issue.get("assignee", {}).get("email") if issue.get("assignee") else None,
770
+ assignee=(
771
+ issue.get("assignee", {}).get("email")
772
+ if issue.get("assignee")
773
+ else None
774
+ ),
719
775
  children=child_ids,
720
776
  estimated_hours=issue.get("estimate"),
721
- created_at=datetime.fromisoformat(issue["createdAt"].replace("Z", "+00:00"))
722
- if issue.get("createdAt") else None,
723
- updated_at=datetime.fromisoformat(issue["updatedAt"].replace("Z", "+00:00"))
724
- if issue.get("updatedAt") else None,
777
+ created_at=(
778
+ datetime.fromisoformat(issue["createdAt"].replace("Z", "+00:00"))
779
+ if issue.get("createdAt")
780
+ else None
781
+ ),
782
+ updated_at=(
783
+ datetime.fromisoformat(issue["updatedAt"].replace("Z", "+00:00"))
784
+ if issue.get("updatedAt")
785
+ else None
786
+ ),
725
787
  metadata=metadata,
726
788
  )
727
789
 
728
- def _epic_from_linear_project(self, project: Dict[str, Any]) -> Epic:
790
+ def _epic_from_linear_project(self, project: dict[str, Any]) -> Epic:
729
791
  """Convert Linear project to universal Epic."""
730
792
  # Map project state to ticket state
731
793
  project_state = project.get("state", "planned").lower()
@@ -764,10 +826,16 @@ class LinearAdapter(BaseAdapter[Task]):
764
826
  state=state,
765
827
  ticket_type=TicketType.EPIC,
766
828
  tags=[f"team:{team}" for team in teams],
767
- created_at=datetime.fromisoformat(project["createdAt"].replace("Z", "+00:00"))
768
- if project.get("createdAt") else None,
769
- updated_at=datetime.fromisoformat(project["updatedAt"].replace("Z", "+00:00"))
770
- if project.get("updatedAt") else None,
829
+ created_at=(
830
+ datetime.fromisoformat(project["createdAt"].replace("Z", "+00:00"))
831
+ if project.get("createdAt")
832
+ else None
833
+ ),
834
+ updated_at=(
835
+ datetime.fromisoformat(project["updatedAt"].replace("Z", "+00:00"))
836
+ if project.get("updatedAt")
837
+ else None
838
+ ),
771
839
  metadata=metadata,
772
840
  )
773
841
 
@@ -803,19 +871,33 @@ class LinearAdapter(BaseAdapter[Task]):
803
871
 
804
872
  # Set priority
805
873
  if ticket.priority:
806
- issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(ticket.priority, 3)
874
+ issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
875
+ ticket.priority, 3
876
+ )
807
877
 
808
878
  # Handle labels/tags
809
879
  if ticket.tags:
810
880
  label_ids = []
811
881
  for tag in ticket.tags:
812
882
  # Add special state labels if needed
813
- if ticket.state == TicketState.BLOCKED and "blocked" not in [t.lower() for t in ticket.tags]:
814
- label_ids.append(await self._get_or_create_label("blocked", "#FF0000"))
815
- elif ticket.state == TicketState.WAITING and "waiting" not in [t.lower() for t in ticket.tags]:
816
- label_ids.append(await self._get_or_create_label("waiting", "#FFA500"))
817
- elif ticket.state == TicketState.READY and "ready" not in [t.lower() for t in ticket.tags]:
818
- label_ids.append(await self._get_or_create_label("ready", "#00FF00"))
883
+ if ticket.state == TicketState.BLOCKED and "blocked" not in [
884
+ t.lower() for t in ticket.tags
885
+ ]:
886
+ label_ids.append(
887
+ await self._get_or_create_label("blocked", "#FF0000")
888
+ )
889
+ elif ticket.state == TicketState.WAITING and "waiting" not in [
890
+ t.lower() for t in ticket.tags
891
+ ]:
892
+ label_ids.append(
893
+ await self._get_or_create_label("waiting", "#FFA500")
894
+ )
895
+ elif ticket.state == TicketState.READY and "ready" not in [
896
+ t.lower() for t in ticket.tags
897
+ ]:
898
+ label_ids.append(
899
+ await self._get_or_create_label("ready", "#00FF00")
900
+ )
819
901
 
820
902
  label_id = await self._get_or_create_label(tag)
821
903
  label_ids.append(label_id)
@@ -835,18 +917,19 @@ class LinearAdapter(BaseAdapter[Task]):
835
917
  # Handle parent issue
836
918
  if ticket.parent_issue:
837
919
  # Get parent issue's Linear ID
838
- parent_query = gql("""
920
+ parent_query = gql(
921
+ """
839
922
  query GetIssue($identifier: String!) {
840
923
  issue(id: $identifier) {
841
924
  id
842
925
  }
843
926
  }
844
- """)
927
+ """
928
+ )
845
929
  client = self._create_client()
846
930
  async with client as session:
847
931
  parent_result = await session.execute(
848
- parent_query,
849
- variable_values={"identifier": ticket.parent_issue}
932
+ parent_query, variable_values={"identifier": ticket.parent_issue}
850
933
  )
851
934
  if parent_result.get("issue"):
852
935
  issue_input["parentId"] = parent_result["issue"]["id"]
@@ -864,7 +947,9 @@ class LinearAdapter(BaseAdapter[Task]):
864
947
  issue_input["cycleId"] = linear_meta["cycle_id"]
865
948
 
866
949
  # Create issue mutation with full fields
867
- create_query = gql(ALL_FRAGMENTS + """
950
+ create_query = gql(
951
+ ALL_FRAGMENTS
952
+ + """
868
953
  mutation CreateIssue($input: IssueCreateInput!) {
869
954
  issueCreate(input: $input) {
870
955
  success
@@ -873,13 +958,13 @@ class LinearAdapter(BaseAdapter[Task]):
873
958
  }
874
959
  }
875
960
  }
876
- """)
961
+ """
962
+ )
877
963
 
878
964
  client = self._create_client()
879
965
  async with client as session:
880
966
  result = await session.execute(
881
- create_query,
882
- variable_values={"input": issue_input}
967
+ create_query, variable_values={"input": issue_input}
883
968
  )
884
969
 
885
970
  if not result["issueCreate"]["success"]:
@@ -895,20 +980,22 @@ class LinearAdapter(BaseAdapter[Task]):
895
980
  if not is_valid:
896
981
  raise ValueError(error_message)
897
982
 
898
- query = gql(ALL_FRAGMENTS + """
983
+ query = gql(
984
+ ALL_FRAGMENTS
985
+ + """
899
986
  query GetIssue($identifier: String!) {
900
987
  issue(id: $identifier) {
901
988
  ...IssueFullFields
902
989
  }
903
990
  }
904
- """)
991
+ """
992
+ )
905
993
 
906
994
  try:
907
995
  client = self._create_client()
908
996
  async with client as session:
909
997
  result = await session.execute(
910
- query,
911
- variable_values={"identifier": ticket_id}
998
+ query, variable_values={"identifier": ticket_id}
912
999
  )
913
1000
 
914
1001
  if result.get("issue"):
@@ -919,7 +1006,7 @@ class LinearAdapter(BaseAdapter[Task]):
919
1006
 
920
1007
  return None
921
1008
 
922
- async def update(self, ticket_id: str, updates: Dict[str, Any]) -> Optional[Task]:
1009
+ async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[Task]:
923
1010
  """Update a Linear issue with comprehensive field support."""
924
1011
  # Validate credentials before attempting operation
925
1012
  is_valid, error_message = self.validate_credentials()
@@ -927,19 +1014,20 @@ class LinearAdapter(BaseAdapter[Task]):
927
1014
  raise ValueError(error_message)
928
1015
 
929
1016
  # First get the Linear internal ID
930
- query = gql("""
1017
+ query = gql(
1018
+ """
931
1019
  query GetIssueId($identifier: String!) {
932
1020
  issue(id: $identifier) {
933
1021
  id
934
1022
  }
935
1023
  }
936
- """)
1024
+ """
1025
+ )
937
1026
 
938
1027
  client = self._create_client()
939
1028
  async with client as session:
940
1029
  result = await session.execute(
941
- query,
942
- variable_values={"identifier": ticket_id}
1030
+ query, variable_values={"identifier": ticket_id}
943
1031
  )
944
1032
 
945
1033
  if not result.get("issue"):
@@ -1001,7 +1089,9 @@ class LinearAdapter(BaseAdapter[Task]):
1001
1089
  update_input["projectId"] = linear_meta["project_id"]
1002
1090
 
1003
1091
  # Update mutation
1004
- update_query = gql(ALL_FRAGMENTS + """
1092
+ update_query = gql(
1093
+ ALL_FRAGMENTS
1094
+ + """
1005
1095
  mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
1006
1096
  issueUpdate(id: $id, input: $input) {
1007
1097
  success
@@ -1010,13 +1100,13 @@ class LinearAdapter(BaseAdapter[Task]):
1010
1100
  }
1011
1101
  }
1012
1102
  }
1013
- """)
1103
+ """
1104
+ )
1014
1105
 
1015
1106
  client = self._create_client()
1016
1107
  async with client as session:
1017
1108
  result = await session.execute(
1018
- update_query,
1019
- variable_values={"id": linear_id, "input": update_input}
1109
+ update_query, variable_values={"id": linear_id, "input": update_input}
1020
1110
  )
1021
1111
 
1022
1112
  if result["issueUpdate"]["success"]:
@@ -1032,19 +1122,20 @@ class LinearAdapter(BaseAdapter[Task]):
1032
1122
  raise ValueError(error_message)
1033
1123
 
1034
1124
  # Get Linear ID
1035
- query = gql("""
1125
+ query = gql(
1126
+ """
1036
1127
  query GetIssueId($identifier: String!) {
1037
1128
  issue(id: $identifier) {
1038
1129
  id
1039
1130
  }
1040
1131
  }
1041
- """)
1132
+ """
1133
+ )
1042
1134
 
1043
1135
  client = self._create_client()
1044
1136
  async with client as session:
1045
1137
  result = await session.execute(
1046
- query,
1047
- variable_values={"identifier": ticket_id}
1138
+ query, variable_values={"identifier": ticket_id}
1048
1139
  )
1049
1140
 
1050
1141
  if not result.get("issue"):
@@ -1053,29 +1144,27 @@ class LinearAdapter(BaseAdapter[Task]):
1053
1144
  linear_id = result["issue"]["id"]
1054
1145
 
1055
1146
  # Archive mutation
1056
- archive_query = gql("""
1147
+ archive_query = gql(
1148
+ """
1057
1149
  mutation ArchiveIssue($id: String!) {
1058
1150
  issueArchive(id: $id) {
1059
1151
  success
1060
1152
  }
1061
1153
  }
1062
- """)
1154
+ """
1155
+ )
1063
1156
 
1064
1157
  client = self._create_client()
1065
1158
  async with client as session:
1066
1159
  result = await session.execute(
1067
- archive_query,
1068
- variable_values={"id": linear_id}
1160
+ archive_query, variable_values={"id": linear_id}
1069
1161
  )
1070
1162
 
1071
1163
  return result.get("issueArchive", {}).get("success", False)
1072
1164
 
1073
1165
  async def list(
1074
- self,
1075
- limit: int = 10,
1076
- offset: int = 0,
1077
- filters: Optional[Dict[str, Any]] = None
1078
- ) -> List[Task]:
1166
+ self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
1167
+ ) -> list[Task]:
1079
1168
  """List Linear issues with comprehensive filtering."""
1080
1169
  team_id = await self._ensure_team_id()
1081
1170
 
@@ -1137,10 +1226,16 @@ class LinearAdapter(BaseAdapter[Task]):
1137
1226
  issue_filter["dueDate"] = {"lte": filters["due_before"]}
1138
1227
 
1139
1228
  # Exclude archived issues by default
1140
- if not filters or "includeArchived" not in filters or not filters["includeArchived"]:
1229
+ if (
1230
+ not filters
1231
+ or "includeArchived" not in filters
1232
+ or not filters["includeArchived"]
1233
+ ):
1141
1234
  issue_filter["archivedAt"] = {"null": True}
1142
1235
 
1143
- query = gql(ISSUE_LIST_FRAGMENTS + """
1236
+ query = gql(
1237
+ ISSUE_LIST_FRAGMENTS
1238
+ + """
1144
1239
  query ListIssues($filter: IssueFilter, $first: Int!) {
1145
1240
  issues(
1146
1241
  filter: $filter
@@ -1156,7 +1251,8 @@ class LinearAdapter(BaseAdapter[Task]):
1156
1251
  }
1157
1252
  }
1158
1253
  }
1159
- """)
1254
+ """
1255
+ )
1160
1256
 
1161
1257
  client = self._create_client()
1162
1258
  async with client as session:
@@ -1167,7 +1263,7 @@ class LinearAdapter(BaseAdapter[Task]):
1167
1263
  "first": limit,
1168
1264
  # Note: Linear uses cursor-based pagination, not offset
1169
1265
  # For simplicity, we ignore offset here
1170
- }
1266
+ },
1171
1267
  )
1172
1268
 
1173
1269
  tasks = []
@@ -1176,7 +1272,7 @@ class LinearAdapter(BaseAdapter[Task]):
1176
1272
 
1177
1273
  return tasks
1178
1274
 
1179
- async def search(self, query: SearchQuery) -> List[Task]:
1275
+ async def search(self, query: SearchQuery) -> builtins.list[Task]:
1180
1276
  """Search Linear issues with advanced filtering and text search."""
1181
1277
  team_id = await self._ensure_team_id()
1182
1278
 
@@ -1219,7 +1315,9 @@ class LinearAdapter(BaseAdapter[Task]):
1219
1315
  # Exclude archived
1220
1316
  issue_filter["archivedAt"] = {"null": True}
1221
1317
 
1222
- search_query = gql(ISSUE_LIST_FRAGMENTS + """
1318
+ search_query = gql(
1319
+ ISSUE_LIST_FRAGMENTS
1320
+ + """
1223
1321
  query SearchIssues($filter: IssueFilter, $first: Int!) {
1224
1322
  issues(
1225
1323
  filter: $filter
@@ -1231,7 +1329,8 @@ class LinearAdapter(BaseAdapter[Task]):
1231
1329
  }
1232
1330
  }
1233
1331
  }
1234
- """)
1332
+ """
1333
+ )
1235
1334
 
1236
1335
  client = self._create_client()
1237
1336
  async with client as session:
@@ -1241,7 +1340,7 @@ class LinearAdapter(BaseAdapter[Task]):
1241
1340
  "filter": issue_filter,
1242
1341
  "first": query.limit,
1243
1342
  # Note: Linear uses cursor-based pagination, not offset
1244
- }
1343
+ },
1245
1344
  )
1246
1345
 
1247
1346
  tasks = []
@@ -1251,9 +1350,7 @@ class LinearAdapter(BaseAdapter[Task]):
1251
1350
  return tasks
1252
1351
 
1253
1352
  async def transition_state(
1254
- self,
1255
- ticket_id: str,
1256
- target_state: TicketState
1353
+ self, ticket_id: str, target_state: TicketState
1257
1354
  ) -> Optional[Task]:
1258
1355
  """Transition Linear issue to new state with workflow validation."""
1259
1356
  # Validate transition
@@ -1266,19 +1363,20 @@ class LinearAdapter(BaseAdapter[Task]):
1266
1363
  async def add_comment(self, comment: Comment) -> Comment:
1267
1364
  """Add comment to a Linear issue."""
1268
1365
  # Get Linear issue ID
1269
- query = gql("""
1366
+ query = gql(
1367
+ """
1270
1368
  query GetIssueId($identifier: String!) {
1271
1369
  issue(id: $identifier) {
1272
1370
  id
1273
1371
  }
1274
1372
  }
1275
- """)
1373
+ """
1374
+ )
1276
1375
 
1277
1376
  client = self._create_client()
1278
1377
  async with client as session:
1279
1378
  result = await session.execute(
1280
- query,
1281
- variable_values={"identifier": comment.ticket_id}
1379
+ query, variable_values={"identifier": comment.ticket_id}
1282
1380
  )
1283
1381
 
1284
1382
  if not result.get("issue"):
@@ -1287,7 +1385,10 @@ class LinearAdapter(BaseAdapter[Task]):
1287
1385
  linear_id = result["issue"]["id"]
1288
1386
 
1289
1387
  # Create comment mutation (only include needed fragments)
1290
- create_comment_query = gql(USER_FRAGMENT + COMMENT_FRAGMENT + """
1388
+ create_comment_query = gql(
1389
+ USER_FRAGMENT
1390
+ + COMMENT_FRAGMENT
1391
+ + """
1291
1392
  mutation CreateComment($input: CommentCreateInput!) {
1292
1393
  commentCreate(input: $input) {
1293
1394
  success
@@ -1296,7 +1397,8 @@ class LinearAdapter(BaseAdapter[Task]):
1296
1397
  }
1297
1398
  }
1298
1399
  }
1299
- """)
1400
+ """
1401
+ )
1300
1402
 
1301
1403
  comment_input = {
1302
1404
  "issueId": linear_id,
@@ -1310,8 +1412,7 @@ class LinearAdapter(BaseAdapter[Task]):
1310
1412
  client = self._create_client()
1311
1413
  async with client as session:
1312
1414
  result = await session.execute(
1313
- create_comment_query,
1314
- variable_values={"input": comment_input}
1415
+ create_comment_query, variable_values={"input": comment_input}
1315
1416
  )
1316
1417
 
1317
1418
  if not result["commentCreate"]["success"]:
@@ -1322,25 +1423,35 @@ class LinearAdapter(BaseAdapter[Task]):
1322
1423
  return Comment(
1323
1424
  id=created_comment["id"],
1324
1425
  ticket_id=comment.ticket_id,
1325
- author=created_comment["user"]["email"] if created_comment.get("user") else None,
1426
+ author=(
1427
+ created_comment["user"]["email"]
1428
+ if created_comment.get("user")
1429
+ else None
1430
+ ),
1326
1431
  content=created_comment["body"],
1327
- created_at=datetime.fromisoformat(created_comment["createdAt"].replace("Z", "+00:00")),
1432
+ created_at=datetime.fromisoformat(
1433
+ created_comment["createdAt"].replace("Z", "+00:00")
1434
+ ),
1328
1435
  metadata={
1329
1436
  "linear": {
1330
1437
  "id": created_comment["id"],
1331
- "parent_id": created_comment.get("parent", {}).get("id") if created_comment.get("parent") else None,
1438
+ "parent_id": (
1439
+ created_comment.get("parent", {}).get("id")
1440
+ if created_comment.get("parent")
1441
+ else None
1442
+ ),
1332
1443
  }
1333
1444
  },
1334
1445
  )
1335
1446
 
1336
1447
  async def get_comments(
1337
- self,
1338
- ticket_id: str,
1339
- limit: int = 10,
1340
- offset: int = 0
1341
- ) -> List[Comment]:
1448
+ self, ticket_id: str, limit: int = 10, offset: int = 0
1449
+ ) -> builtins.list[Comment]:
1342
1450
  """Get comments for a Linear issue with pagination."""
1343
- query = gql(USER_FRAGMENT + COMMENT_FRAGMENT + """
1451
+ query = gql(
1452
+ USER_FRAGMENT
1453
+ + COMMENT_FRAGMENT
1454
+ + """
1344
1455
  query GetIssueComments($identifier: String!, $first: Int!) {
1345
1456
  issue(id: $identifier) {
1346
1457
  comments(first: $first, orderBy: createdAt) {
@@ -1350,7 +1461,8 @@ class LinearAdapter(BaseAdapter[Task]):
1350
1461
  }
1351
1462
  }
1352
1463
  }
1353
- """)
1464
+ """
1465
+ )
1354
1466
 
1355
1467
  try:
1356
1468
  client = self._create_client()
@@ -1361,7 +1473,7 @@ class LinearAdapter(BaseAdapter[Task]):
1361
1473
  "identifier": ticket_id,
1362
1474
  "first": limit,
1363
1475
  # Note: Linear uses cursor-based pagination
1364
- }
1476
+ },
1365
1477
  )
1366
1478
 
1367
1479
  if not result.get("issue"):
@@ -1369,19 +1481,31 @@ class LinearAdapter(BaseAdapter[Task]):
1369
1481
 
1370
1482
  comments = []
1371
1483
  for comment_data in result["issue"]["comments"]["nodes"]:
1372
- comments.append(Comment(
1373
- id=comment_data["id"],
1374
- ticket_id=ticket_id,
1375
- author=comment_data["user"]["email"] if comment_data.get("user") else None,
1376
- content=comment_data["body"],
1377
- created_at=datetime.fromisoformat(comment_data["createdAt"].replace("Z", "+00:00")),
1378
- metadata={
1379
- "linear": {
1380
- "id": comment_data["id"],
1381
- "parent_id": comment_data.get("parent", {}).get("id") if comment_data.get("parent") else None,
1382
- }
1383
- },
1384
- ))
1484
+ comments.append(
1485
+ Comment(
1486
+ id=comment_data["id"],
1487
+ ticket_id=ticket_id,
1488
+ author=(
1489
+ comment_data["user"]["email"]
1490
+ if comment_data.get("user")
1491
+ else None
1492
+ ),
1493
+ content=comment_data["body"],
1494
+ created_at=datetime.fromisoformat(
1495
+ comment_data["createdAt"].replace("Z", "+00:00")
1496
+ ),
1497
+ metadata={
1498
+ "linear": {
1499
+ "id": comment_data["id"],
1500
+ "parent_id": (
1501
+ comment_data.get("parent", {}).get("id")
1502
+ if comment_data.get("parent")
1503
+ else None
1504
+ ),
1505
+ }
1506
+ },
1507
+ )
1508
+ )
1385
1509
 
1386
1510
  return comments
1387
1511
  except TransportQueryError:
@@ -1391,7 +1515,8 @@ class LinearAdapter(BaseAdapter[Task]):
1391
1515
  """Create a Linear project."""
1392
1516
  team_id = await self._ensure_team_id()
1393
1517
 
1394
- create_query = gql("""
1518
+ create_query = gql(
1519
+ """
1395
1520
  mutation CreateProject($input: ProjectCreateInput!) {
1396
1521
  projectCreate(input: $input) {
1397
1522
  success
@@ -1401,7 +1526,8 @@ class LinearAdapter(BaseAdapter[Task]):
1401
1526
  }
1402
1527
  }
1403
1528
  }
1404
- """)
1529
+ """
1530
+ )
1405
1531
 
1406
1532
  project_input = {
1407
1533
  "name": name,
@@ -1413,8 +1539,7 @@ class LinearAdapter(BaseAdapter[Task]):
1413
1539
  client = self._create_client()
1414
1540
  async with client as session:
1415
1541
  result = await session.execute(
1416
- create_query,
1417
- variable_values={"input": project_input}
1542
+ create_query, variable_values={"input": project_input}
1418
1543
  )
1419
1544
 
1420
1545
  if not result["projectCreate"]["success"]:
@@ -1422,7 +1547,7 @@ class LinearAdapter(BaseAdapter[Task]):
1422
1547
 
1423
1548
  return result["projectCreate"]["project"]["id"]
1424
1549
 
1425
- async def get_cycles(self, active_only: bool = True) -> List[Dict[str, Any]]:
1550
+ async def get_cycles(self, active_only: bool = True) -> builtins.list[dict[str, Any]]:
1426
1551
  """Get Linear cycles (sprints) for the team."""
1427
1552
  team_id = await self._ensure_team_id()
1428
1553
 
@@ -1430,7 +1555,8 @@ class LinearAdapter(BaseAdapter[Task]):
1430
1555
  if active_only:
1431
1556
  cycle_filter["isActive"] = {"eq": True}
1432
1557
 
1433
- query = gql("""
1558
+ query = gql(
1559
+ """
1434
1560
  query GetCycles($filter: CycleFilter) {
1435
1561
  cycles(filter: $filter, orderBy: createdAt) {
1436
1562
  nodes {
@@ -1450,43 +1576,49 @@ class LinearAdapter(BaseAdapter[Task]):
1450
1576
  }
1451
1577
  }
1452
1578
  }
1453
- """)
1579
+ """
1580
+ )
1454
1581
 
1455
1582
  client = self._create_client()
1456
1583
  async with client as session:
1457
1584
  result = await session.execute(
1458
- query,
1459
- variable_values={"filter": cycle_filter}
1585
+ query, variable_values={"filter": cycle_filter}
1460
1586
  )
1461
1587
 
1462
1588
  return result["cycles"]["nodes"]
1463
1589
 
1464
1590
  async def add_to_cycle(self, ticket_id: str, cycle_id: str) -> bool:
1465
1591
  """Add an issue to a cycle."""
1466
- return await self.update(
1467
- ticket_id,
1468
- {"metadata": {"linear": {"cycle_id": cycle_id}}}
1469
- ) is not None
1592
+ return (
1593
+ await self.update(
1594
+ ticket_id, {"metadata": {"linear": {"cycle_id": cycle_id}}}
1595
+ )
1596
+ is not None
1597
+ )
1470
1598
 
1471
1599
  async def set_due_date(self, ticket_id: str, due_date: Union[str, date]) -> bool:
1472
1600
  """Set due date for an issue."""
1473
1601
  if isinstance(due_date, date):
1474
1602
  due_date = due_date.isoformat()
1475
1603
 
1476
- return await self.update(
1477
- ticket_id,
1478
- {"metadata": {"linear": {"due_date": due_date}}}
1479
- ) is not None
1604
+ return (
1605
+ await self.update(
1606
+ ticket_id, {"metadata": {"linear": {"due_date": due_date}}}
1607
+ )
1608
+ is not None
1609
+ )
1480
1610
 
1481
1611
  async def add_reaction(self, comment_id: str, emoji: str) -> bool:
1482
1612
  """Add reaction to a comment."""
1483
- create_query = gql("""
1613
+ create_query = gql(
1614
+ """
1484
1615
  mutation CreateReaction($input: ReactionCreateInput!) {
1485
1616
  reactionCreate(input: $input) {
1486
1617
  success
1487
1618
  }
1488
1619
  }
1489
- """)
1620
+ """
1621
+ )
1490
1622
 
1491
1623
  client = self._create_client()
1492
1624
  async with client as session:
@@ -1497,7 +1629,7 @@ class LinearAdapter(BaseAdapter[Task]):
1497
1629
  "commentId": comment_id,
1498
1630
  "emoji": emoji,
1499
1631
  }
1500
- }
1632
+ },
1501
1633
  )
1502
1634
 
1503
1635
  return result.get("reactionCreate", {}).get("success", False)
@@ -1507,7 +1639,7 @@ class LinearAdapter(BaseAdapter[Task]):
1507
1639
  ticket_id: str,
1508
1640
  pr_url: str,
1509
1641
  pr_number: Optional[int] = None,
1510
- ) -> Dict[str, Any]:
1642
+ ) -> dict[str, Any]:
1511
1643
  """Link a Linear issue to a GitHub pull request.
1512
1644
 
1513
1645
  Args:
@@ -1517,9 +1649,11 @@ class LinearAdapter(BaseAdapter[Task]):
1517
1649
 
1518
1650
  Returns:
1519
1651
  Dictionary with link status and details
1652
+
1520
1653
  """
1521
1654
  # Parse PR URL to extract details
1522
1655
  import re
1656
+
1523
1657
  pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
1524
1658
  match = re.search(pr_pattern, pr_url)
1525
1659
 
@@ -1531,7 +1665,8 @@ class LinearAdapter(BaseAdapter[Task]):
1531
1665
  pr_number = int(extracted_pr_number)
1532
1666
 
1533
1667
  # Create an attachment to link the PR
1534
- create_query = gql("""
1668
+ create_query = gql(
1669
+ """
1535
1670
  mutation CreateAttachment($input: AttachmentCreateInput!) {
1536
1671
  attachmentCreate(input: $input) {
1537
1672
  attachment {
@@ -1544,7 +1679,8 @@ class LinearAdapter(BaseAdapter[Task]):
1544
1679
  success
1545
1680
  }
1546
1681
  }
1547
- """)
1682
+ """
1683
+ )
1548
1684
 
1549
1685
  # Get the issue ID from the identifier
1550
1686
  issue = await self.read(ticket_id)
@@ -1563,15 +1699,14 @@ class LinearAdapter(BaseAdapter[Task]):
1563
1699
  "number": pr_number,
1564
1700
  "owner": owner,
1565
1701
  "repo": repo,
1566
- }
1702
+ },
1567
1703
  },
1568
1704
  }
1569
1705
 
1570
1706
  client = self._create_client()
1571
1707
  async with client as session:
1572
1708
  result = await session.execute(
1573
- create_query,
1574
- variable_values={"input": attachment_input}
1709
+ create_query, variable_values={"input": attachment_input}
1575
1710
  )
1576
1711
 
1577
1712
  if result.get("attachmentCreate", {}).get("success"):
@@ -1607,8 +1742,8 @@ class LinearAdapter(BaseAdapter[Task]):
1607
1742
  async def create_pull_request_for_issue(
1608
1743
  self,
1609
1744
  ticket_id: str,
1610
- github_config: Dict[str, Any],
1611
- ) -> Dict[str, Any]:
1745
+ github_config: dict[str, Any],
1746
+ ) -> dict[str, Any]:
1612
1747
  """Create a GitHub PR for a Linear issue using GitHub integration.
1613
1748
 
1614
1749
  This requires GitHub integration to be configured in Linear.
@@ -1623,6 +1758,7 @@ class LinearAdapter(BaseAdapter[Task]):
1623
1758
 
1624
1759
  Returns:
1625
1760
  Dictionary with PR creation status
1761
+
1626
1762
  """
1627
1763
  # Get the issue details
1628
1764
  issue = await self.read(ticket_id)
@@ -1646,7 +1782,8 @@ class LinearAdapter(BaseAdapter[Task]):
1646
1782
  head_branch = f"{ticket_id.lower()}-{safe_title}"
1647
1783
 
1648
1784
  # Update the issue with the branch name
1649
- update_query = gql("""
1785
+ update_query = gql(
1786
+ """
1650
1787
  mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
1651
1788
  issueUpdate(id: $id, input: $input) {
1652
1789
  issue {
@@ -1657,7 +1794,8 @@ class LinearAdapter(BaseAdapter[Task]):
1657
1794
  success
1658
1795
  }
1659
1796
  }
1660
- """)
1797
+ """
1798
+ )
1661
1799
 
1662
1800
  linear_id = issue.metadata.get("linear", {}).get("id")
1663
1801
  if not linear_id:
@@ -1671,10 +1809,7 @@ class LinearAdapter(BaseAdapter[Task]):
1671
1809
  async with client as session:
1672
1810
  result = await session.execute(
1673
1811
  update_query,
1674
- variable_values={
1675
- "id": linear_id,
1676
- "input": {"branchName": head_branch}
1677
- }
1812
+ variable_values={"id": linear_id, "input": {"branchName": head_branch}},
1678
1813
  )
1679
1814
 
1680
1815
  if result.get("issueUpdate", {}).get("success"):
@@ -1703,23 +1838,24 @@ class LinearAdapter(BaseAdapter[Task]):
1703
1838
  else:
1704
1839
  raise ValueError(f"Failed to update issue {ticket_id} with branch name")
1705
1840
 
1706
- async def _search_by_identifier(self, identifier: str) -> Optional[Dict[str, Any]]:
1841
+ async def _search_by_identifier(self, identifier: str) -> Optional[dict[str, Any]]:
1707
1842
  """Search for an issue by its identifier."""
1708
- search_query = gql("""
1843
+ search_query = gql(
1844
+ """
1709
1845
  query SearchIssue($identifier: String!) {
1710
1846
  issue(id: $identifier) {
1711
1847
  id
1712
1848
  identifier
1713
1849
  }
1714
1850
  }
1715
- """)
1851
+ """
1852
+ )
1716
1853
 
1717
1854
  try:
1718
1855
  client = self._create_client()
1719
1856
  async with client as session:
1720
1857
  result = await session.execute(
1721
- search_query,
1722
- variable_values={"identifier": identifier}
1858
+ search_query, variable_values={"identifier": identifier}
1723
1859
  )
1724
1860
  return result.get("issue")
1725
1861
  except:
@@ -1728,10 +1864,7 @@ class LinearAdapter(BaseAdapter[Task]):
1728
1864
  # Epic/Issue/Task Hierarchy Methods (Linear: Project = Epic, Issue = Issue, Sub-issue = Task)
1729
1865
 
1730
1866
  async def create_epic(
1731
- self,
1732
- title: str,
1733
- description: Optional[str] = None,
1734
- **kwargs
1867
+ self, title: str, description: Optional[str] = None, **kwargs
1735
1868
  ) -> Optional[Epic]:
1736
1869
  """Create epic (Linear Project).
1737
1870
 
@@ -1742,10 +1875,13 @@ class LinearAdapter(BaseAdapter[Task]):
1742
1875
 
1743
1876
  Returns:
1744
1877
  Created epic or None if failed
1878
+
1745
1879
  """
1746
1880
  team_id = await self._ensure_team_id()
1747
1881
 
1748
- create_query = gql(PROJECT_FRAGMENT + """
1882
+ create_query = gql(
1883
+ PROJECT_FRAGMENT
1884
+ + """
1749
1885
  mutation CreateProject($input: ProjectCreateInput!) {
1750
1886
  projectCreate(input: $input) {
1751
1887
  success
@@ -1754,7 +1890,8 @@ class LinearAdapter(BaseAdapter[Task]):
1754
1890
  }
1755
1891
  }
1756
1892
  }
1757
- """)
1893
+ """
1894
+ )
1758
1895
 
1759
1896
  project_input = {
1760
1897
  "name": title,
@@ -1772,8 +1909,7 @@ class LinearAdapter(BaseAdapter[Task]):
1772
1909
  client = self._create_client()
1773
1910
  async with client as session:
1774
1911
  result = await session.execute(
1775
- create_query,
1776
- variable_values={"input": project_input}
1912
+ create_query, variable_values={"input": project_input}
1777
1913
  )
1778
1914
 
1779
1915
  if not result["projectCreate"]["success"]:
@@ -1790,22 +1926,23 @@ class LinearAdapter(BaseAdapter[Task]):
1790
1926
 
1791
1927
  Returns:
1792
1928
  Epic if found, None otherwise
1929
+
1793
1930
  """
1794
- query = gql(PROJECT_FRAGMENT + """
1931
+ query = gql(
1932
+ PROJECT_FRAGMENT
1933
+ + """
1795
1934
  query GetProject($id: String!) {
1796
1935
  project(id: $id) {
1797
1936
  ...ProjectFields
1798
1937
  }
1799
1938
  }
1800
- """)
1939
+ """
1940
+ )
1801
1941
 
1802
1942
  try:
1803
1943
  client = self._create_client()
1804
1944
  async with client as session:
1805
- result = await session.execute(
1806
- query,
1807
- variable_values={"id": epic_id}
1808
- )
1945
+ result = await session.execute(query, variable_values={"id": epic_id})
1809
1946
 
1810
1947
  if result.get("project"):
1811
1948
  return self._epic_from_linear_project(result["project"])
@@ -1814,7 +1951,7 @@ class LinearAdapter(BaseAdapter[Task]):
1814
1951
 
1815
1952
  return None
1816
1953
 
1817
- async def list_epics(self, **kwargs) -> List[Epic]:
1954
+ async def list_epics(self, **kwargs) -> builtins.list[Epic]:
1818
1955
  """List all Linear Projects (Epics).
1819
1956
 
1820
1957
  Args:
@@ -1822,6 +1959,7 @@ class LinearAdapter(BaseAdapter[Task]):
1822
1959
 
1823
1960
  Returns:
1824
1961
  List of epics
1962
+
1825
1963
  """
1826
1964
  team_id = await self._ensure_team_id()
1827
1965
 
@@ -1840,7 +1978,9 @@ class LinearAdapter(BaseAdapter[Task]):
1840
1978
  linear_state = state_mapping.get(kwargs["state"], "planned")
1841
1979
  project_filter["state"] = {"eq": linear_state}
1842
1980
 
1843
- query = gql(PROJECT_FRAGMENT + """
1981
+ query = gql(
1982
+ PROJECT_FRAGMENT
1983
+ + """
1844
1984
  query ListProjects($filter: ProjectFilter, $first: Int!) {
1845
1985
  projects(filter: $filter, first: $first, orderBy: updatedAt) {
1846
1986
  nodes {
@@ -1848,7 +1988,8 @@ class LinearAdapter(BaseAdapter[Task]):
1848
1988
  }
1849
1989
  }
1850
1990
  }
1851
- """)
1991
+ """
1992
+ )
1852
1993
 
1853
1994
  client = self._create_client()
1854
1995
  async with client as session:
@@ -1856,8 +1997,8 @@ class LinearAdapter(BaseAdapter[Task]):
1856
1997
  query,
1857
1998
  variable_values={
1858
1999
  "filter": project_filter,
1859
- "first": kwargs.get("limit", 50)
1860
- }
2000
+ "first": kwargs.get("limit", 50),
2001
+ },
1861
2002
  )
1862
2003
 
1863
2004
  epics = []
@@ -1871,7 +2012,7 @@ class LinearAdapter(BaseAdapter[Task]):
1871
2012
  title: str,
1872
2013
  description: Optional[str] = None,
1873
2014
  epic_id: Optional[str] = None,
1874
- **kwargs
2015
+ **kwargs,
1875
2016
  ) -> Optional[Task]:
1876
2017
  """Create issue and optionally associate with project (epic).
1877
2018
 
@@ -1883,6 +2024,7 @@ class LinearAdapter(BaseAdapter[Task]):
1883
2024
 
1884
2025
  Returns:
1885
2026
  Created issue or None if failed
2027
+
1886
2028
  """
1887
2029
  # Use existing create method but ensure it's created as an ISSUE type
1888
2030
  task = Task(
@@ -1890,13 +2032,13 @@ class LinearAdapter(BaseAdapter[Task]):
1890
2032
  description=description,
1891
2033
  ticket_type=TicketType.ISSUE,
1892
2034
  parent_epic=epic_id,
1893
- **{k: v for k, v in kwargs.items() if k in Task.__fields__}
2035
+ **{k: v for k, v in kwargs.items() if k in Task.__fields__},
1894
2036
  )
1895
2037
 
1896
2038
  # The existing create method handles project association via parent_epic field
1897
2039
  return await self.create(task)
1898
2040
 
1899
- async def list_issues_by_epic(self, epic_id: str) -> List[Task]:
2041
+ async def list_issues_by_epic(self, epic_id: str) -> builtins.list[Task]:
1900
2042
  """List all issues in a Linear project (epic).
1901
2043
 
1902
2044
  Args:
@@ -1904,8 +2046,11 @@ class LinearAdapter(BaseAdapter[Task]):
1904
2046
 
1905
2047
  Returns:
1906
2048
  List of issues belonging to project
2049
+
1907
2050
  """
1908
- query = gql(ISSUE_LIST_FRAGMENTS + """
2051
+ query = gql(
2052
+ ISSUE_LIST_FRAGMENTS
2053
+ + """
1909
2054
  query GetProjectIssues($projectId: String!, $first: Int!) {
1910
2055
  project(id: $projectId) {
1911
2056
  issues(first: $first) {
@@ -1915,14 +2060,14 @@ class LinearAdapter(BaseAdapter[Task]):
1915
2060
  }
1916
2061
  }
1917
2062
  }
1918
- """)
2063
+ """
2064
+ )
1919
2065
 
1920
2066
  try:
1921
2067
  client = self._create_client()
1922
2068
  async with client as session:
1923
2069
  result = await session.execute(
1924
- query,
1925
- variable_values={"projectId": epic_id, "first": 100}
2070
+ query, variable_values={"projectId": epic_id, "first": 100}
1926
2071
  )
1927
2072
 
1928
2073
  if not result.get("project"):
@@ -1940,11 +2085,7 @@ class LinearAdapter(BaseAdapter[Task]):
1940
2085
  return []
1941
2086
 
1942
2087
  async def create_task(
1943
- self,
1944
- title: str,
1945
- parent_id: str,
1946
- description: Optional[str] = None,
1947
- **kwargs
2088
+ self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
1948
2089
  ) -> Optional[Task]:
1949
2090
  """Create task as sub-issue of parent.
1950
2091
 
@@ -1959,24 +2100,26 @@ class LinearAdapter(BaseAdapter[Task]):
1959
2100
 
1960
2101
  Raises:
1961
2102
  ValueError: If parent_id is not provided
2103
+
1962
2104
  """
1963
2105
  if not parent_id:
1964
2106
  raise ValueError("Tasks must have a parent_id (issue identifier)")
1965
2107
 
1966
2108
  # Get parent issue's Linear ID
1967
- parent_query = gql("""
2109
+ parent_query = gql(
2110
+ """
1968
2111
  query GetIssueId($identifier: String!) {
1969
2112
  issue(id: $identifier) {
1970
2113
  id
1971
2114
  }
1972
2115
  }
1973
- """)
2116
+ """
2117
+ )
1974
2118
 
1975
2119
  client = self._create_client()
1976
2120
  async with client as session:
1977
2121
  parent_result = await session.execute(
1978
- parent_query,
1979
- variable_values={"identifier": parent_id}
2122
+ parent_query, variable_values={"identifier": parent_id}
1980
2123
  )
1981
2124
 
1982
2125
  if not parent_result.get("issue"):
@@ -1990,7 +2133,7 @@ class LinearAdapter(BaseAdapter[Task]):
1990
2133
  description=description,
1991
2134
  ticket_type=TicketType.TASK,
1992
2135
  parent_issue=parent_id,
1993
- **{k: v for k, v in kwargs.items() if k in Task.__fields__}
2136
+ **{k: v for k, v in kwargs.items() if k in Task.__fields__},
1994
2137
  )
1995
2138
 
1996
2139
  # Validate hierarchy
@@ -2024,10 +2167,14 @@ class LinearAdapter(BaseAdapter[Task]):
2024
2167
 
2025
2168
  # Set priority
2026
2169
  if task.priority:
2027
- issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(task.priority, 3)
2170
+ issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
2171
+ task.priority, 3
2172
+ )
2028
2173
 
2029
2174
  # Create sub-issue mutation
2030
- create_query = gql(ALL_FRAGMENTS + """
2175
+ create_query = gql(
2176
+ ALL_FRAGMENTS
2177
+ + """
2031
2178
  mutation CreateSubIssue($input: IssueCreateInput!) {
2032
2179
  issueCreate(input: $input) {
2033
2180
  success
@@ -2036,13 +2183,13 @@ class LinearAdapter(BaseAdapter[Task]):
2036
2183
  }
2037
2184
  }
2038
2185
  }
2039
- """)
2186
+ """
2187
+ )
2040
2188
 
2041
2189
  client = self._create_client()
2042
2190
  async with client as session:
2043
2191
  result = await session.execute(
2044
- create_query,
2045
- variable_values={"input": issue_input}
2192
+ create_query, variable_values={"input": issue_input}
2046
2193
  )
2047
2194
 
2048
2195
  if not result["issueCreate"]["success"]:
@@ -2051,7 +2198,7 @@ class LinearAdapter(BaseAdapter[Task]):
2051
2198
  created_issue = result["issueCreate"]["issue"]
2052
2199
  return self._task_from_linear_issue(created_issue)
2053
2200
 
2054
- async def list_tasks_by_issue(self, issue_id: str) -> List[Task]:
2201
+ async def list_tasks_by_issue(self, issue_id: str) -> builtins.list[Task]:
2055
2202
  """List all tasks (sub-issues) under an issue.
2056
2203
 
2057
2204
  Args:
@@ -2059,8 +2206,11 @@ class LinearAdapter(BaseAdapter[Task]):
2059
2206
 
2060
2207
  Returns:
2061
2208
  List of tasks belonging to issue
2209
+
2062
2210
  """
2063
- query = gql(ISSUE_LIST_FRAGMENTS + """
2211
+ query = gql(
2212
+ ISSUE_LIST_FRAGMENTS
2213
+ + """
2064
2214
  query GetIssueSubtasks($identifier: String!) {
2065
2215
  issue(id: $identifier) {
2066
2216
  children {
@@ -2070,14 +2220,14 @@ class LinearAdapter(BaseAdapter[Task]):
2070
2220
  }
2071
2221
  }
2072
2222
  }
2073
- """)
2223
+ """
2224
+ )
2074
2225
 
2075
2226
  try:
2076
2227
  client = self._create_client()
2077
2228
  async with client as session:
2078
2229
  result = await session.execute(
2079
- query,
2080
- variable_values={"identifier": issue_id}
2230
+ query, variable_values={"identifier": issue_id}
2081
2231
  )
2082
2232
 
2083
2233
  if not result.get("issue"):
@@ -2105,4 +2255,4 @@ class LinearAdapter(BaseAdapter[Task]):
2105
2255
 
2106
2256
 
2107
2257
  # Register the adapter
2108
- AdapterRegistry.register("linear", LinearAdapter)
2258
+ AdapterRegistry.register("linear", LinearAdapter)