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