mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
7
7
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
8
8
 
9
9
  from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
10
+ from .state_matcher import get_state_matcher
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from .models import Attachment
@@ -22,17 +23,52 @@ class BaseAdapter(ABC, Generic[T]):
22
23
  """Initialize adapter with configuration.
23
24
 
24
25
  Args:
26
+ ----
25
27
  config: Adapter-specific configuration dictionary
26
28
 
27
29
  """
28
30
  self.config = config
29
31
  self._state_mapping = self._get_state_mapping()
30
32
 
33
+ @property
34
+ def adapter_type(self) -> str:
35
+ """Return lowercase adapter type identifier.
36
+
37
+ This identifier is used in MCP responses to show which adapter
38
+ handled the operation (e.g., "linear", "github", "jira", "asana").
39
+
40
+ Returns:
41
+ -------
42
+ Lowercase adapter type (e.g., "linear", "github")
43
+
44
+ """
45
+ # Extract adapter type from class name
46
+ # LinearAdapter -> linear, GitHubAdapter -> github
47
+ class_name = self.__class__.__name__
48
+ if class_name.endswith("Adapter"):
49
+ adapter_name = class_name[: -len("Adapter")]
50
+ else:
51
+ adapter_name = class_name
52
+
53
+ return adapter_name.lower()
54
+
55
+ @property
56
+ def adapter_display_name(self) -> str:
57
+ """Return human-readable adapter name.
58
+
59
+ Returns:
60
+ -------
61
+ Title-cased adapter name (e.g., "Linear", "Github", "Jira")
62
+
63
+ """
64
+ return self.adapter_type.title()
65
+
31
66
  @abstractmethod
32
67
  def _get_state_mapping(self) -> dict[TicketState, str]:
33
68
  """Get mapping from universal states to system-specific states.
34
69
 
35
70
  Returns:
71
+ -------
36
72
  Dictionary mapping TicketState to system-specific state strings
37
73
 
38
74
  """
@@ -43,6 +79,7 @@ class BaseAdapter(ABC, Generic[T]):
43
79
  """Validate that required credentials are present.
44
80
 
45
81
  Returns:
82
+ -------
46
83
  (is_valid, error_message) - Tuple of validation result and error message
47
84
 
48
85
  """
@@ -53,9 +90,11 @@ class BaseAdapter(ABC, Generic[T]):
53
90
  """Create a new ticket.
54
91
 
55
92
  Args:
93
+ ----
56
94
  ticket: Ticket to create (Epic or Task)
57
95
 
58
96
  Returns:
97
+ -------
59
98
  Created ticket with ID populated
60
99
 
61
100
  """
@@ -66,9 +105,11 @@ class BaseAdapter(ABC, Generic[T]):
66
105
  """Read a ticket by ID.
67
106
 
68
107
  Args:
108
+ ----
69
109
  ticket_id: Unique ticket identifier
70
110
 
71
111
  Returns:
112
+ -------
72
113
  Ticket if found, None otherwise
73
114
 
74
115
  """
@@ -79,10 +120,12 @@ class BaseAdapter(ABC, Generic[T]):
79
120
  """Update a ticket.
80
121
 
81
122
  Args:
123
+ ----
82
124
  ticket_id: Ticket identifier
83
125
  updates: Fields to update
84
126
 
85
127
  Returns:
128
+ -------
86
129
  Updated ticket if successful, None otherwise
87
130
 
88
131
  """
@@ -93,9 +136,11 @@ class BaseAdapter(ABC, Generic[T]):
93
136
  """Delete a ticket.
94
137
 
95
138
  Args:
139
+ ----
96
140
  ticket_id: Ticket identifier
97
141
 
98
142
  Returns:
143
+ -------
99
144
  True if deleted, False otherwise
100
145
 
101
146
  """
@@ -108,11 +153,13 @@ class BaseAdapter(ABC, Generic[T]):
108
153
  """List tickets with pagination and filters.
109
154
 
110
155
  Args:
156
+ ----
111
157
  limit: Maximum number of tickets
112
158
  offset: Skip this many tickets
113
159
  filters: Optional filter criteria
114
160
 
115
161
  Returns:
162
+ -------
116
163
  List of tickets matching criteria
117
164
 
118
165
  """
@@ -123,9 +170,11 @@ class BaseAdapter(ABC, Generic[T]):
123
170
  """Search tickets using advanced query.
124
171
 
125
172
  Args:
173
+ ----
126
174
  query: Search parameters
127
175
 
128
176
  Returns:
177
+ -------
129
178
  List of tickets matching search criteria
130
179
 
131
180
  """
@@ -138,10 +187,12 @@ class BaseAdapter(ABC, Generic[T]):
138
187
  """Transition ticket to a new state.
139
188
 
140
189
  Args:
190
+ ----
141
191
  ticket_id: Ticket identifier
142
192
  target_state: Target state
143
193
 
144
194
  Returns:
195
+ -------
145
196
  Updated ticket if transition successful, None otherwise
146
197
 
147
198
  """
@@ -152,9 +203,11 @@ class BaseAdapter(ABC, Generic[T]):
152
203
  """Add a comment to a ticket.
153
204
 
154
205
  Args:
206
+ ----
155
207
  comment: Comment to add
156
208
 
157
209
  Returns:
210
+ -------
158
211
  Created comment with ID populated
159
212
 
160
213
  """
@@ -167,11 +220,13 @@ class BaseAdapter(ABC, Generic[T]):
167
220
  """Get comments for a ticket.
168
221
 
169
222
  Args:
223
+ ----
170
224
  ticket_id: Ticket identifier
171
225
  limit: Maximum number of comments
172
226
  offset: Skip this many comments
173
227
 
174
228
  Returns:
229
+ -------
175
230
  List of comments for the ticket
176
231
 
177
232
  """
@@ -181,9 +236,11 @@ class BaseAdapter(ABC, Generic[T]):
181
236
  """Map universal state to system-specific state.
182
237
 
183
238
  Args:
239
+ ----
184
240
  state: Universal ticket state
185
241
 
186
242
  Returns:
243
+ -------
187
244
  System-specific state string
188
245
 
189
246
  """
@@ -193,31 +250,88 @@ class BaseAdapter(ABC, Generic[T]):
193
250
  """Map system-specific state to universal state.
194
251
 
195
252
  Args:
253
+ ----
196
254
  system_state: System-specific state string
197
255
 
198
256
  Returns:
257
+ -------
199
258
  Universal ticket state
200
259
 
201
260
  """
202
261
  reverse_mapping = {v: k for k, v in self._state_mapping.items()}
203
262
  return reverse_mapping.get(system_state, TicketState.OPEN)
204
263
 
264
+ def get_available_states(self) -> list[str]:
265
+ """Get list of adapter-specific available states.
266
+
267
+ Returns adapter-specific state names that can be used for
268
+ semantic state matching. Override in subclasses to provide
269
+ platform-specific state names.
270
+
271
+ Returns:
272
+ -------
273
+ List of adapter-specific state names
274
+
275
+ Example:
276
+ -------
277
+ >>> # Linear adapter override
278
+ >>> def get_available_states(self):
279
+ ... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
280
+
281
+ """
282
+ # Default: return universal state values
283
+ return [state.value for state in TicketState]
284
+
285
+ def resolve_state(self, user_input: str) -> TicketState:
286
+ """Resolve user input to universal state using semantic matcher.
287
+
288
+ Uses the semantic state matcher to interpret natural language
289
+ inputs and resolve them to universal TicketState values.
290
+
291
+ Args:
292
+ ----
293
+ user_input: Natural language state input (e.g., "working on it")
294
+
295
+ Returns:
296
+ -------
297
+ Resolved universal TicketState
298
+
299
+ Example:
300
+ -------
301
+ >>> adapter = get_adapter()
302
+ >>> state = adapter.resolve_state("working on it")
303
+ >>> print(state)
304
+ TicketState.IN_PROGRESS
305
+
306
+ """
307
+ matcher = get_state_matcher()
308
+ adapter_states = self.get_available_states()
309
+ result = matcher.match_state(user_input, adapter_states)
310
+ return result.state
311
+
205
312
  async def validate_transition(
206
313
  self, ticket_id: str, target_state: TicketState
207
314
  ) -> bool:
208
315
  """Validate if state transition is allowed.
209
316
 
317
+ Validates both workflow rules and parent/child state constraints:
318
+ - Parent issues must remain at least as complete as their most complete child
319
+ - Standard workflow transitions must be valid
320
+
210
321
  Args:
322
+ ----
211
323
  ticket_id: Ticket identifier
212
324
  target_state: Target state
213
325
 
214
326
  Returns:
327
+ -------
215
328
  True if transition is valid
216
329
 
217
330
  """
218
331
  ticket = await self.read(ticket_id)
219
332
  if not ticket:
220
333
  return False
334
+
221
335
  # Handle case where state might be stored as string due to use_enum_values=True
222
336
  current_state = ticket.state
223
337
  if isinstance(current_state, str):
@@ -225,21 +339,51 @@ class BaseAdapter(ABC, Generic[T]):
225
339
  current_state = TicketState(current_state)
226
340
  except ValueError:
227
341
  return False
228
- return current_state.can_transition_to(target_state)
342
+
343
+ # Check workflow transition validity
344
+ if not current_state.can_transition_to(target_state):
345
+ return False
346
+
347
+ # Check parent/child state constraint
348
+ # If this ticket has children, ensure target state >= max child state
349
+ if isinstance(ticket, Task):
350
+ # Get all children
351
+ children = await self.list_tasks_by_issue(ticket_id)
352
+ if children:
353
+ # Find max child completion level
354
+ max_child_level = 0
355
+ for child in children:
356
+ child_state = child.state
357
+ if isinstance(child_state, str):
358
+ try:
359
+ child_state = TicketState(child_state)
360
+ except ValueError:
361
+ continue
362
+ max_child_level = max(
363
+ max_child_level, child_state.completion_level()
364
+ )
365
+
366
+ # Target state must be at least as complete as most complete child
367
+ if target_state.completion_level() < max_child_level:
368
+ return False
369
+
370
+ return True
229
371
 
230
372
  # Epic/Issue/Task Hierarchy Methods
231
373
 
232
374
  async def create_epic(
233
- self, title: str, description: str | None = None, **kwargs
375
+ self, title: str, description: str | None = None, **kwargs: Any
234
376
  ) -> Epic | None:
235
377
  """Create epic (top-level grouping).
236
378
 
237
379
  Args:
380
+ ----
238
381
  title: Epic title
239
382
  description: Epic description
240
383
  **kwargs: Additional adapter-specific fields
241
384
 
242
385
  Returns:
386
+ -------
243
387
  Created epic or None if failed
244
388
 
245
389
  """
@@ -258,9 +402,11 @@ class BaseAdapter(ABC, Generic[T]):
258
402
  """Get epic by ID.
259
403
 
260
404
  Args:
405
+ ----
261
406
  epic_id: Epic identifier
262
407
 
263
408
  Returns:
409
+ -------
264
410
  Epic if found, None otherwise
265
411
 
266
412
  """
@@ -270,13 +416,15 @@ class BaseAdapter(ABC, Generic[T]):
270
416
  return result
271
417
  return None
272
418
 
273
- async def list_epics(self, **kwargs) -> builtins.list[Epic]:
419
+ async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
274
420
  """List all epics.
275
421
 
276
422
  Args:
423
+ ----
277
424
  **kwargs: Adapter-specific filter parameters
278
425
 
279
426
  Returns:
427
+ -------
280
428
  List of epics
281
429
 
282
430
  """
@@ -291,17 +439,19 @@ class BaseAdapter(ABC, Generic[T]):
291
439
  title: str,
292
440
  description: str | None = None,
293
441
  epic_id: str | None = None,
294
- **kwargs,
442
+ **kwargs: Any,
295
443
  ) -> Task | None:
296
444
  """Create issue, optionally linked to epic.
297
445
 
298
446
  Args:
447
+ ----
299
448
  title: Issue title
300
449
  description: Issue description
301
450
  epic_id: Optional parent epic ID
302
451
  **kwargs: Additional adapter-specific fields
303
452
 
304
453
  Returns:
454
+ -------
305
455
  Created issue or None if failed
306
456
 
307
457
  """
@@ -318,9 +468,11 @@ class BaseAdapter(ABC, Generic[T]):
318
468
  """List all issues in epic.
319
469
 
320
470
  Args:
471
+ ----
321
472
  epic_id: Epic identifier
322
473
 
323
474
  Returns:
475
+ -------
324
476
  List of issues belonging to epic
325
477
 
326
478
  """
@@ -330,20 +482,23 @@ class BaseAdapter(ABC, Generic[T]):
330
482
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
331
483
 
332
484
  async def create_task(
333
- self, title: str, parent_id: str, description: str | None = None, **kwargs
485
+ self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
334
486
  ) -> Task | None:
335
487
  """Create task as sub-ticket of parent issue.
336
488
 
337
489
  Args:
490
+ ----
338
491
  title: Task title
339
492
  parent_id: Required parent issue ID
340
493
  description: Task description
341
494
  **kwargs: Additional adapter-specific fields
342
495
 
343
496
  Returns:
497
+ -------
344
498
  Created task or None if failed
345
499
 
346
500
  Raises:
501
+ ------
347
502
  ValueError: If parent_id is not provided
348
503
 
349
504
  """
@@ -369,9 +524,11 @@ class BaseAdapter(ABC, Generic[T]):
369
524
  """List all tasks under an issue.
370
525
 
371
526
  Args:
527
+ ----
372
528
  issue_id: Issue identifier
373
529
 
374
530
  Returns:
531
+ -------
375
532
  List of tasks belonging to issue
376
533
 
377
534
  """
@@ -390,14 +547,17 @@ class BaseAdapter(ABC, Generic[T]):
390
547
  """Attach a file to a ticket.
391
548
 
392
549
  Args:
550
+ ----
393
551
  ticket_id: Ticket identifier
394
552
  file_path: Local file path to upload
395
553
  description: Optional attachment description
396
554
 
397
555
  Returns:
556
+ -------
398
557
  Created Attachment with metadata
399
558
 
400
559
  Raises:
560
+ ------
401
561
  NotImplementedError: If adapter doesn't support attachments
402
562
  FileNotFoundError: If file doesn't exist
403
563
  ValueError: If ticket doesn't exist or upload fails
@@ -412,9 +572,11 @@ class BaseAdapter(ABC, Generic[T]):
412
572
  """Get all attachments for a ticket.
413
573
 
414
574
  Args:
575
+ ----
415
576
  ticket_id: Ticket identifier
416
577
 
417
578
  Returns:
579
+ -------
418
580
  List of attachments (empty if none or not supported)
419
581
 
420
582
  """
@@ -430,13 +592,16 @@ class BaseAdapter(ABC, Generic[T]):
430
592
  """Delete an attachment (optional implementation).
431
593
 
432
594
  Args:
595
+ ----
433
596
  ticket_id: Ticket identifier
434
597
  attachment_id: Attachment identifier
435
598
 
436
599
  Returns:
600
+ -------
437
601
  True if deleted, False otherwise
438
602
 
439
603
  Raises:
604
+ ------
440
605
  NotImplementedError: If adapter doesn't support deletion
441
606
 
442
607
  """
@@ -6,7 +6,7 @@ import os
6
6
  from enum import Enum
7
7
  from functools import lru_cache
8
8
  from pathlib import Path
9
- from typing import Any, Optional
9
+ from typing import Any, Optional, cast
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, Field, field_validator, model_validator
@@ -38,92 +38,98 @@ class GitHubConfig(BaseAdapterConfig):
38
38
  """GitHub adapter configuration."""
39
39
 
40
40
  type: AdapterType = AdapterType.GITHUB
41
- token: str | None = Field(None, env="GITHUB_TOKEN")
42
- owner: str | None = Field(None, env="GITHUB_OWNER")
43
- repo: str | None = Field(None, env="GITHUB_REPO")
41
+ token: str | None = Field(default=None)
42
+ owner: str | None = Field(default=None)
43
+ repo: str | None = Field(default=None)
44
44
  api_url: str = "https://api.github.com"
45
45
  use_projects_v2: bool = False
46
46
  custom_priority_scheme: dict[str, list[str]] | None = None
47
47
 
48
48
  @field_validator("token", mode="before")
49
49
  @classmethod
50
- def validate_token(cls, v):
50
+ def validate_token(cls, v: Any) -> str:
51
+ """Validate GitHub token from config or environment."""
51
52
  if not v:
52
53
  v = os.getenv("GITHUB_TOKEN")
53
54
  if not v:
54
55
  raise ValueError("GitHub token is required")
55
- return v
56
+ return cast(str, v)
56
57
 
57
58
  @field_validator("owner", mode="before")
58
59
  @classmethod
59
- def validate_owner(cls, v):
60
+ def validate_owner(cls, v: Any) -> str:
61
+ """Validate GitHub repository owner from config or environment."""
60
62
  if not v:
61
63
  v = os.getenv("GITHUB_OWNER")
62
64
  if not v:
63
65
  raise ValueError("GitHub owner is required")
64
- return v
66
+ return cast(str, v)
65
67
 
66
68
  @field_validator("repo", mode="before")
67
69
  @classmethod
68
- def validate_repo(cls, v):
70
+ def validate_repo(cls, v: Any) -> str:
71
+ """Validate GitHub repository name from config or environment."""
69
72
  if not v:
70
73
  v = os.getenv("GITHUB_REPO")
71
74
  if not v:
72
75
  raise ValueError("GitHub repo is required")
73
- return v
76
+ return cast(str, v)
74
77
 
75
78
 
76
79
  class JiraConfig(BaseAdapterConfig):
77
80
  """JIRA adapter configuration."""
78
81
 
79
82
  type: AdapterType = AdapterType.JIRA
80
- server: str | None = Field(None, env="JIRA_SERVER")
81
- email: str | None = Field(None, env="JIRA_EMAIL")
82
- api_token: str | None = Field(None, env="JIRA_API_TOKEN")
83
- project_key: str | None = Field(None, env="JIRA_PROJECT_KEY")
83
+ server: str | None = Field(default=None)
84
+ email: str | None = Field(default=None)
85
+ api_token: str | None = Field(default=None)
86
+ project_key: str | None = Field(default=None)
84
87
  cloud: bool = True
85
88
  verify_ssl: bool = True
86
89
 
87
90
  @field_validator("server", mode="before")
88
91
  @classmethod
89
- def validate_server(cls, v):
92
+ def validate_server(cls, v: Any) -> str:
93
+ """Validate JIRA server URL from config or environment."""
90
94
  if not v:
91
95
  v = os.getenv("JIRA_SERVER")
92
96
  if not v:
93
97
  raise ValueError("JIRA server URL is required")
94
- return v.rstrip("/")
98
+ return cast(str, v).rstrip("/")
95
99
 
96
100
  @field_validator("email", mode="before")
97
101
  @classmethod
98
- def validate_email(cls, v):
102
+ def validate_email(cls, v: Any) -> str:
103
+ """Validate JIRA user email from config or environment."""
99
104
  if not v:
100
105
  v = os.getenv("JIRA_EMAIL")
101
106
  if not v:
102
107
  raise ValueError("JIRA email is required")
103
- return v
108
+ return cast(str, v)
104
109
 
105
110
  @field_validator("api_token", mode="before")
106
111
  @classmethod
107
- def validate_api_token(cls, v):
112
+ def validate_api_token(cls, v: Any) -> str:
113
+ """Validate JIRA API token from config or environment."""
108
114
  if not v:
109
115
  v = os.getenv("JIRA_API_TOKEN")
110
116
  if not v:
111
117
  raise ValueError("JIRA API token is required")
112
- return v
118
+ return cast(str, v)
113
119
 
114
120
 
115
121
  class LinearConfig(BaseAdapterConfig):
116
122
  """Linear adapter configuration."""
117
123
 
118
124
  type: AdapterType = AdapterType.LINEAR
119
- api_key: str | None = Field(None, env="LINEAR_API_KEY")
125
+ api_key: str | None = Field(default=None)
120
126
  workspace: str | None = None
121
127
  team_key: str | None = None # Short team key like "BTA"
122
128
  team_id: str | None = None # UUID team identifier
123
129
  api_url: str = "https://api.linear.app/graphql"
124
130
 
125
131
  @model_validator(mode="after")
126
- def validate_team_identifier(self):
132
+ def validate_team_identifier(self) -> "LinearConfig":
127
133
  """Ensure either team_key or team_id is provided."""
128
134
  if not self.team_key and not self.team_id:
129
135
  raise ValueError("Either team_key or team_id is required")
@@ -131,12 +137,13 @@ class LinearConfig(BaseAdapterConfig):
131
137
 
132
138
  @field_validator("api_key", mode="before")
133
139
  @classmethod
134
- def validate_api_key(cls, v):
140
+ def validate_api_key(cls, v: Any) -> str:
141
+ """Validate Linear API key from config or environment."""
135
142
  if not v:
136
143
  v = os.getenv("LINEAR_API_KEY")
137
144
  if not v:
138
145
  raise ValueError("Linear API key is required")
139
- return v
146
+ return cast(str, v)
140
147
 
141
148
 
142
149
  class AITrackdownConfig(BaseAdapterConfig):
@@ -179,7 +186,7 @@ class AppConfig(BaseModel):
179
186
  default_adapter: str | None = None
180
187
 
181
188
  @model_validator(mode="after")
182
- def validate_adapters(self):
189
+ def validate_adapters(self) -> "AppConfig":
183
190
  """Validate adapter configurations."""
184
191
  adapters = self.adapters
185
192
 
@@ -220,7 +227,7 @@ class ConfigurationManager:
220
227
  cls._instance = super().__new__(cls)
221
228
  return cls._instance
222
229
 
223
- def __init__(self):
230
+ def __init__(self) -> None:
224
231
  """Initialize configuration manager."""
225
232
  if not hasattr(self, "_initialized"):
226
233
  self._initialized = True
@@ -336,16 +343,16 @@ class ConfigurationManager:
336
343
  try:
337
344
  with open(config_path, encoding="utf-8") as file:
338
345
  if config_path.suffix.lower() in [".yaml", ".yml"]:
339
- return yaml.safe_load(file) or {}
346
+ return cast(dict[str, Any], yaml.safe_load(file) or {})
340
347
  elif config_path.suffix.lower() == ".json":
341
- return json.load(file)
348
+ return cast(dict[str, Any], json.load(file))
342
349
  else:
343
350
  # Try YAML first, then JSON
344
351
  content = file.read()
345
352
  try:
346
- return yaml.safe_load(content) or {}
353
+ return cast(dict[str, Any], yaml.safe_load(content) or {})
347
354
  except yaml.YAMLError:
348
- return json.loads(content)
355
+ return cast(dict[str, Any], json.loads(content))
349
356
  except Exception as e:
350
357
  logger.error(f"Error loading config file {config_path}: {e}")
351
358
  return {}
@@ -375,7 +382,7 @@ class ConfigurationManager:
375
382
  }
376
383
 
377
384
  # Convert discovered adapters to config format
378
- config_data = {"adapters": {}, "default_adapter": None}
385
+ config_data: dict[str, Any] = {"adapters": {}, "default_adapter": None}
379
386
 
380
387
  for adapter in discovered.adapters:
381
388
  adapter_config = {"type": adapter.adapter_type, "enabled": True}