specfact-cli 0.4.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.

Potentially problematic release.


This version of specfact-cli might be problematic. Click here for more details.

Files changed (60) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +23 -0
  3. specfact_cli/agents/analyze_agent.py +392 -0
  4. specfact_cli/agents/base.py +95 -0
  5. specfact_cli/agents/plan_agent.py +202 -0
  6. specfact_cli/agents/registry.py +176 -0
  7. specfact_cli/agents/sync_agent.py +133 -0
  8. specfact_cli/analyzers/__init__.py +10 -0
  9. specfact_cli/analyzers/code_analyzer.py +775 -0
  10. specfact_cli/cli.py +397 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +87 -0
  13. specfact_cli/commands/import_cmd.py +355 -0
  14. specfact_cli/commands/init.py +119 -0
  15. specfact_cli/commands/plan.py +1090 -0
  16. specfact_cli/commands/repro.py +172 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +24 -0
  19. specfact_cli/common/logger_setup.py +673 -0
  20. specfact_cli/common/logging_utils.py +41 -0
  21. specfact_cli/common/text_utils.py +52 -0
  22. specfact_cli/common/utils.py +48 -0
  23. specfact_cli/comparators/__init__.py +10 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +13 -0
  26. specfact_cli/generators/plan_generator.py +105 -0
  27. specfact_cli/generators/protocol_generator.py +115 -0
  28. specfact_cli/generators/report_generator.py +200 -0
  29. specfact_cli/generators/workflow_generator.py +111 -0
  30. specfact_cli/importers/__init__.py +6 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +704 -0
  33. specfact_cli/models/__init__.py +32 -0
  34. specfact_cli/models/deviation.py +105 -0
  35. specfact_cli/models/enforcement.py +150 -0
  36. specfact_cli/models/plan.py +97 -0
  37. specfact_cli/models/protocol.py +28 -0
  38. specfact_cli/modes/__init__.py +18 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/sync/__init__.py +11 -0
  42. specfact_cli/sync/repository_sync.py +279 -0
  43. specfact_cli/sync/speckit_sync.py +388 -0
  44. specfact_cli/utils/__init__.py +57 -0
  45. specfact_cli/utils/console.py +69 -0
  46. specfact_cli/utils/feature_keys.py +213 -0
  47. specfact_cli/utils/git.py +241 -0
  48. specfact_cli/utils/ide_setup.py +381 -0
  49. specfact_cli/utils/prompts.py +179 -0
  50. specfact_cli/utils/structure.py +496 -0
  51. specfact_cli/utils/yaml_utils.py +200 -0
  52. specfact_cli/validators/__init__.py +19 -0
  53. specfact_cli/validators/fsm.py +260 -0
  54. specfact_cli/validators/repro_checker.py +320 -0
  55. specfact_cli/validators/schema.py +200 -0
  56. specfact_cli-0.4.0.dist-info/METADATA +332 -0
  57. specfact_cli-0.4.0.dist-info/RECORD +60 -0
  58. specfact_cli-0.4.0.dist-info/WHEEL +4 -0
  59. specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
  60. specfact_cli-0.4.0.dist-info/licenses/LICENSE.md +55 -0
@@ -0,0 +1,32 @@
1
+ """
2
+ SpecFact CLI data models.
3
+
4
+ This package contains Pydantic models for plan bundles, protocols,
5
+ features, stories, and validation results.
6
+ """
7
+
8
+ from specfact_cli.models.deviation import Deviation, DeviationReport, DeviationSeverity, DeviationType, ValidationReport
9
+ from specfact_cli.models.enforcement import EnforcementAction, EnforcementConfig, EnforcementPreset
10
+ from specfact_cli.models.plan import Business, Feature, Idea, Metadata, PlanBundle, Product, Release, Story
11
+ from specfact_cli.models.protocol import Protocol, Transition
12
+
13
+ __all__ = [
14
+ "Business",
15
+ "Deviation",
16
+ "DeviationReport",
17
+ "DeviationSeverity",
18
+ "DeviationType",
19
+ "EnforcementAction",
20
+ "EnforcementConfig",
21
+ "EnforcementPreset",
22
+ "Feature",
23
+ "Idea",
24
+ "Metadata",
25
+ "PlanBundle",
26
+ "Product",
27
+ "Protocol",
28
+ "Release",
29
+ "Story",
30
+ "Transition",
31
+ "ValidationReport",
32
+ ]
@@ -0,0 +1,105 @@
1
+ """
2
+ Deviation tracking models.
3
+
4
+ This module defines models for tracking deviations between plans,
5
+ protocols, and actual implementation following the CLI-First specification.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+ from beartype import beartype
13
+ from icontract import ensure, require
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ class DeviationSeverity(str, Enum):
18
+ """Deviation severity level."""
19
+
20
+ HIGH = "HIGH"
21
+ MEDIUM = "MEDIUM"
22
+ LOW = "LOW"
23
+
24
+
25
+ class DeviationType(str, Enum):
26
+ """Type of deviation."""
27
+
28
+ MISSING_FEATURE = "missing_feature"
29
+ MISSING_STORY = "missing_story"
30
+ MISSING_BUSINESS_CONTEXT = "missing_business_context"
31
+ EXTRA_FEATURE = "extra_feature"
32
+ EXTRA_STORY = "extra_story"
33
+ EXTRA_IMPLEMENTATION = "extra_implementation"
34
+ MISMATCH = "mismatch"
35
+ ACCEPTANCE_DRIFT = "acceptance_drift"
36
+ FSM_MISMATCH = "fsm_mismatch"
37
+ RISK_OMISSION = "risk_omission"
38
+
39
+
40
+ class Deviation(BaseModel):
41
+ """Deviation model."""
42
+
43
+ type: DeviationType = Field(..., description="Deviation type")
44
+ severity: DeviationSeverity = Field(..., description="Severity level")
45
+ description: str = Field(..., description="Deviation description")
46
+ location: str = Field(..., description="File/module location")
47
+ fix_hint: str | None = Field(None, description="Fix suggestion")
48
+
49
+
50
+ class DeviationReport(BaseModel):
51
+ """Deviation report model."""
52
+
53
+ manual_plan: str = Field(..., description="Path to manual plan bundle")
54
+ auto_plan: str = Field(..., description="Path to auto-generated plan bundle")
55
+ deviations: list[Deviation] = Field(default_factory=list, description="All deviations")
56
+ summary: dict[str, int] = Field(default_factory=dict, description="Deviation counts by type")
57
+
58
+ @property
59
+ def total_deviations(self) -> int:
60
+ """Total number of deviations."""
61
+ return len(self.deviations)
62
+
63
+ @property
64
+ def high_count(self) -> int:
65
+ """Number of high severity deviations."""
66
+ return sum(1 for d in self.deviations if d.severity == DeviationSeverity.HIGH)
67
+
68
+ @property
69
+ def medium_count(self) -> int:
70
+ """Number of medium severity deviations."""
71
+ return sum(1 for d in self.deviations if d.severity == DeviationSeverity.MEDIUM)
72
+
73
+ @property
74
+ def low_count(self) -> int:
75
+ """Number of low severity deviations."""
76
+ return sum(1 for d in self.deviations if d.severity == DeviationSeverity.LOW)
77
+
78
+
79
+ class ValidationReport(BaseModel):
80
+ """Validation report model (for backward compatibility)."""
81
+
82
+ deviations: list[Deviation] = Field(default_factory=list, description="All deviations")
83
+ high_count: int = Field(default=0, description="Number of high severity deviations")
84
+ medium_count: int = Field(default=0, description="Number of medium severity deviations")
85
+ low_count: int = Field(default=0, description="Number of low severity deviations")
86
+ passed: bool = Field(default=True, description="Whether validation passed")
87
+
88
+ @beartype
89
+ @require(lambda deviation: isinstance(deviation, Deviation), "Must be Deviation instance")
90
+ @ensure(
91
+ lambda self: self.high_count + self.medium_count + self.low_count == len(self.deviations),
92
+ "Counts must match deviations",
93
+ )
94
+ @ensure(lambda self: self.passed == (self.high_count == 0), "Must fail if high severity deviations exist")
95
+ def add_deviation(self, deviation: Deviation) -> None:
96
+ """Add a deviation and update counts."""
97
+ self.deviations.append(deviation)
98
+
99
+ if deviation.severity == DeviationSeverity.HIGH:
100
+ self.high_count += 1
101
+ self.passed = False
102
+ elif deviation.severity == DeviationSeverity.MEDIUM:
103
+ self.medium_count += 1
104
+ elif deviation.severity == DeviationSeverity.LOW:
105
+ self.low_count += 1
@@ -0,0 +1,150 @@
1
+ """Enforcement configuration models for quality gates."""
2
+
3
+ from enum import Enum
4
+
5
+ from beartype import beartype
6
+ from icontract import ensure, require
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class EnforcementAction(str, Enum):
11
+ """Actions that can be taken when a deviation is detected."""
12
+
13
+ BLOCK = "BLOCK" # Fail the validation (exit code 1)
14
+ WARN = "WARN" # Show warning but continue (exit code 0)
15
+ LOG = "LOG" # Only log, no warning (exit code 0)
16
+
17
+
18
+ class EnforcementPreset(str, Enum):
19
+ """Predefined enforcement presets."""
20
+
21
+ MINIMAL = "minimal" # Log everything, never block
22
+ BALANCED = "balanced" # Block HIGH, warn MEDIUM, log LOW
23
+ STRICT = "strict" # Block HIGH+MEDIUM, warn LOW
24
+
25
+
26
+ class EnforcementConfig(BaseModel):
27
+ """Configuration for contract enforcement and quality gates."""
28
+
29
+ preset: EnforcementPreset = Field(default=EnforcementPreset.BALANCED, description="Enforcement preset mode")
30
+ high_action: EnforcementAction = Field(
31
+ default=EnforcementAction.BLOCK, description="Action for HIGH severity deviations"
32
+ )
33
+ medium_action: EnforcementAction = Field(
34
+ default=EnforcementAction.WARN, description="Action for MEDIUM severity deviations"
35
+ )
36
+ low_action: EnforcementAction = Field(
37
+ default=EnforcementAction.LOG, description="Action for LOW severity deviations"
38
+ )
39
+ enabled: bool = Field(default=True, description="Whether enforcement is enabled")
40
+
41
+ @classmethod
42
+ @beartype
43
+ @require(lambda preset: preset in EnforcementPreset, "Preset must be valid EnforcementPreset")
44
+ @ensure(lambda result: isinstance(result, EnforcementConfig), "Must return EnforcementConfig")
45
+ @ensure(lambda result: result.enabled is True, "Config must be enabled")
46
+ def from_preset(cls, preset: EnforcementPreset) -> "EnforcementConfig":
47
+ """
48
+ Create an enforcement config from a preset.
49
+
50
+ Args:
51
+ preset: The preset to use
52
+
53
+ Returns:
54
+ EnforcementConfig with preset values
55
+ """
56
+ if preset == EnforcementPreset.MINIMAL:
57
+ return cls(
58
+ preset=preset,
59
+ high_action=EnforcementAction.WARN,
60
+ medium_action=EnforcementAction.WARN,
61
+ low_action=EnforcementAction.LOG,
62
+ enabled=True,
63
+ )
64
+ if preset == EnforcementPreset.BALANCED:
65
+ return cls(
66
+ preset=preset,
67
+ high_action=EnforcementAction.BLOCK,
68
+ medium_action=EnforcementAction.WARN,
69
+ low_action=EnforcementAction.LOG,
70
+ enabled=True,
71
+ )
72
+ if preset == EnforcementPreset.STRICT:
73
+ return cls(
74
+ preset=preset,
75
+ high_action=EnforcementAction.BLOCK,
76
+ medium_action=EnforcementAction.BLOCK,
77
+ low_action=EnforcementAction.WARN,
78
+ enabled=True,
79
+ )
80
+ # Default to balanced
81
+ return cls.from_preset(EnforcementPreset.BALANCED)
82
+
83
+ @beartype
84
+ @require(lambda severity: isinstance(severity, str) and len(severity) > 0, "Severity must be non-empty string")
85
+ @require(lambda severity: severity.upper() in ("HIGH", "MEDIUM", "LOW"), "Severity must be HIGH/MEDIUM/LOW")
86
+ @ensure(lambda result: isinstance(result, bool), "Must return boolean")
87
+ def should_block_deviation(self, severity: str) -> bool:
88
+ """
89
+ Determine if a deviation should block execution.
90
+
91
+ Args:
92
+ severity: Deviation severity (HIGH, MEDIUM, LOW)
93
+
94
+ Returns:
95
+ True if this deviation should cause a failure (exit 1)
96
+ """
97
+ if not self.enabled:
98
+ return False
99
+
100
+ severity_upper = severity.upper()
101
+ if severity_upper == "HIGH":
102
+ return self.high_action == EnforcementAction.BLOCK
103
+ if severity_upper == "MEDIUM":
104
+ return self.medium_action == EnforcementAction.BLOCK
105
+ if severity_upper == "LOW":
106
+ return self.low_action == EnforcementAction.BLOCK
107
+ return False
108
+
109
+ @beartype
110
+ @require(lambda severity: isinstance(severity, str) and len(severity) > 0, "Severity must be non-empty string")
111
+ @require(lambda severity: severity.upper() in ("HIGH", "MEDIUM", "LOW"), "Severity must be HIGH/MEDIUM/LOW")
112
+ @ensure(lambda result: isinstance(result, EnforcementAction), "Must return EnforcementAction")
113
+ def get_action(self, severity: str) -> EnforcementAction:
114
+ """
115
+ Get the action for a given severity level.
116
+
117
+ Args:
118
+ severity: Deviation severity (HIGH, MEDIUM, LOW)
119
+
120
+ Returns:
121
+ The enforcement action to take
122
+ """
123
+ severity_upper = severity.upper()
124
+ if severity_upper == "HIGH":
125
+ return self.high_action
126
+ if severity_upper == "MEDIUM":
127
+ return self.medium_action
128
+ if severity_upper == "LOW":
129
+ return self.low_action
130
+ return EnforcementAction.LOG
131
+
132
+ @beartype
133
+ @ensure(lambda result: isinstance(result, dict), "Must return dictionary")
134
+ @ensure(
135
+ lambda result: all(isinstance(k, str) and isinstance(v, str) for k, v in result.items()),
136
+ "All keys and values must be strings",
137
+ )
138
+ @ensure(lambda result: set(result.keys()) == {"HIGH", "MEDIUM", "LOW"}, "Must have all three severity levels")
139
+ def to_summary_dict(self) -> dict[str, str]:
140
+ """
141
+ Convert config to a summary dictionary for display.
142
+
143
+ Returns:
144
+ Dictionary mapping severity to action
145
+ """
146
+ return {
147
+ "HIGH": self.high_action.value,
148
+ "MEDIUM": self.medium_action.value,
149
+ "LOW": self.low_action.value,
150
+ }
@@ -0,0 +1,97 @@
1
+ """
2
+ Plan bundle data models.
3
+
4
+ This module defines Pydantic models for development plans, features,
5
+ and stories following the CLI-First specification.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class Story(BaseModel):
16
+ """User story model following Scrum/Agile practices."""
17
+
18
+ key: str = Field(..., description="Story key (e.g., STORY-001)")
19
+ title: str = Field(..., description="Story title (user-facing value statement)")
20
+ acceptance: list[str] = Field(default_factory=list, description="Acceptance criteria")
21
+ tags: list[str] = Field(default_factory=list, description="Story tags")
22
+ story_points: int | None = Field(None, ge=0, le=100, description="Story points (complexity: 1,2,3,5,8,13,21...)")
23
+ value_points: int | None = Field(
24
+ None, ge=0, le=100, description="Value points (business value: 1,2,3,5,8,13,21...)"
25
+ )
26
+ tasks: list[str] = Field(default_factory=list, description="Implementation tasks (methods, functions)")
27
+ confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Confidence score (0.0-1.0)")
28
+ draft: bool = Field(default=False, description="Whether this is a draft story")
29
+
30
+
31
+ class Feature(BaseModel):
32
+ """Feature model."""
33
+
34
+ key: str = Field(..., description="Feature key (e.g., FEATURE-001)")
35
+ title: str = Field(..., description="Feature title")
36
+ outcomes: list[str] = Field(default_factory=list, description="Expected outcomes")
37
+ acceptance: list[str] = Field(default_factory=list, description="Acceptance criteria")
38
+ constraints: list[str] = Field(default_factory=list, description="Constraints")
39
+ stories: list[Story] = Field(default_factory=list, description="User stories")
40
+ confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Confidence score (0.0-1.0)")
41
+ draft: bool = Field(default=False, description="Whether this is a draft feature")
42
+
43
+
44
+ class Release(BaseModel):
45
+ """Release model."""
46
+
47
+ name: str = Field(..., description="Release name")
48
+ objectives: list[str] = Field(default_factory=list, description="Release objectives")
49
+ scope: list[str] = Field(default_factory=list, description="Features in scope")
50
+ risks: list[str] = Field(default_factory=list, description="Release risks")
51
+
52
+
53
+ class Product(BaseModel):
54
+ """Product definition model."""
55
+
56
+ themes: list[str] = Field(default_factory=list, description="Product themes")
57
+ releases: list[Release] = Field(default_factory=list, description="Product releases")
58
+
59
+
60
+ class Business(BaseModel):
61
+ """Business context model."""
62
+
63
+ segments: list[str] = Field(default_factory=list, description="Market segments")
64
+ problems: list[str] = Field(default_factory=list, description="Problems being solved")
65
+ solutions: list[str] = Field(default_factory=list, description="Proposed solutions")
66
+ differentiation: list[str] = Field(default_factory=list, description="Differentiation points")
67
+ risks: list[str] = Field(default_factory=list, description="Business risks")
68
+
69
+
70
+ class Idea(BaseModel):
71
+ """Initial idea model."""
72
+
73
+ title: str = Field(..., description="Idea title")
74
+ narrative: str = Field(..., description="Idea narrative")
75
+ target_users: list[str] = Field(default_factory=list, description="Target user personas")
76
+ value_hypothesis: str = Field(default="", description="Value hypothesis")
77
+ constraints: list[str] = Field(default_factory=list, description="Idea constraints")
78
+ metrics: dict[str, Any] | None = Field(None, description="Success metrics")
79
+
80
+
81
+ class Metadata(BaseModel):
82
+ """Plan bundle metadata."""
83
+
84
+ stage: str = Field(default="draft", description="Plan stage (draft, review, approved, released)")
85
+ promoted_at: str | None = Field(None, description="ISO timestamp of last promotion")
86
+ promoted_by: str | None = Field(None, description="User who performed last promotion")
87
+
88
+
89
+ class PlanBundle(BaseModel):
90
+ """Complete plan bundle model."""
91
+
92
+ version: str = Field(default="1.0", description="Plan bundle version")
93
+ idea: Idea | None = Field(None, description="Initial idea")
94
+ business: Business | None = Field(None, description="Business context")
95
+ product: Product = Field(..., description="Product definition")
96
+ features: list[Feature] = Field(default_factory=list, description="Product features")
97
+ metadata: Metadata | None = Field(None, description="Plan bundle metadata")
@@ -0,0 +1,28 @@
1
+ """
2
+ Protocol data models.
3
+
4
+ This module defines Pydantic models for FSM protocols, states,
5
+ and transitions following the CLI-First specification.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class Transition(BaseModel):
14
+ """State machine transition."""
15
+
16
+ from_state: str = Field(..., description="Source state")
17
+ on_event: str = Field(..., description="Triggering event")
18
+ to_state: str = Field(..., description="Target state")
19
+ guard: str | None = Field(None, description="Guard function name")
20
+
21
+
22
+ class Protocol(BaseModel):
23
+ """FSM protocol definition."""
24
+
25
+ states: list[str] = Field(..., description="State names")
26
+ start: str = Field(..., description="Initial state")
27
+ transitions: list[Transition] = Field(..., description="State transitions")
28
+ guards: dict[str, str] = Field(default_factory=dict, description="Guard definitions")
@@ -0,0 +1,18 @@
1
+ """
2
+ Mode detection and routing for SpecFact CLI.
3
+
4
+ This package provides operational mode detection (CI/CD vs CoPilot) and command routing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from specfact_cli.modes.detector import OperationalMode, detect_mode
10
+ from specfact_cli.modes.router import CommandRouter, RoutingResult, get_router
11
+
12
+ __all__ = [
13
+ "CommandRouter",
14
+ "OperationalMode",
15
+ "RoutingResult",
16
+ "detect_mode",
17
+ "get_router",
18
+ ]
@@ -0,0 +1,126 @@
1
+ """
2
+ Mode detection for SpecFact CLI.
3
+
4
+ This module provides automatic detection of operational mode (CI/CD vs CoPilot)
5
+ based on environment and explicit flags.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from enum import Enum
12
+
13
+ from beartype import beartype
14
+ from icontract import ensure, require
15
+
16
+
17
+ class OperationalMode(str, Enum):
18
+ """Operational modes for SpecFact CLI."""
19
+
20
+ CICD = "cicd"
21
+ COPILOT = "copilot"
22
+
23
+
24
+ @beartype
25
+ @require(lambda explicit_mode: explicit_mode is None or isinstance(explicit_mode, OperationalMode))
26
+ @ensure(lambda result: isinstance(result, OperationalMode))
27
+ def detect_mode(explicit_mode: OperationalMode | None = None) -> OperationalMode:
28
+ """
29
+ Auto-detect operational mode or use explicit override.
30
+
31
+ Priority:
32
+ 1. Explicit mode flag (highest)
33
+ 2. CoPilot API availability
34
+ 3. IDE integration (VS Code/Cursor with CoPilot)
35
+ 4. Default to CI/CD mode
36
+
37
+ Args:
38
+ explicit_mode: Explicitly specified mode (overrides auto-detection)
39
+
40
+ Returns:
41
+ Detected or explicit operational mode
42
+
43
+ Raises:
44
+ ValueError: If explicit_mode is invalid OperationalMode value
45
+ """
46
+ # 1. Check explicit flag (highest priority)
47
+ if explicit_mode is not None:
48
+ return explicit_mode
49
+
50
+ # 2. Check environment variable (SPECFACT_MODE)
51
+ env_mode = os.environ.get("SPECFACT_MODE", "").lower()
52
+ if env_mode == "copilot":
53
+ return OperationalMode.COPILOT
54
+ if env_mode == "cicd":
55
+ return OperationalMode.CICD
56
+
57
+ # 3. Check CoPilot API availability
58
+ if copilot_api_available():
59
+ return OperationalMode.COPILOT
60
+
61
+ # 4. Check IDE integration
62
+ if ide_detected() and ide_has_copilot():
63
+ return OperationalMode.COPILOT
64
+
65
+ # 5. Default to CI/CD
66
+ return OperationalMode.CICD
67
+
68
+
69
+ @beartype
70
+ @ensure(lambda result: isinstance(result, bool))
71
+ def copilot_api_available() -> bool:
72
+ """
73
+ Check if CoPilot API is available.
74
+
75
+ Returns:
76
+ True if CoPilot API is available, False otherwise
77
+ """
78
+ # Check environment variables
79
+ if os.environ.get("COPILOT_API_URL"):
80
+ return True
81
+
82
+ # Check for CoPilot token or credentials
83
+ return bool(os.environ.get("COPILOT_API_TOKEN") or os.environ.get("GITHUB_COPILOT_TOKEN"))
84
+
85
+
86
+ @beartype
87
+ @ensure(lambda result: isinstance(result, bool))
88
+ def ide_detected() -> bool:
89
+ """
90
+ Check if running in IDE (VS Code/Cursor).
91
+
92
+ Returns:
93
+ True if running in IDE, False otherwise
94
+ """
95
+ # Check VS Code
96
+ if os.environ.get("VSCODE_PID") or os.environ.get("VSCODE_INJECTION"):
97
+ return True
98
+
99
+ # Check Cursor
100
+ if os.environ.get("CURSOR_PID") or os.environ.get("CURSOR_MODE"):
101
+ return True
102
+
103
+ # Check for common IDE environment variables
104
+ return os.environ.get("TERM_PROGRAM") == "vscode"
105
+
106
+
107
+ @beartype
108
+ @ensure(lambda result: isinstance(result, bool))
109
+ def ide_has_copilot() -> bool:
110
+ """
111
+ Check if IDE has CoPilot extension enabled.
112
+
113
+ Returns:
114
+ True if IDE has CoPilot enabled, False otherwise
115
+ """
116
+ # Check for CoPilot extension environment variables
117
+ if os.environ.get("COPILOT_ENABLED") == "true":
118
+ return True
119
+
120
+ # Check for VS Code/Cursor Copilot settings
121
+ if os.environ.get("VSCODE_COPILOT_ENABLED") == "true":
122
+ return True
123
+
124
+ # Placeholder: Future implementation may check IDE configuration files
125
+ # For now, we only check environment variables
126
+ return os.environ.get("CURSOR_COPILOT_ENABLED") == "true"