mcp-ticketer 0.1.38__py3-none-any.whl → 0.1.39__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.

@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified Environment Loading System for MCP Ticketer.
4
+
5
+ This module provides a resilient environment loading system that:
6
+ 1. Supports multiple naming conventions for each configuration key
7
+ 2. Loads from multiple sources (.env.local, .env, environment variables)
8
+ 3. Works consistently across CLI, worker processes, and MCP server
9
+ 4. Provides fallback mechanisms for different key aliases
10
+ """
11
+
12
+ import os
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import Dict, Any, List, Optional, Union
16
+ from dataclasses import dataclass
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class EnvKeyConfig:
23
+ """Configuration for environment variable key aliases."""
24
+ primary_key: str
25
+ aliases: List[str]
26
+ description: str
27
+ required: bool = False
28
+ default: Optional[str] = None
29
+
30
+
31
+ class UnifiedEnvLoader:
32
+ """
33
+ Unified environment loader that handles multiple naming conventions
34
+ and provides consistent environment loading across all contexts.
35
+ """
36
+
37
+ # Define key aliases for all adapters
38
+ KEY_MAPPINGS = {
39
+ # Linear adapter keys
40
+ "linear_api_key": EnvKeyConfig(
41
+ primary_key="LINEAR_API_KEY",
42
+ aliases=["LINEAR_TOKEN", "LINEAR_ACCESS_TOKEN", "LINEAR_AUTH_TOKEN"],
43
+ description="Linear API key",
44
+ required=True
45
+ ),
46
+ "linear_team_id": EnvKeyConfig(
47
+ primary_key="LINEAR_TEAM_ID",
48
+ aliases=["LINEAR_TEAM_UUID", "LINEAR_TEAM_IDENTIFIER"],
49
+ description="Linear team ID (UUID)",
50
+ required=False
51
+ ),
52
+ "linear_team_key": EnvKeyConfig(
53
+ primary_key="LINEAR_TEAM_KEY",
54
+ aliases=["LINEAR_TEAM_IDENTIFIER", "LINEAR_TEAM_NAME"],
55
+ description="Linear team key (short name)",
56
+ required=False
57
+ ),
58
+
59
+ # JIRA adapter keys
60
+ "jira_server": EnvKeyConfig(
61
+ primary_key="JIRA_SERVER",
62
+ aliases=["JIRA_URL", "JIRA_HOST", "JIRA_BASE_URL"],
63
+ description="JIRA server URL",
64
+ required=True
65
+ ),
66
+ "jira_email": EnvKeyConfig(
67
+ primary_key="JIRA_EMAIL",
68
+ aliases=["JIRA_USER", "JIRA_USERNAME", "JIRA_ACCESS_USER"],
69
+ description="JIRA user email",
70
+ required=True
71
+ ),
72
+ "jira_api_token": EnvKeyConfig(
73
+ primary_key="JIRA_API_TOKEN",
74
+ aliases=["JIRA_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_AUTH_TOKEN", "JIRA_PASSWORD"],
75
+ description="JIRA API token",
76
+ required=True
77
+ ),
78
+ "jira_project_key": EnvKeyConfig(
79
+ primary_key="JIRA_PROJECT_KEY",
80
+ aliases=["JIRA_PROJECT", "JIRA_PROJECT_ID"],
81
+ description="JIRA project key",
82
+ required=False
83
+ ),
84
+
85
+ # GitHub adapter keys
86
+ "github_token": EnvKeyConfig(
87
+ primary_key="GITHUB_TOKEN",
88
+ aliases=["GITHUB_ACCESS_TOKEN", "GITHUB_API_TOKEN", "GITHUB_AUTH_TOKEN"],
89
+ description="GitHub access token",
90
+ required=True
91
+ ),
92
+ "github_owner": EnvKeyConfig(
93
+ primary_key="GITHUB_OWNER",
94
+ aliases=["GITHUB_USER", "GITHUB_USERNAME", "GITHUB_ORG"],
95
+ description="GitHub repository owner",
96
+ required=True
97
+ ),
98
+ "github_repo": EnvKeyConfig(
99
+ primary_key="GITHUB_REPO",
100
+ aliases=["GITHUB_REPOSITORY", "GITHUB_REPO_NAME"],
101
+ description="GitHub repository name",
102
+ required=True
103
+ ),
104
+ }
105
+
106
+ def __init__(self, project_root: Optional[Path] = None):
107
+ """Initialize the environment loader.
108
+
109
+ Args:
110
+ project_root: Project root directory. If None, will auto-detect.
111
+ """
112
+ self.project_root = project_root or self._find_project_root()
113
+ self._env_cache: Dict[str, str] = {}
114
+ self._load_env_files()
115
+
116
+ def _find_project_root(self) -> Path:
117
+ """Find the project root directory."""
118
+ current = Path.cwd()
119
+
120
+ # Look for common project indicators
121
+ indicators = [".mcp-ticketer", ".git", "pyproject.toml", "setup.py"]
122
+
123
+ while current != current.parent:
124
+ if any((current / indicator).exists() for indicator in indicators):
125
+ return current
126
+ current = current.parent
127
+
128
+ # Fallback to current directory
129
+ return Path.cwd()
130
+
131
+ def _load_env_files(self):
132
+ """Load environment variables from .env files."""
133
+ env_files = [
134
+ self.project_root / ".env.local",
135
+ self.project_root / ".env",
136
+ Path.home() / ".mcp-ticketer" / ".env",
137
+ ]
138
+
139
+ for env_file in env_files:
140
+ if env_file.exists():
141
+ logger.debug(f"Loading environment from: {env_file}")
142
+ self._load_env_file(env_file)
143
+
144
+ def _load_env_file(self, env_file: Path):
145
+ """Load variables from a single .env file."""
146
+ try:
147
+ with open(env_file, 'r') as f:
148
+ for line_num, line in enumerate(f, 1):
149
+ line = line.strip()
150
+
151
+ # Skip empty lines and comments
152
+ if not line or line.startswith('#'):
153
+ continue
154
+
155
+ # Parse KEY=VALUE format
156
+ if '=' in line:
157
+ key, value = line.split('=', 1)
158
+ key = key.strip()
159
+ value = value.strip()
160
+
161
+ # Remove quotes if present
162
+ if value.startswith('"') and value.endswith('"'):
163
+ value = value[1:-1]
164
+ elif value.startswith("'") and value.endswith("'"):
165
+ value = value[1:-1]
166
+
167
+ # Only set if not already in environment
168
+ if key not in os.environ:
169
+ os.environ[key] = value
170
+ self._env_cache[key] = value
171
+ logger.debug(f"Loaded {key} from {env_file}")
172
+
173
+ except Exception as e:
174
+ logger.warning(f"Failed to load {env_file}: {e}")
175
+
176
+ def get_value(self, config_key: str, config: Optional[Dict[str, Any]] = None) -> Optional[str]:
177
+ """
178
+ Get a configuration value using the key alias system.
179
+
180
+ Args:
181
+ config_key: The configuration key (e.g., 'linear_api_key')
182
+ config: Optional configuration dictionary to check first
183
+
184
+ Returns:
185
+ The value if found, None otherwise
186
+ """
187
+ if config_key not in self.KEY_MAPPINGS:
188
+ logger.warning(f"Unknown configuration key: {config_key}")
189
+ return None
190
+
191
+ key_config = self.KEY_MAPPINGS[config_key]
192
+
193
+ # 1. Check provided config dictionary first
194
+ if config:
195
+ # Check for the config key itself (without adapter prefix)
196
+ simple_key = config_key.split('_', 1)[1] if '_' in config_key else config_key
197
+ if simple_key in config:
198
+ value = config[simple_key]
199
+ if value:
200
+ logger.debug(f"Found {config_key} in config as {simple_key}")
201
+ return str(value)
202
+
203
+ # 2. Check environment variables (primary key first, then aliases)
204
+ all_keys = [key_config.primary_key] + key_config.aliases
205
+
206
+ for env_key in all_keys:
207
+ value = os.getenv(env_key)
208
+ if value:
209
+ logger.debug(f"Found {config_key} as {env_key}")
210
+ return value
211
+
212
+ # 3. Return default if available
213
+ if key_config.default:
214
+ logger.debug(f"Using default for {config_key}")
215
+ return key_config.default
216
+
217
+ # 4. Log if required key is missing
218
+ if key_config.required:
219
+ logger.warning(f"Required configuration key {config_key} not found. Tried: {all_keys}")
220
+
221
+ return None
222
+
223
+ def get_adapter_config(self, adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
224
+ """
225
+ Get complete configuration for an adapter with environment variable resolution.
226
+
227
+ Args:
228
+ adapter_name: Name of the adapter ('linear', 'jira', 'github')
229
+ base_config: Base configuration dictionary
230
+
231
+ Returns:
232
+ Complete configuration with environment variables resolved
233
+ """
234
+ config = base_config.copy() if base_config else {}
235
+
236
+ # Get adapter-specific keys
237
+ adapter_keys = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
238
+
239
+ for config_key in adapter_keys:
240
+ # Remove adapter prefix for the config key
241
+ simple_key = config_key.split('_', 1)[1]
242
+
243
+ # Only set if not already in config or if config value is empty
244
+ if simple_key not in config or not config[simple_key]:
245
+ value = self.get_value(config_key, config)
246
+ if value:
247
+ config[simple_key] = value
248
+
249
+ return config
250
+
251
+ def validate_adapter_config(self, adapter_name: str, config: Dict[str, Any]) -> List[str]:
252
+ """
253
+ Validate that all required configuration is present for an adapter.
254
+
255
+ Args:
256
+ adapter_name: Name of the adapter
257
+ config: Configuration dictionary
258
+
259
+ Returns:
260
+ List of missing required keys (empty if all required keys are present)
261
+ """
262
+ missing_keys = []
263
+ adapter_keys = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
264
+
265
+ for config_key in adapter_keys:
266
+ key_config = self.KEY_MAPPINGS[config_key]
267
+ if key_config.required:
268
+ simple_key = config_key.split('_', 1)[1]
269
+ if simple_key not in config or not config[simple_key]:
270
+ missing_keys.append(f"{simple_key} ({key_config.description})")
271
+
272
+ return missing_keys
273
+
274
+ def get_debug_info(self) -> Dict[str, Any]:
275
+ """Get debug information about environment loading."""
276
+ return {
277
+ "project_root": str(self.project_root),
278
+ "env_files_checked": [
279
+ str(self.project_root / ".env.local"),
280
+ str(self.project_root / ".env"),
281
+ str(Path.home() / ".mcp-ticketer" / ".env"),
282
+ ],
283
+ "loaded_keys": list(self._env_cache.keys()),
284
+ "available_configs": list(self.KEY_MAPPINGS.keys()),
285
+ }
286
+
287
+
288
+ # Global instance
289
+ _env_loader: Optional[UnifiedEnvLoader] = None
290
+
291
+
292
+ def get_env_loader() -> UnifiedEnvLoader:
293
+ """Get the global environment loader instance."""
294
+ global _env_loader
295
+ if _env_loader is None:
296
+ _env_loader = UnifiedEnvLoader()
297
+ return _env_loader
298
+
299
+
300
+ def load_adapter_config(adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
301
+ """
302
+ Convenience function to load adapter configuration with environment variables.
303
+
304
+ Args:
305
+ adapter_name: Name of the adapter ('linear', 'jira', 'github')
306
+ base_config: Base configuration dictionary
307
+
308
+ Returns:
309
+ Complete configuration with environment variables resolved
310
+ """
311
+ return get_env_loader().get_adapter_config(adapter_name, base_config)
312
+
313
+
314
+ def validate_adapter_config(adapter_name: str, config: Dict[str, Any]) -> List[str]:
315
+ """
316
+ Convenience function to validate adapter configuration.
317
+
318
+ Args:
319
+ adapter_name: Name of the adapter
320
+ config: Configuration dictionary
321
+
322
+ Returns:
323
+ List of missing required keys (empty if all required keys are present)
324
+ """
325
+ return get_env_loader().validate_adapter_config(adapter_name, config)
@@ -1,4 +1,26 @@
1
- """Simplified Universal Ticket models using Pydantic."""
1
+ """Universal Ticket models using Pydantic.
2
+
3
+ This module defines the core data models for the MCP Ticketer system, providing
4
+ a unified interface across different ticket management platforms (Linear, JIRA,
5
+ GitHub, etc.).
6
+
7
+ The models follow a hierarchical structure:
8
+ - Epic: Strategic level containers (Projects in Linear, Epics in JIRA)
9
+ - Issue: Standard work items (Issues in GitHub, Stories in JIRA)
10
+ - Task: Sub-work items (Sub-issues in Linear, Sub-tasks in JIRA)
11
+
12
+ All models use Pydantic v2 for validation and serialization, ensuring type safety
13
+ and consistent data handling across adapters.
14
+
15
+ Example:
16
+ >>> from mcp_ticketer.core.models import Task, Priority, TicketState
17
+ >>> task = Task(
18
+ ... title="Fix authentication bug",
19
+ ... priority=Priority.HIGH,
20
+ ... state=TicketState.IN_PROGRESS
21
+ ... )
22
+ >>> print(task.model_dump_json())
23
+ """
2
24
 
3
25
  from datetime import datetime
4
26
  from enum import Enum
@@ -8,7 +30,19 @@ from pydantic import BaseModel, ConfigDict, Field
8
30
 
9
31
 
10
32
  class Priority(str, Enum):
11
- """Universal priority levels."""
33
+ """Universal priority levels for tickets.
34
+
35
+ These priority levels are mapped to platform-specific priorities:
36
+ - Linear: 1 (Critical), 2 (High), 3 (Medium), 4 (Low)
37
+ - JIRA: Highest, High, Medium, Low
38
+ - GitHub: P0/critical, P1/high, P2/medium, P3/low labels
39
+
40
+ Attributes:
41
+ LOW: Low priority, non-urgent work
42
+ MEDIUM: Standard priority, default for most work
43
+ HIGH: High priority, should be addressed soon
44
+ CRITICAL: Critical priority, urgent work requiring immediate attention
45
+ """
12
46
 
13
47
  LOW = "low"
14
48
  MEDIUM = "medium"
@@ -17,7 +51,22 @@ class Priority(str, Enum):
17
51
 
18
52
 
19
53
  class TicketType(str, Enum):
20
- """Ticket type hierarchy."""
54
+ """Ticket type hierarchy for organizing work.
55
+
56
+ Defines the three-level hierarchy used across all platforms:
57
+
58
+ Platform Mappings:
59
+ - Linear: Project (Epic) → Issue (Issue) → Sub-issue (Task)
60
+ - JIRA: Epic (Epic) → Story/Task (Issue) → Sub-task (Task)
61
+ - GitHub: Milestone (Epic) → Issue (Issue) → Checklist item (Task)
62
+ - Aitrackdown: Epic file → Issue file → Task reference
63
+
64
+ Attributes:
65
+ EPIC: Strategic level containers for large features or initiatives
66
+ ISSUE: Standard work items, the primary unit of work
67
+ TASK: Sub-work items, smaller pieces of an issue
68
+ SUBTASK: Alias for TASK for backward compatibility
69
+ """
21
70
 
22
71
  EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
23
72
  ISSUE = "issue" # Work item level (standard issues/tasks)
@@ -26,7 +75,33 @@ class TicketType(str, Enum):
26
75
 
27
76
 
28
77
  class TicketState(str, Enum):
29
- """Universal ticket states with state machine abstraction."""
78
+ """Universal ticket states with workflow state machine.
79
+
80
+ Implements a standardized workflow that maps to different platform states:
81
+
82
+ State Flow:
83
+ OPEN → IN_PROGRESS → READY → TESTED → DONE → CLOSED
84
+ ↓ ↓ ↓
85
+ CLOSED WAITING BLOCKED
86
+ ↓ ↓
87
+ IN_PROGRESS ← IN_PROGRESS
88
+
89
+ Platform Mappings:
90
+ - Linear: Backlog (OPEN), Started (IN_PROGRESS), Completed (DONE), Canceled (CLOSED)
91
+ - JIRA: To Do (OPEN), In Progress (IN_PROGRESS), Done (DONE), etc.
92
+ - GitHub: open (OPEN), closed (CLOSED) + labels for extended states
93
+ - Aitrackdown: File-based state tracking
94
+
95
+ Attributes:
96
+ OPEN: Initial state, work not yet started
97
+ IN_PROGRESS: Work is actively being done
98
+ READY: Work is complete and ready for review/testing
99
+ TESTED: Work has been tested and verified
100
+ DONE: Work is complete and accepted
101
+ WAITING: Work is paused waiting for external dependency
102
+ BLOCKED: Work is blocked by an impediment
103
+ CLOSED: Final state, work is closed/archived
104
+ """
30
105
 
31
106
  OPEN = "open"
32
107
  IN_PROGRESS = "in_progress"
@@ -39,7 +114,14 @@ class TicketState(str, Enum):
39
114
 
40
115
  @classmethod
41
116
  def valid_transitions(cls) -> dict[str, list[str]]:
42
- """Define valid state transitions."""
117
+ """Define valid state transitions for workflow enforcement.
118
+
119
+ Returns:
120
+ Dictionary mapping each state to list of valid target states
121
+
122
+ Note:
123
+ CLOSED is a terminal state with no valid transitions
124
+ """
43
125
  return {
44
126
  cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
45
127
  cls.IN_PROGRESS: [cls.READY, cls.WAITING, cls.BLOCKED, cls.OPEN],
@@ -52,12 +134,56 @@ class TicketState(str, Enum):
52
134
  }
53
135
 
54
136
  def can_transition_to(self, target: "TicketState") -> bool:
55
- """Check if transition to target state is valid."""
137
+ """Check if transition to target state is valid.
138
+
139
+ Validates state transitions according to the defined workflow rules.
140
+ This prevents invalid state changes and ensures workflow integrity.
141
+
142
+ Args:
143
+ target: The state to transition to
144
+
145
+ Returns:
146
+ True if the transition is valid, False otherwise
147
+
148
+ Example:
149
+ >>> state = TicketState.OPEN
150
+ >>> state.can_transition_to(TicketState.IN_PROGRESS)
151
+ True
152
+ >>> state.can_transition_to(TicketState.DONE)
153
+ False
154
+ """
56
155
  return target.value in self.valid_transitions().get(self, [])
57
156
 
58
157
 
59
158
  class BaseTicket(BaseModel):
60
- """Base model for all ticket types."""
159
+ """Base model for all ticket types with universal field mapping.
160
+
161
+ Provides common fields and functionality shared across all ticket types
162
+ (Epic, Task, Comment). Uses Pydantic v2 for validation and serialization.
163
+
164
+ The metadata field allows adapters to store platform-specific information
165
+ while maintaining the universal interface.
166
+
167
+ Attributes:
168
+ id: Unique identifier assigned by the platform
169
+ title: Human-readable title (required, min 1 character)
170
+ description: Optional detailed description or body text
171
+ state: Current workflow state (defaults to OPEN)
172
+ priority: Priority level (defaults to MEDIUM)
173
+ tags: List of tags/labels for categorization
174
+ created_at: Timestamp when ticket was created
175
+ updated_at: Timestamp when ticket was last modified
176
+ metadata: Platform-specific data and field mappings
177
+
178
+ Example:
179
+ >>> ticket = BaseTicket(
180
+ ... title="Fix login issue",
181
+ ... description="Users cannot log in with SSO",
182
+ ... priority=Priority.HIGH,
183
+ ... tags=["bug", "authentication"]
184
+ ... )
185
+ >>> ticket.state = TicketState.IN_PROGRESS
186
+ """
61
187
 
62
188
  model_config = ConfigDict(use_enum_values=True)
63
189
 
@@ -77,7 +203,32 @@ class BaseTicket(BaseModel):
77
203
 
78
204
 
79
205
  class Epic(BaseTicket):
80
- """Epic - highest level container for work (Projects in Linear, Milestones in GitHub)."""
206
+ """Epic - highest level container for strategic work initiatives.
207
+
208
+ Epics represent large features, projects, or initiatives that contain
209
+ multiple related issues. They map to different concepts across platforms:
210
+
211
+ Platform Mappings:
212
+ - Linear: Projects (with issues as children)
213
+ - JIRA: Epics (with stories/tasks as children)
214
+ - GitHub: Milestones (with issues as children)
215
+ - Aitrackdown: Epic files (with issue references)
216
+
217
+ Epics sit at the top of the hierarchy and cannot have parent epics.
218
+ They can contain multiple child issues, which in turn can contain tasks.
219
+
220
+ Attributes:
221
+ ticket_type: Always TicketType.EPIC (frozen field)
222
+ child_issues: List of issue IDs that belong to this epic
223
+
224
+ Example:
225
+ >>> epic = Epic(
226
+ ... title="User Authentication System",
227
+ ... description="Complete overhaul of authentication",
228
+ ... priority=Priority.HIGH
229
+ ... )
230
+ >>> epic.child_issues = ["ISSUE-123", "ISSUE-124"]
231
+ """
81
232
 
82
233
  ticket_type: TicketType = Field(
83
234
  default=TicketType.EPIC, frozen=True, description="Always EPIC type"
@@ -89,9 +240,11 @@ class Epic(BaseTicket):
89
240
  def validate_hierarchy(self) -> list[str]:
90
241
  """Validate epic hierarchy rules.
91
242
 
92
- Returns:
93
- List of validation errors (empty if valid)
243
+ Epics are at the top of the hierarchy and have no parent constraints.
244
+ This method is provided for consistency with other ticket types.
94
245
 
246
+ Returns:
247
+ Empty list (epics have no hierarchy constraints)
95
248
  """
96
249
  # Epics don't have parents in our hierarchy
97
250
  return []
@@ -99,8 +99,23 @@ class WorkerManager:
99
99
  return False
100
100
 
101
101
  try:
102
- # Start worker in subprocess
103
- cmd = [sys.executable, "-m", "mcp_ticketer.queue.run_worker"]
102
+ # Start worker in subprocess using the same Python executable as the CLI
103
+ # This ensures the worker can import mcp_ticketer modules
104
+ python_executable = self._get_python_executable()
105
+ cmd = [python_executable, "-m", "mcp_ticketer.queue.run_worker"]
106
+
107
+ # Prepare environment for subprocess
108
+ # Ensure the subprocess gets the same environment as the parent
109
+ subprocess_env = os.environ.copy()
110
+
111
+ # Explicitly load environment variables from .env.local if it exists
112
+ env_file = Path.cwd() / ".env.local"
113
+ if env_file.exists():
114
+ logger.debug(f"Loading environment from {env_file} for subprocess")
115
+ from dotenv import dotenv_values
116
+ env_vars = dotenv_values(env_file)
117
+ subprocess_env.update(env_vars)
118
+ logger.debug(f"Added {len(env_vars)} environment variables from .env.local")
104
119
 
105
120
  # Start as background process
106
121
  process = subprocess.Popen(
@@ -108,6 +123,8 @@ class WorkerManager:
108
123
  stdout=subprocess.DEVNULL,
109
124
  stderr=subprocess.DEVNULL,
110
125
  start_new_session=True,
126
+ env=subprocess_env, # Pass environment explicitly
127
+ cwd=str(Path.cwd()), # Ensure correct working directory
111
128
  )
112
129
 
113
130
  # Save PID
@@ -256,6 +273,44 @@ class WorkerManager:
256
273
  except (OSError, ValueError):
257
274
  return None
258
275
 
276
+ def _get_python_executable(self) -> str:
277
+ """Get the correct Python executable for the worker subprocess.
278
+
279
+ This ensures the worker uses the same Python environment as the CLI,
280
+ which is critical for module imports to work correctly.
281
+
282
+ Returns:
283
+ Path to Python executable
284
+ """
285
+ # First, try to detect if we're running in a pipx environment
286
+ # by checking if the current executable is in a pipx venv
287
+ current_executable = sys.executable
288
+
289
+ # Check if we're in a pipx venv (path contains /pipx/venvs/)
290
+ if "/pipx/venvs/" in current_executable:
291
+ logger.debug(f"Using pipx Python executable: {current_executable}")
292
+ return current_executable
293
+
294
+ # Check if we can find the mcp-ticketer executable and extract its Python
295
+ import shutil
296
+ mcp_ticketer_path = shutil.which("mcp-ticketer")
297
+ if mcp_ticketer_path:
298
+ try:
299
+ # Read the shebang line to get the Python executable
300
+ with open(mcp_ticketer_path, 'r') as f:
301
+ first_line = f.readline().strip()
302
+ if first_line.startswith("#!") and "python" in first_line:
303
+ python_path = first_line[2:].strip()
304
+ if os.path.exists(python_path):
305
+ logger.debug(f"Using Python from mcp-ticketer shebang: {python_path}")
306
+ return python_path
307
+ except (OSError, IOError):
308
+ pass
309
+
310
+ # Fallback to sys.executable
311
+ logger.debug(f"Using sys.executable as fallback: {current_executable}")
312
+ return current_executable
313
+
259
314
  def _cleanup(self):
260
315
  """Clean up lock and PID files."""
261
316
  self._release_lock()
@@ -15,7 +15,12 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
  def main():
17
17
  """Run the worker process."""
18
+ import sys
19
+ import os
18
20
  logger.info("Starting standalone worker process")
21
+ logger.info(f"Worker Python executable: {sys.executable}")
22
+ logger.info(f"Worker working directory: {os.getcwd()}")
23
+ logger.info(f"Worker Python path: {sys.path[:3]}...") # Show first 3 entries
19
24
 
20
25
  try:
21
26
  # Create queue and worker
@@ -367,12 +367,17 @@ class Worker:
367
367
  if project_path:
368
368
  env_file = project_path / ".env.local"
369
369
  if env_file.exists():
370
- logger.debug(f"Loading environment from {env_file}")
370
+ logger.info(f"Worker loading environment from {env_file}")
371
371
  load_dotenv(env_file)
372
372
 
373
+ logger.info(f"Worker project_path: {project_path}")
374
+ logger.info(f"Worker current working directory: {os.getcwd()}")
375
+
373
376
  config = load_config(project_dir=project_path)
377
+ logger.info(f"Worker loaded config: {config}")
374
378
  adapters_config = config.get("adapters", {})
375
379
  adapter_config = adapters_config.get(item.adapter, {})
380
+ logger.info(f"Worker adapter config for {item.adapter}: {adapter_config}")
376
381
 
377
382
  # Add environment variables for authentication
378
383
  if item.adapter == "linear":
@@ -387,7 +392,24 @@ class Worker:
387
392
  if not adapter_config.get("email"):
388
393
  adapter_config["email"] = os.getenv("JIRA_ACCESS_USER")
389
394
 
390
- return AdapterRegistry.get_adapter(item.adapter, adapter_config)
395
+ logger.info(f"Worker final adapter config: {adapter_config}")
396
+
397
+ # Add debugging for Linear adapter specifically
398
+ if item.adapter == "linear":
399
+ import os
400
+ linear_api_key = os.getenv("LINEAR_API_KEY", "Not set")
401
+ logger.info(f"Worker LINEAR_API_KEY: {linear_api_key[:20] if linear_api_key != 'Not set' else 'Not set'}...")
402
+ logger.info(f"Worker adapter_config api_key: {adapter_config.get('api_key', 'Not set')[:20] if adapter_config.get('api_key') else 'Not set'}...")
403
+
404
+ adapter = AdapterRegistry.get_adapter(item.adapter, adapter_config)
405
+ logger.info(f"Worker created adapter: {type(adapter)} with team_id: {getattr(adapter, 'team_id_config', 'Not set')}")
406
+
407
+ # Add more debugging for Linear adapter
408
+ if item.adapter == "linear":
409
+ logger.info(f"Worker Linear adapter api_key: {getattr(adapter, 'api_key', 'Not set')[:20] if getattr(adapter, 'api_key', None) else 'Not set'}...")
410
+ logger.info(f"Worker Linear adapter team_key: {getattr(adapter, 'team_key', 'Not set')}")
411
+
412
+ return adapter
391
413
 
392
414
  async def _execute_operation(self, adapter, item: QueueItem) -> dict[str, Any]:
393
415
  """Execute the queued operation.