mcp-ticketer 0.1.38__py3-none-any.whl → 0.2.0__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.
@@ -0,0 +1,490 @@
1
+ """Linear-specific CLI commands for workspace and team management."""
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from gql import gql, Client
10
+ from gql.transport.httpx import HTTPXTransport
11
+
12
+ app = typer.Typer(name="linear", help="Linear workspace and team management")
13
+ console = Console()
14
+
15
+
16
+ def _create_linear_client() -> Client:
17
+ """Create a Linear GraphQL client."""
18
+ api_key = os.getenv("LINEAR_API_KEY")
19
+ if not api_key:
20
+ console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
21
+ console.print("Set it in .env.local or environment variables")
22
+ raise typer.Exit(1)
23
+
24
+ transport = HTTPXTransport(
25
+ url="https://api.linear.app/graphql",
26
+ headers={"Authorization": api_key}
27
+ )
28
+ return Client(transport=transport, fetch_schema_from_transport=False)
29
+
30
+
31
+ @app.command("workspaces")
32
+ def list_workspaces():
33
+ """List all accessible Linear workspaces."""
34
+ console.print("🔍 Discovering Linear workspaces...")
35
+
36
+ # Query for current organization and user info
37
+ query = gql("""
38
+ query GetWorkspaceInfo {
39
+ viewer {
40
+ id
41
+ name
42
+ email
43
+ organization {
44
+ id
45
+ name
46
+ urlKey
47
+ createdAt
48
+ }
49
+ }
50
+ }
51
+ """)
52
+
53
+ try:
54
+ client = _create_linear_client()
55
+ result = client.execute(query)
56
+
57
+ viewer = result.get('viewer', {})
58
+ organization = viewer.get('organization', {})
59
+
60
+ console.print(f"\n👤 User: {viewer.get('name')} ({viewer.get('email')})")
61
+ console.print(f"🏢 Current Workspace:")
62
+ console.print(f" Name: {organization.get('name')}")
63
+ console.print(f" URL Key: {organization.get('urlKey')}")
64
+ console.print(f" ID: {organization.get('id')}")
65
+ if organization.get('createdAt'):
66
+ console.print(f" Created: {organization.get('createdAt')}")
67
+
68
+ console.print(f"\n✅ API key has access to: {organization.get('name')} workspace")
69
+ console.print(f"🌐 Workspace URL: https://linear.app/{organization.get('urlKey')}")
70
+
71
+ except Exception as e:
72
+ console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
73
+ raise typer.Exit(1)
74
+
75
+
76
+ @app.command("teams")
77
+ def list_teams(
78
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace URL key (optional)"),
79
+ all_teams: bool = typer.Option(False, "--all", "-a", help="Show all teams across all workspaces")
80
+ ):
81
+ """List all teams in the current workspace or all accessible teams."""
82
+ if all_teams:
83
+ console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
84
+ else:
85
+ console.print("🔍 Discovering Linear teams...")
86
+
87
+ # Query for all teams with pagination
88
+ query = gql("""
89
+ query GetTeams($first: Int, $after: String) {
90
+ viewer {
91
+ organization {
92
+ name
93
+ urlKey
94
+ }
95
+ }
96
+ teams(first: $first, after: $after) {
97
+ nodes {
98
+ id
99
+ key
100
+ name
101
+ description
102
+ private
103
+ createdAt
104
+ organization {
105
+ name
106
+ urlKey
107
+ }
108
+ members {
109
+ nodes {
110
+ id
111
+ name
112
+ }
113
+ }
114
+ issues(first: 1) {
115
+ nodes {
116
+ id
117
+ }
118
+ }
119
+ projects(first: 1) {
120
+ nodes {
121
+ id
122
+ }
123
+ }
124
+ }
125
+ pageInfo {
126
+ hasNextPage
127
+ endCursor
128
+ }
129
+ }
130
+ }
131
+ """)
132
+
133
+ try:
134
+ client = _create_linear_client()
135
+
136
+ # Fetch all teams with pagination
137
+ all_teams = []
138
+ has_next_page = True
139
+ after_cursor = None
140
+ current_workspace = None
141
+
142
+ while has_next_page:
143
+ variables = {"first": 50}
144
+ if after_cursor:
145
+ variables["after"] = after_cursor
146
+
147
+ result = client.execute(query, variable_values=variables)
148
+
149
+ # Get workspace info from first page
150
+ if current_workspace is None:
151
+ viewer = result.get('viewer', {})
152
+ current_workspace = viewer.get('organization', {})
153
+
154
+ teams_data = result.get('teams', {})
155
+ page_teams = teams_data.get('nodes', [])
156
+ page_info = teams_data.get('pageInfo', {})
157
+
158
+ all_teams.extend(page_teams)
159
+ has_next_page = page_info.get('hasNextPage', False)
160
+ after_cursor = page_info.get('endCursor')
161
+
162
+ # Display workspace info
163
+ console.print(f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})")
164
+
165
+ # Filter teams by workspace if specified
166
+ if workspace:
167
+ filtered_teams = [team for team in all_teams
168
+ if team.get('organization', {}).get('urlKey') == workspace]
169
+ if not filtered_teams:
170
+ console.print(f"[yellow]No teams found in workspace '{workspace}'[/yellow]")
171
+ return
172
+ all_teams = filtered_teams
173
+ elif not all_teams and current_workspace:
174
+ # If not showing all teams, filter to current workspace only
175
+ filtered_teams = [team for team in all_teams
176
+ if team.get('organization', {}).get('urlKey') == current_workspace.get('urlKey')]
177
+ all_teams = filtered_teams
178
+
179
+ if not all_teams:
180
+ console.print("[yellow]No teams found[/yellow]")
181
+ return
182
+
183
+ # Create table
184
+ title_suffix = " (all workspaces)" if all_teams else ""
185
+ table = Table(title=f"Linear Teams ({len(all_teams)} found){title_suffix}")
186
+ table.add_column("Key", style="cyan", no_wrap=True)
187
+ table.add_column("Name", style="bold")
188
+ table.add_column("Workspace", style="dim")
189
+ table.add_column("ID", style="dim")
190
+ table.add_column("Members", justify="center")
191
+ table.add_column("Issues", justify="center")
192
+ table.add_column("Projects", justify="center")
193
+ table.add_column("Private", justify="center")
194
+
195
+ for team in all_teams:
196
+ member_count = len(team.get('members', {}).get('nodes', []))
197
+ issue_count = len(team.get('issues', {}).get('nodes', []))
198
+ project_count = len(team.get('projects', {}).get('nodes', []))
199
+ is_private = "🔒" if team.get('private') else "🌐"
200
+ workspace_key = team.get('organization', {}).get('urlKey', '')
201
+
202
+ table.add_row(
203
+ team.get('key', ''),
204
+ team.get('name', ''),
205
+ workspace_key,
206
+ team.get('id', ''),
207
+ str(member_count),
208
+ str(issue_count),
209
+ str(project_count),
210
+ is_private
211
+ )
212
+
213
+ console.print(table)
214
+
215
+ # Show configuration suggestions
216
+ if all_teams:
217
+ console.print(f"\n💡 Configuration suggestions:")
218
+ for team in all_teams[:3]: # Show first 3 teams
219
+ console.print(f" Team '{team.get('name')}':")
220
+ console.print(f" team_key: '{team.get('key')}'")
221
+ console.print(f" team_id: '{team.get('id')}'")
222
+ console.print()
223
+
224
+ except Exception as e:
225
+ console.print(f"[red]❌ Error fetching teams: {e}[/red]")
226
+ raise typer.Exit(1)
227
+
228
+
229
+ @app.command("configure")
230
+ def configure_team(
231
+ team_key: Optional[str] = typer.Option(None, "--team-key", "-k", help="Team key (e.g., '1M')"),
232
+ team_id: Optional[str] = typer.Option(None, "--team-id", "-i", help="Team UUID"),
233
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace URL key"),
234
+ ):
235
+ """Configure Linear adapter with a specific team."""
236
+ from ..cli.main import load_config, save_config
237
+
238
+ if not team_key and not team_id:
239
+ console.print("[red]❌ Either --team-key or --team-id is required[/red]")
240
+ raise typer.Exit(1)
241
+
242
+ console.print("🔧 Configuring Linear adapter...")
243
+
244
+ # Validate team exists
245
+ if team_id:
246
+ # Validate team by ID
247
+ query = gql("""
248
+ query GetTeamById($id: String!) {
249
+ team(id: $id) {
250
+ id
251
+ key
252
+ name
253
+ organization {
254
+ name
255
+ urlKey
256
+ }
257
+ }
258
+ }
259
+ """)
260
+
261
+ try:
262
+ client = _create_linear_client()
263
+ result = client.execute(query, variable_values={"id": team_id})
264
+ team = result.get('team')
265
+
266
+ if not team:
267
+ console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
268
+ raise typer.Exit(1)
269
+
270
+ except Exception as e:
271
+ console.print(f"[red]❌ Error validating team: {e}[/red]")
272
+ raise typer.Exit(1)
273
+
274
+ elif team_key:
275
+ # Validate team by key
276
+ query = gql("""
277
+ query GetTeamByKey($key: String!) {
278
+ teams(filter: { key: { eq: $key } }) {
279
+ nodes {
280
+ id
281
+ key
282
+ name
283
+ organization {
284
+ name
285
+ urlKey
286
+ }
287
+ }
288
+ }
289
+ }
290
+ """)
291
+
292
+ try:
293
+ client = _create_linear_client()
294
+ result = client.execute(query, variable_values={"key": team_key})
295
+ teams = result.get('teams', {}).get('nodes', [])
296
+
297
+ if not teams:
298
+ console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
299
+ raise typer.Exit(1)
300
+
301
+ team = teams[0]
302
+ team_id = team['id'] # Use the found team ID
303
+
304
+ except Exception as e:
305
+ console.print(f"[red]❌ Error validating team: {e}[/red]")
306
+ raise typer.Exit(1)
307
+
308
+ # Update configuration
309
+ config = load_config()
310
+
311
+ # Ensure adapters section exists
312
+ if "adapters" not in config:
313
+ config["adapters"] = {}
314
+
315
+ # Update Linear adapter configuration
316
+ linear_config = {
317
+ "type": "linear",
318
+ "team_id": team_id
319
+ }
320
+
321
+ if team_key:
322
+ linear_config["team_key"] = team_key
323
+ if workspace:
324
+ linear_config["workspace"] = workspace
325
+
326
+ config["adapters"]["linear"] = linear_config
327
+
328
+ # Save configuration
329
+ save_config(config)
330
+
331
+ console.print(f"✅ Linear adapter configured successfully!")
332
+ console.print(f" Team: {team.get('name')} ({team.get('key')})")
333
+ console.print(f" Team ID: {team_id}")
334
+ console.print(f" Workspace: {team.get('organization', {}).get('name')}")
335
+
336
+ # Test the configuration
337
+ console.print("\n🧪 Testing configuration...")
338
+ try:
339
+ from ..adapters.linear import LinearAdapter
340
+ adapter = LinearAdapter(linear_config)
341
+
342
+ # Test by listing a few tickets
343
+ import asyncio
344
+ tickets = asyncio.run(adapter.list(limit=1))
345
+ console.print(f"✅ Configuration test successful! Found {len(tickets)} ticket(s)")
346
+
347
+ except Exception as e:
348
+ console.print(f"[yellow]⚠️ Configuration saved but test failed: {e}[/yellow]")
349
+ console.print("You may need to check your API key or team permissions")
350
+
351
+
352
+ @app.command("info")
353
+ def show_info(
354
+ team_key: Optional[str] = typer.Option(None, "--team-key", "-k", help="Team key to show info for"),
355
+ team_id: Optional[str] = typer.Option(None, "--team-id", "-i", help="Team UUID to show info for"),
356
+ ):
357
+ """Show detailed information about a specific team."""
358
+ if not team_key and not team_id:
359
+ console.print("[red]❌ Either --team-key or --team-id is required[/red]")
360
+ raise typer.Exit(1)
361
+
362
+ # Query for detailed team information
363
+ if team_id:
364
+ query = gql("""
365
+ query GetTeamInfo($id: String!) {
366
+ team(id: $id) {
367
+ id
368
+ key
369
+ name
370
+ description
371
+ private
372
+ createdAt
373
+ updatedAt
374
+ organization {
375
+ name
376
+ urlKey
377
+ }
378
+ members(first: 10) {
379
+ nodes {
380
+ id
381
+ name
382
+ active
383
+ }
384
+ }
385
+ states(first: 20) {
386
+ nodes {
387
+ id
388
+ name
389
+ type
390
+ position
391
+ }
392
+ }
393
+ }
394
+ }
395
+ """)
396
+ variables = {"id": team_id}
397
+ else:
398
+ query = gql("""
399
+ query GetTeamInfoByKey($key: String!) {
400
+ teams(filter: { key: { eq: $key } }) {
401
+ nodes {
402
+ id
403
+ key
404
+ name
405
+ description
406
+ private
407
+ createdAt
408
+ updatedAt
409
+ organization {
410
+ name
411
+ urlKey
412
+ }
413
+ members(first: 10) {
414
+ nodes {
415
+ id
416
+ name
417
+ active
418
+ }
419
+ }
420
+ states(first: 20) {
421
+ nodes {
422
+ id
423
+ name
424
+ type
425
+ position
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ """)
432
+ variables = {"key": team_key}
433
+
434
+ try:
435
+ client = _create_linear_client()
436
+ result = client.execute(query, variable_values=variables)
437
+
438
+ if team_id:
439
+ team = result.get('team')
440
+ else:
441
+ teams = result.get('teams', {}).get('nodes', [])
442
+ team = teams[0] if teams else None
443
+
444
+ if not team:
445
+ identifier = team_id or team_key
446
+ console.print(f"[red]❌ Team '{identifier}' not found[/red]")
447
+ raise typer.Exit(1)
448
+
449
+ # Display team information
450
+ console.print(f"\n🏷️ Team: {team.get('name')}")
451
+ console.print(f" Key: {team.get('key')}")
452
+ console.print(f" ID: {team.get('id')}")
453
+ console.print(f" Workspace: {team.get('organization', {}).get('name')} ({team.get('organization', {}).get('urlKey')})")
454
+ console.print(f" Privacy: {'🔒 Private' if team.get('private') else '🌐 Public'}")
455
+
456
+ if team.get('description'):
457
+ console.print(f" Description: {team.get('description')}")
458
+
459
+ console.print(f" Created: {team.get('createdAt')}")
460
+
461
+ # Statistics
462
+ member_count = len(team.get('members', {}).get('nodes', []))
463
+ state_count = len(team.get('states', {}).get('nodes', []))
464
+
465
+ console.print(f"\n📊 Statistics:")
466
+ console.print(f" Members: {member_count}")
467
+ console.print(f" Workflow States: {state_count}")
468
+
469
+ # Show members
470
+ members = team.get('members', {}).get('nodes', [])
471
+ if members:
472
+ console.print(f"\n👥 Members ({len(members)}):")
473
+ for member in members:
474
+ status = "✅" if member.get('active') else "❌"
475
+ console.print(f" {status} {member.get('name')}")
476
+ if len(members) == 10:
477
+ console.print(f" ... (showing first 10 members)")
478
+
479
+ # Show workflow states
480
+ states = team.get('states', {}).get('nodes', [])
481
+ if states:
482
+ console.print(f"\n🔄 Workflow States ({len(states)}):")
483
+ for state in sorted(states, key=lambda s: s.get('position', 0)):
484
+ console.print(f" {state.get('name')} ({state.get('type')})")
485
+ if len(states) == 20:
486
+ console.print(f" ... (showing first 20 states)")
487
+
488
+ except Exception as e:
489
+ console.print(f"[red]❌ Error fetching team info: {e}[/red]")
490
+ raise typer.Exit(1)
mcp_ticketer/cli/main.py CHANGED
@@ -14,7 +14,7 @@ from rich.table import Table
14
14
 
15
15
  from ..__version__ import __version__
16
16
  from ..core import AdapterRegistry, Priority, TicketState
17
- from ..core.models import SearchQuery
17
+ from ..core.models import SearchQuery, Comment
18
18
  from ..queue import Queue, QueueStatus, WorkerManager
19
19
  from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
20
20
  from ..queue.ticket_registry import TicketRegistry
@@ -24,6 +24,7 @@ import mcp_ticketer.adapters # noqa: F401
24
24
  from .configure import configure_wizard, set_adapter_config, show_current_config
25
25
  from .diagnostics import run_diagnostics
26
26
  from .discover import app as discover_app
27
+ from .linear_commands import app as linear_app
27
28
  from .migrate_config import migrate_config_command
28
29
  from .queue_commands import app as queue_app
29
30
 
@@ -1033,18 +1034,64 @@ def create(
1033
1034
  adapter_name = "aitrackdown"
1034
1035
 
1035
1036
  # Create task data
1037
+ # Import Priority for type checking
1038
+ from ..core.models import Priority as PriorityEnum
1039
+
1036
1040
  task_data = {
1037
1041
  "title": title,
1038
1042
  "description": description,
1039
- "priority": priority.value if isinstance(priority, Priority) else priority,
1043
+ "priority": priority.value if isinstance(priority, PriorityEnum) else priority,
1040
1044
  "tags": tags or [],
1041
1045
  "assignee": assignee,
1042
1046
  }
1043
1047
 
1044
- # Add to queue
1048
+ # WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
1049
+ if adapter_name == "linear":
1050
+ console.print("[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)")
1051
+ try:
1052
+ # Load configuration and create adapter directly
1053
+ config = load_config()
1054
+ adapter_config = config.get("adapters", {}).get(adapter_name, {})
1055
+
1056
+ # Import and create adapter
1057
+ from ..core.registry import AdapterRegistry
1058
+ adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
1059
+
1060
+ # Create task directly
1061
+ from ..core.models import Task, Priority
1062
+ task = Task(
1063
+ title=task_data["title"],
1064
+ description=task_data.get("description"),
1065
+ priority=Priority(task_data["priority"]) if task_data.get("priority") else Priority.MEDIUM,
1066
+ tags=task_data.get("tags", []),
1067
+ assignee=task_data.get("assignee")
1068
+ )
1069
+
1070
+ # Create ticket synchronously
1071
+ import asyncio
1072
+ result = asyncio.run(adapter.create(task))
1073
+
1074
+ console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
1075
+ console.print(f" Title: {result.title}")
1076
+ console.print(f" Priority: {result.priority}")
1077
+ console.print(f" State: {result.state}")
1078
+ # Get URL from metadata if available
1079
+ if result.metadata and 'linear' in result.metadata and 'url' in result.metadata['linear']:
1080
+ console.print(f" URL: {result.metadata['linear']['url']}")
1081
+
1082
+ return result.id
1083
+
1084
+ except Exception as e:
1085
+ console.print(f"[red]❌[/red] Failed to create ticket: {e}")
1086
+ raise
1087
+
1088
+ # Use queue for other adapters
1045
1089
  queue = Queue()
1046
1090
  queue_id = queue.add(
1047
- ticket_data=task_data, adapter=adapter_name, operation="create"
1091
+ ticket_data=task_data,
1092
+ adapter=adapter_name,
1093
+ operation="create",
1094
+ project_dir=str(Path.cwd()) # Explicitly pass current project directory
1048
1095
  )
1049
1096
 
1050
1097
  # Register in ticket registry for tracking
@@ -1127,12 +1174,15 @@ def list_tickets(
1127
1174
  table.add_column("Assignee", style="blue")
1128
1175
 
1129
1176
  for ticket in tickets:
1177
+ # Handle assignee field - Epic doesn't have assignee, Task does
1178
+ assignee = getattr(ticket, 'assignee', None) or "-"
1179
+
1130
1180
  table.add_row(
1131
1181
  ticket.id or "N/A",
1132
1182
  ticket.title,
1133
1183
  ticket.state,
1134
1184
  ticket.priority,
1135
- ticket.assignee or "-",
1185
+ assignee,
1136
1186
  )
1137
1187
 
1138
1188
  console.print(table)
@@ -1188,6 +1238,42 @@ def show(
1188
1238
  console.print(comment.content)
1189
1239
 
1190
1240
 
1241
+ @app.command()
1242
+ def comment(
1243
+ ticket_id: str = typer.Argument(..., help="Ticket ID"),
1244
+ content: str = typer.Argument(..., help="Comment content"),
1245
+ adapter: Optional[AdapterType] = typer.Option(
1246
+ None, "--adapter", help="Override default adapter"
1247
+ ),
1248
+ ) -> None:
1249
+ """Add a comment to a ticket."""
1250
+
1251
+ async def _comment():
1252
+ adapter_instance = get_adapter(
1253
+ override_adapter=adapter.value if adapter else None
1254
+ )
1255
+
1256
+ # Create comment
1257
+ comment = Comment(
1258
+ ticket_id=ticket_id,
1259
+ content=content,
1260
+ author="cli-user" # Could be made configurable
1261
+ )
1262
+
1263
+ result = await adapter_instance.add_comment(comment)
1264
+ return result
1265
+
1266
+ try:
1267
+ result = asyncio.run(_comment())
1268
+ console.print(f"[green]✓[/green] Comment added successfully")
1269
+ if result.id:
1270
+ console.print(f"Comment ID: {result.id}")
1271
+ console.print(f"Content: {content}")
1272
+ except Exception as e:
1273
+ console.print(f"[red]✗[/red] Failed to add comment: {e}")
1274
+ raise typer.Exit(1)
1275
+
1276
+
1191
1277
  @app.command()
1192
1278
  def update(
1193
1279
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
@@ -1231,9 +1317,14 @@ def update(
1231
1317
  # Add ticket_id to updates
1232
1318
  updates["ticket_id"] = ticket_id
1233
1319
 
1234
- # Add to queue
1320
+ # Add to queue with explicit project directory
1235
1321
  queue = Queue()
1236
- queue_id = queue.add(ticket_data=updates, adapter=adapter_name, operation="update")
1322
+ queue_id = queue.add(
1323
+ ticket_data=updates,
1324
+ adapter=adapter_name,
1325
+ operation="update",
1326
+ project_dir=str(Path.cwd()) # Explicitly pass current project directory
1327
+ )
1237
1328
 
1238
1329
  console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
1239
1330
  for key, value in updates.items():
@@ -1289,7 +1380,7 @@ def transition(
1289
1380
  adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1290
1381
  )
1291
1382
 
1292
- # Add to queue
1383
+ # Add to queue with explicit project directory
1293
1384
  queue = Queue()
1294
1385
  queue_id = queue.add(
1295
1386
  ticket_data={
@@ -1300,6 +1391,7 @@ def transition(
1300
1391
  },
1301
1392
  adapter=adapter_name,
1302
1393
  operation="transition",
1394
+ project_dir=str(Path.cwd()) # Explicitly pass current project directory
1303
1395
  )
1304
1396
 
1305
1397
  console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
@@ -1679,7 +1771,8 @@ def mcp_auggie(
1679
1771
  raise typer.Exit(1)
1680
1772
 
1681
1773
 
1682
- # Add MCP command group to main app (must be after all subcommands are defined)
1774
+ # Add command groups to main app (must be after all subcommands are defined)
1775
+ app.add_typer(linear_app, name="linear")
1683
1776
  app.add_typer(mcp_app, name="mcp")
1684
1777
 
1685
1778
 
mcp_ticketer/cli/utils.py CHANGED
@@ -210,10 +210,14 @@ class CommonPatterns:
210
210
  config = CommonPatterns.load_config()
211
211
  adapter_name = config.get("default_adapter", "aitrackdown")
212
212
 
213
- # Add to queue
213
+ # Add to queue with explicit project directory
214
+ from pathlib import Path
214
215
  queue = Queue()
215
216
  queue_id = queue.add(
216
- ticket_data=ticket_data, adapter=adapter_name, operation=operation
217
+ ticket_data=ticket_data,
218
+ adapter=adapter_name,
219
+ operation=operation,
220
+ project_dir=str(Path.cwd()) # Explicitly pass current project directory
217
221
  )
218
222
 
219
223
  if show_progress: