spec-kitty-cli 0.12.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.
Files changed (242) hide show
  1. spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
  2. spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
  3. spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
  4. spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
  5. spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
  6. specify_cli/__init__.py +171 -0
  7. specify_cli/acceptance.py +627 -0
  8. specify_cli/agent_utils/README.md +157 -0
  9. specify_cli/agent_utils/__init__.py +9 -0
  10. specify_cli/agent_utils/status.py +356 -0
  11. specify_cli/cli/__init__.py +6 -0
  12. specify_cli/cli/commands/__init__.py +46 -0
  13. specify_cli/cli/commands/accept.py +189 -0
  14. specify_cli/cli/commands/agent/__init__.py +22 -0
  15. specify_cli/cli/commands/agent/config.py +382 -0
  16. specify_cli/cli/commands/agent/context.py +191 -0
  17. specify_cli/cli/commands/agent/feature.py +1057 -0
  18. specify_cli/cli/commands/agent/release.py +11 -0
  19. specify_cli/cli/commands/agent/tasks.py +1253 -0
  20. specify_cli/cli/commands/agent/workflow.py +801 -0
  21. specify_cli/cli/commands/context.py +246 -0
  22. specify_cli/cli/commands/dashboard.py +85 -0
  23. specify_cli/cli/commands/implement.py +973 -0
  24. specify_cli/cli/commands/init.py +827 -0
  25. specify_cli/cli/commands/init_help.py +62 -0
  26. specify_cli/cli/commands/merge.py +755 -0
  27. specify_cli/cli/commands/mission.py +240 -0
  28. specify_cli/cli/commands/ops.py +265 -0
  29. specify_cli/cli/commands/orchestrate.py +640 -0
  30. specify_cli/cli/commands/repair.py +175 -0
  31. specify_cli/cli/commands/research.py +165 -0
  32. specify_cli/cli/commands/sync.py +364 -0
  33. specify_cli/cli/commands/upgrade.py +249 -0
  34. specify_cli/cli/commands/validate_encoding.py +186 -0
  35. specify_cli/cli/commands/validate_tasks.py +186 -0
  36. specify_cli/cli/commands/verify.py +310 -0
  37. specify_cli/cli/helpers.py +123 -0
  38. specify_cli/cli/step_tracker.py +91 -0
  39. specify_cli/cli/ui.py +192 -0
  40. specify_cli/core/__init__.py +53 -0
  41. specify_cli/core/agent_context.py +311 -0
  42. specify_cli/core/config.py +96 -0
  43. specify_cli/core/context_validation.py +362 -0
  44. specify_cli/core/dependency_graph.py +351 -0
  45. specify_cli/core/git_ops.py +129 -0
  46. specify_cli/core/multi_parent_merge.py +323 -0
  47. specify_cli/core/paths.py +260 -0
  48. specify_cli/core/project_resolver.py +110 -0
  49. specify_cli/core/stale_detection.py +263 -0
  50. specify_cli/core/tool_checker.py +79 -0
  51. specify_cli/core/utils.py +43 -0
  52. specify_cli/core/vcs/__init__.py +114 -0
  53. specify_cli/core/vcs/detection.py +341 -0
  54. specify_cli/core/vcs/exceptions.py +85 -0
  55. specify_cli/core/vcs/git.py +1304 -0
  56. specify_cli/core/vcs/jujutsu.py +1208 -0
  57. specify_cli/core/vcs/protocol.py +285 -0
  58. specify_cli/core/vcs/types.py +249 -0
  59. specify_cli/core/version_checker.py +261 -0
  60. specify_cli/core/worktree.py +506 -0
  61. specify_cli/dashboard/__init__.py +28 -0
  62. specify_cli/dashboard/diagnostics.py +204 -0
  63. specify_cli/dashboard/handlers/__init__.py +17 -0
  64. specify_cli/dashboard/handlers/api.py +143 -0
  65. specify_cli/dashboard/handlers/base.py +65 -0
  66. specify_cli/dashboard/handlers/features.py +390 -0
  67. specify_cli/dashboard/handlers/router.py +81 -0
  68. specify_cli/dashboard/handlers/static.py +50 -0
  69. specify_cli/dashboard/lifecycle.py +541 -0
  70. specify_cli/dashboard/scanner.py +437 -0
  71. specify_cli/dashboard/server.py +123 -0
  72. specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
  73. specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
  74. specify_cli/dashboard/static/spec-kitty.png +0 -0
  75. specify_cli/dashboard/templates/__init__.py +36 -0
  76. specify_cli/dashboard/templates/index.html +258 -0
  77. specify_cli/doc_generators.py +621 -0
  78. specify_cli/doc_state.py +408 -0
  79. specify_cli/frontmatter.py +384 -0
  80. specify_cli/gap_analysis.py +915 -0
  81. specify_cli/gitignore_manager.py +300 -0
  82. specify_cli/guards.py +145 -0
  83. specify_cli/legacy_detector.py +83 -0
  84. specify_cli/manifest.py +286 -0
  85. specify_cli/merge/__init__.py +63 -0
  86. specify_cli/merge/executor.py +653 -0
  87. specify_cli/merge/forecast.py +215 -0
  88. specify_cli/merge/ordering.py +126 -0
  89. specify_cli/merge/preflight.py +230 -0
  90. specify_cli/merge/state.py +185 -0
  91. specify_cli/merge/status_resolver.py +354 -0
  92. specify_cli/mission.py +654 -0
  93. specify_cli/missions/documentation/command-templates/implement.md +309 -0
  94. specify_cli/missions/documentation/command-templates/plan.md +275 -0
  95. specify_cli/missions/documentation/command-templates/review.md +344 -0
  96. specify_cli/missions/documentation/command-templates/specify.md +206 -0
  97. specify_cli/missions/documentation/command-templates/tasks.md +189 -0
  98. specify_cli/missions/documentation/mission.yaml +113 -0
  99. specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
  100. specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
  101. specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
  102. specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
  103. specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
  104. specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
  105. specify_cli/missions/documentation/templates/plan-template.md +269 -0
  106. specify_cli/missions/documentation/templates/release-template.md +222 -0
  107. specify_cli/missions/documentation/templates/spec-template.md +172 -0
  108. specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
  109. specify_cli/missions/documentation/templates/tasks-template.md +159 -0
  110. specify_cli/missions/research/command-templates/merge.md +388 -0
  111. specify_cli/missions/research/command-templates/plan.md +125 -0
  112. specify_cli/missions/research/command-templates/review.md +144 -0
  113. specify_cli/missions/research/command-templates/tasks.md +225 -0
  114. specify_cli/missions/research/mission.yaml +115 -0
  115. specify_cli/missions/research/templates/data-model-template.md +33 -0
  116. specify_cli/missions/research/templates/plan-template.md +161 -0
  117. specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
  118. specify_cli/missions/research/templates/research/source-register.csv +18 -0
  119. specify_cli/missions/research/templates/research-template.md +35 -0
  120. specify_cli/missions/research/templates/spec-template.md +64 -0
  121. specify_cli/missions/research/templates/task-prompt-template.md +148 -0
  122. specify_cli/missions/research/templates/tasks-template.md +114 -0
  123. specify_cli/missions/software-dev/command-templates/accept.md +75 -0
  124. specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
  125. specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
  126. specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
  127. specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
  128. specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
  129. specify_cli/missions/software-dev/command-templates/implement.md +41 -0
  130. specify_cli/missions/software-dev/command-templates/merge.md +383 -0
  131. specify_cli/missions/software-dev/command-templates/plan.md +171 -0
  132. specify_cli/missions/software-dev/command-templates/review.md +32 -0
  133. specify_cli/missions/software-dev/command-templates/specify.md +321 -0
  134. specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
  135. specify_cli/missions/software-dev/mission.yaml +100 -0
  136. specify_cli/missions/software-dev/templates/plan-template.md +132 -0
  137. specify_cli/missions/software-dev/templates/spec-template.md +116 -0
  138. specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
  139. specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
  140. specify_cli/orchestrator/__init__.py +75 -0
  141. specify_cli/orchestrator/agent_config.py +224 -0
  142. specify_cli/orchestrator/agents/__init__.py +170 -0
  143. specify_cli/orchestrator/agents/augment.py +112 -0
  144. specify_cli/orchestrator/agents/base.py +243 -0
  145. specify_cli/orchestrator/agents/claude.py +112 -0
  146. specify_cli/orchestrator/agents/codex.py +106 -0
  147. specify_cli/orchestrator/agents/copilot.py +137 -0
  148. specify_cli/orchestrator/agents/cursor.py +139 -0
  149. specify_cli/orchestrator/agents/gemini.py +115 -0
  150. specify_cli/orchestrator/agents/kilocode.py +94 -0
  151. specify_cli/orchestrator/agents/opencode.py +132 -0
  152. specify_cli/orchestrator/agents/qwen.py +96 -0
  153. specify_cli/orchestrator/config.py +455 -0
  154. specify_cli/orchestrator/executor.py +642 -0
  155. specify_cli/orchestrator/integration.py +1230 -0
  156. specify_cli/orchestrator/monitor.py +898 -0
  157. specify_cli/orchestrator/scheduler.py +832 -0
  158. specify_cli/orchestrator/state.py +508 -0
  159. specify_cli/orchestrator/testing/__init__.py +122 -0
  160. specify_cli/orchestrator/testing/availability.py +346 -0
  161. specify_cli/orchestrator/testing/fixtures.py +684 -0
  162. specify_cli/orchestrator/testing/paths.py +218 -0
  163. specify_cli/plan_validation.py +107 -0
  164. specify_cli/scripts/debug-dashboard-scan.py +61 -0
  165. specify_cli/scripts/tasks/acceptance_support.py +695 -0
  166. specify_cli/scripts/tasks/task_helpers.py +506 -0
  167. specify_cli/scripts/tasks/tasks_cli.py +848 -0
  168. specify_cli/scripts/validate_encoding.py +180 -0
  169. specify_cli/task_metadata_validation.py +274 -0
  170. specify_cli/tasks_support.py +447 -0
  171. specify_cli/template/__init__.py +47 -0
  172. specify_cli/template/asset_generator.py +206 -0
  173. specify_cli/template/github_client.py +334 -0
  174. specify_cli/template/manager.py +193 -0
  175. specify_cli/template/renderer.py +99 -0
  176. specify_cli/templates/AGENTS.md +190 -0
  177. specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
  178. specify_cli/templates/agent-file-template.md +35 -0
  179. specify_cli/templates/checklist-template.md +42 -0
  180. specify_cli/templates/claudeignore-template +58 -0
  181. specify_cli/templates/command-templates/accept.md +141 -0
  182. specify_cli/templates/command-templates/analyze.md +253 -0
  183. specify_cli/templates/command-templates/checklist.md +352 -0
  184. specify_cli/templates/command-templates/clarify.md +224 -0
  185. specify_cli/templates/command-templates/constitution.md +432 -0
  186. specify_cli/templates/command-templates/dashboard.md +175 -0
  187. specify_cli/templates/command-templates/implement.md +190 -0
  188. specify_cli/templates/command-templates/merge.md +374 -0
  189. specify_cli/templates/command-templates/plan.md +171 -0
  190. specify_cli/templates/command-templates/research.md +88 -0
  191. specify_cli/templates/command-templates/review.md +510 -0
  192. specify_cli/templates/command-templates/specify.md +321 -0
  193. specify_cli/templates/command-templates/status.md +92 -0
  194. specify_cli/templates/command-templates/tasks.md +199 -0
  195. specify_cli/templates/git-hooks/pre-commit +22 -0
  196. specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
  197. specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
  198. specify_cli/templates/plan-template.md +108 -0
  199. specify_cli/templates/spec-template.md +118 -0
  200. specify_cli/templates/task-prompt-template.md +165 -0
  201. specify_cli/templates/tasks-template.md +161 -0
  202. specify_cli/templates/vscode-settings.json +13 -0
  203. specify_cli/text_sanitization.py +225 -0
  204. specify_cli/upgrade/__init__.py +18 -0
  205. specify_cli/upgrade/detector.py +239 -0
  206. specify_cli/upgrade/metadata.py +182 -0
  207. specify_cli/upgrade/migrations/__init__.py +65 -0
  208. specify_cli/upgrade/migrations/base.py +80 -0
  209. specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
  210. specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
  211. specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
  212. specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
  213. specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
  214. specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
  215. specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
  216. specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
  217. specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
  218. specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
  219. specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
  220. specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
  221. specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
  222. specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
  223. specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
  224. specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
  225. specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
  226. specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
  227. specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
  228. specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
  229. specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
  230. specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
  231. specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
  232. specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
  233. specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
  234. specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
  235. specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
  236. specify_cli/upgrade/registry.py +121 -0
  237. specify_cli/upgrade/runner.py +284 -0
  238. specify_cli/validators/__init__.py +14 -0
  239. specify_cli/validators/paths.py +154 -0
  240. specify_cli/validators/research.py +428 -0
  241. specify_cli/verify_enhanced.py +270 -0
  242. specify_cli/workspace_context.py +224 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ VCS Detection Module
3
+ ====================
4
+
5
+ This module provides tool detection and the get_vcs() factory function.
6
+ It detects which VCS tools (git, jj) are available and returns the
7
+ appropriate implementation.
8
+
9
+ See kitty-specs/015-first-class-jujutsu-vcs-integration/contracts/vcs-protocol.py
10
+ for the factory function contract.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ from functools import lru_cache
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ from .exceptions import (
24
+ VCSBackendMismatchError,
25
+ VCSNotFoundError,
26
+ )
27
+ from .types import VCSBackend
28
+
29
+ if TYPE_CHECKING:
30
+ from .protocol import VCSProtocol
31
+
32
+
33
+ # =============================================================================
34
+ # Tool Detection Functions
35
+ # =============================================================================
36
+
37
+
38
+ @lru_cache(maxsize=1)
39
+ def is_jj_available() -> bool:
40
+ """
41
+ Check if jj is installed and working.
42
+
43
+ DISABLED: jj colocated mode is incompatible with sparse checkouts.
44
+ This function now always returns False to prevent jj detection.
45
+
46
+ Returns:
47
+ False (jj detection disabled)
48
+ """
49
+ # DISABLED: jj is not compatible with sparse checkouts
50
+ # Keeping function signature for VCS abstraction layer compatibility
51
+ return False
52
+
53
+ # Original implementation (commented out for reference):
54
+ # if shutil.which("jj") is None:
55
+ # return False
56
+ # try:
57
+ # result = subprocess.run(
58
+ # ["jj", "--version"],
59
+ # capture_output=True,
60
+ # timeout=5,
61
+ # )
62
+ # return result.returncode == 0
63
+ # except (subprocess.TimeoutExpired, OSError):
64
+ # return False
65
+
66
+
67
+ @lru_cache(maxsize=1)
68
+ def is_git_available() -> bool:
69
+ """
70
+ Check if git is installed and working.
71
+
72
+ Returns:
73
+ True if git is installed and responds to --version, False otherwise.
74
+ """
75
+ if shutil.which("git") is None:
76
+ return False
77
+ try:
78
+ result = subprocess.run(
79
+ ["git", "--version"],
80
+ capture_output=True,
81
+ timeout=5,
82
+ )
83
+ return result.returncode == 0
84
+ except (subprocess.TimeoutExpired, OSError):
85
+ return False
86
+
87
+
88
+ @lru_cache(maxsize=1)
89
+ def get_jj_version() -> str | None:
90
+ """
91
+ Get installed jj version, or None if not installed.
92
+
93
+ DISABLED: jj detection is disabled (incompatible with sparse checkouts).
94
+
95
+ Returns:
96
+ None (jj detection disabled)
97
+ """
98
+ # DISABLED: jj is not compatible with sparse checkouts
99
+ return None
100
+
101
+
102
+ @lru_cache(maxsize=1)
103
+ def get_git_version() -> str | None:
104
+ """
105
+ Get installed git version, or None if not installed.
106
+
107
+ Returns:
108
+ Version string (e.g., "2.43.0") or None if git is not available.
109
+ """
110
+ if not is_git_available():
111
+ return None
112
+ try:
113
+ result = subprocess.run(
114
+ ["git", "--version"],
115
+ capture_output=True,
116
+ timeout=5,
117
+ text=True,
118
+ )
119
+ if result.returncode != 0:
120
+ return None
121
+ # git version format: "git version 2.43.0" or "git version 2.43.0.windows.1"
122
+ output = result.stdout.strip()
123
+ match = re.search(r"git version\s+(\d+\.\d+\.\d+)", output)
124
+ if match:
125
+ return match.group(1)
126
+ # Fallback: return everything after "git version "
127
+ if "git version " in output:
128
+ return output.split("git version ")[1].strip()
129
+ return "unknown"
130
+ except (subprocess.TimeoutExpired, OSError):
131
+ return None
132
+
133
+
134
+ def detect_available_backends() -> list[VCSBackend]:
135
+ """
136
+ Detect which VCS tools are installed and available.
137
+
138
+ Returns:
139
+ List of available backends, in preference order (jj first if available).
140
+ """
141
+ backends = []
142
+ if is_jj_available():
143
+ backends.append(VCSBackend.JUJUTSU)
144
+ if is_git_available():
145
+ backends.append(VCSBackend.GIT)
146
+ return backends
147
+
148
+
149
+ # =============================================================================
150
+ # Factory Function
151
+ # =============================================================================
152
+
153
+
154
+ def _get_locked_vcs_from_feature(path: Path) -> VCSBackend | None:
155
+ """
156
+ Read VCS from feature meta.json if path is inside that feature.
157
+
158
+ Args:
159
+ path: A path that might be within a feature directory.
160
+
161
+ Returns:
162
+ The locked VCSBackend if path is inside a feature with locked VCS, None otherwise.
163
+
164
+ Note:
165
+ Only returns a locked VCS if `path` is actually inside the feature directory
166
+ (either in kitty-specs/###-feature/ or in a worktree for that feature).
167
+ Does NOT return VCS for unrelated features.
168
+ """
169
+ current = path.resolve()
170
+
171
+ # Strategy 1: Check if path is directly inside kitty-specs/###-feature/
172
+ # e.g., /repo/kitty-specs/015-feature/tasks/WP01.md
173
+ for parent in [current, *current.parents]:
174
+ if parent.parent and parent.parent.name == "kitty-specs":
175
+ # parent is a feature directory like kitty-specs/015-feature/
176
+ meta_path = parent / "meta.json"
177
+ if meta_path.is_file():
178
+ try:
179
+ meta = json.loads(meta_path.read_text())
180
+ if "vcs" in meta:
181
+ return VCSBackend(meta["vcs"])
182
+ except (json.JSONDecodeError, ValueError, OSError):
183
+ pass
184
+ # Path is in a feature dir but no valid meta.json
185
+ return None
186
+
187
+ # Strategy 2: Check if we're in a worktree for a feature
188
+ # e.g., .worktrees/015-feature-name-WP01/src/file.py
189
+ if ".worktrees" in str(current):
190
+ # Find the worktree root (direct child of .worktrees/)
191
+ worktree_root = None
192
+ for parent in [current, *current.parents]:
193
+ if parent.parent and parent.parent.name == ".worktrees":
194
+ worktree_root = parent
195
+ break
196
+
197
+ if worktree_root:
198
+ # Extract feature number from worktree name
199
+ # Pattern: ###-feature-name-WP##
200
+ worktree_name = worktree_root.name
201
+ match = re.match(r"(\d{3})-", worktree_name)
202
+ if match:
203
+ feature_num = match.group(1)
204
+ # Find main repo (parent of .worktrees)
205
+ main_repo = worktree_root.parent.parent
206
+ kitty_specs = main_repo / "kitty-specs"
207
+ if kitty_specs.is_dir():
208
+ # Find the specific feature directory matching feature_num
209
+ for feature_dir in kitty_specs.iterdir():
210
+ if feature_dir.is_dir() and feature_dir.name.startswith(
211
+ f"{feature_num}-"
212
+ ):
213
+ meta_path = feature_dir / "meta.json"
214
+ if meta_path.is_file():
215
+ try:
216
+ meta = json.loads(meta_path.read_text())
217
+ if "vcs" in meta:
218
+ return VCSBackend(meta["vcs"])
219
+ except (json.JSONDecodeError, ValueError, OSError):
220
+ pass
221
+ # Found feature dir but no valid meta.json
222
+ return None
223
+
224
+ # Path is not inside any feature
225
+ return None
226
+
227
+
228
+ def _instantiate_backend(backend: VCSBackend) -> "VCSProtocol":
229
+ """
230
+ Instantiate the appropriate VCS implementation.
231
+
232
+ Args:
233
+ backend: The backend to instantiate.
234
+
235
+ Returns:
236
+ A VCSProtocol implementation.
237
+
238
+ Raises:
239
+ VCSNotFoundError: If the requested backend is not available.
240
+ """
241
+ if backend == VCSBackend.JUJUTSU:
242
+ if not is_jj_available():
243
+ raise VCSNotFoundError(
244
+ "jj is not available. Install jj from https://github.com/martinvonz/jj"
245
+ )
246
+ # Lazy import to avoid circular imports
247
+ from .jujutsu import JujutsuVCS
248
+
249
+ return JujutsuVCS()
250
+ elif backend == VCSBackend.GIT:
251
+ if not is_git_available():
252
+ raise VCSNotFoundError("git is not available. Please install git.")
253
+ # Lazy import to avoid circular imports
254
+ from .git import GitVCS
255
+
256
+ return GitVCS()
257
+ else:
258
+ raise VCSNotFoundError(f"Unknown VCS backend: {backend}")
259
+
260
+
261
+ def get_vcs(
262
+ path: Path,
263
+ backend: VCSBackend | None = None,
264
+ prefer_jj: bool = True,
265
+ ) -> "VCSProtocol":
266
+ """
267
+ Factory function to get appropriate VCS implementation.
268
+
269
+ Args:
270
+ path: Path within a repository or feature directory.
271
+ backend: Explicit backend choice (None = auto-detect).
272
+ prefer_jj: If auto-detecting, prefer jj over git when both available.
273
+
274
+ Returns:
275
+ VCSProtocol implementation (GitVCS or JujutsuVCS).
276
+
277
+ Raises:
278
+ VCSNotFoundError: Neither jj nor git available.
279
+ VCSBackendMismatchError: Requested backend doesn't match feature's locked VCS.
280
+
281
+ Detection order:
282
+ 1. If backend specified, use that
283
+ 2. If path is in a feature, read meta.json for locked VCS
284
+ 3. If jj available and prefer_jj=True, use jj
285
+ 4. If git available, use git
286
+ 5. Raise VCSNotFoundError
287
+ """
288
+ # 1. If explicit backend specified, use that
289
+ if backend is not None:
290
+ # Check if there's a locked VCS that conflicts
291
+ locked = _get_locked_vcs_from_feature(path)
292
+ if locked is not None and locked != backend:
293
+ raise VCSBackendMismatchError(
294
+ f"Requested backend '{backend.value}' doesn't match feature's "
295
+ f"locked VCS '{locked.value}'. "
296
+ f"Features must use the same VCS throughout their lifecycle."
297
+ )
298
+ return _instantiate_backend(backend)
299
+
300
+ # 2. Check for locked VCS in feature meta.json
301
+ locked = _get_locked_vcs_from_feature(path)
302
+ if locked is not None:
303
+ return _instantiate_backend(locked)
304
+
305
+ # 3. Auto-detect based on availability
306
+ # DISABLED: jj detection disabled (incompatible with sparse checkouts)
307
+ # if prefer_jj and is_jj_available():
308
+ # # Lazy import to avoid circular imports
309
+ # from .jujutsu import JujutsuVCS
310
+ #
311
+ # return JujutsuVCS()
312
+
313
+ if is_git_available():
314
+ # Lazy import to avoid circular imports
315
+ from .git import GitVCS
316
+
317
+ return GitVCS()
318
+
319
+ # 4. git not available
320
+ raise VCSNotFoundError(
321
+ "git is not available. "
322
+ "Please install git: https://git-scm.com/downloads"
323
+ )
324
+
325
+
326
+ # =============================================================================
327
+ # Cache Management (for testing)
328
+ # =============================================================================
329
+
330
+
331
+ def _clear_detection_cache() -> None:
332
+ """
333
+ Clear the detection cache. For testing purposes only.
334
+
335
+ This clears the cached results of is_jj_available, is_git_available,
336
+ get_jj_version, and get_git_version.
337
+ """
338
+ is_jj_available.cache_clear()
339
+ is_git_available.cache_clear()
340
+ get_jj_version.cache_clear()
341
+ get_git_version.cache_clear()
@@ -0,0 +1,85 @@
1
+ """
2
+ VCS Exceptions Module
3
+ =====================
4
+
5
+ This module defines the exception hierarchy for VCS operations.
6
+ All VCS-related exceptions inherit from VCSError.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class VCSError(Exception):
13
+ """
14
+ Base exception for VCS operations.
15
+
16
+ All VCS-related exceptions inherit from this class.
17
+ """
18
+
19
+ pass
20
+
21
+
22
+ class VCSNotFoundError(VCSError):
23
+ """
24
+ Neither jj nor git is available.
25
+
26
+ Raised when attempting VCS operations but no supported
27
+ VCS tool is installed or accessible.
28
+ """
29
+
30
+ pass
31
+
32
+
33
+ class VCSCapabilityError(VCSError):
34
+ """
35
+ Operation not supported by this backend.
36
+
37
+ Raised when attempting an operation that the current
38
+ VCS backend does not support (e.g., jj undo on git).
39
+ """
40
+
41
+ pass
42
+
43
+
44
+ class VCSBackendMismatchError(VCSError):
45
+ """
46
+ Requested backend doesn't match feature's locked VCS.
47
+
48
+ Raised when explicitly requesting a backend that differs
49
+ from the VCS locked in the feature's meta.json.
50
+ """
51
+
52
+ pass
53
+
54
+
55
+ class VCSLockError(VCSError):
56
+ """
57
+ Attempted to change VCS for a feature after it was locked.
58
+
59
+ Once a feature has its VCS set (on first workspace creation),
60
+ it cannot be changed. This exception is raised on such attempts.
61
+ """
62
+
63
+ pass
64
+
65
+
66
+ class VCSConflictError(VCSError):
67
+ """
68
+ Operation blocked due to unresolved conflicts.
69
+
70
+ Raised when an operation cannot proceed because the
71
+ workspace has unresolved conflicts that must be addressed first.
72
+ """
73
+
74
+ pass
75
+
76
+
77
+ class VCSSyncError(VCSError):
78
+ """
79
+ Sync operation failed.
80
+
81
+ Raised when workspace synchronization fails due to
82
+ network issues, permissions, or other errors.
83
+ """
84
+
85
+ pass