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,973 @@
1
+ """Implement command - create workspace for work package implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import subprocess
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from specify_cli.cli import StepTracker
15
+ from specify_cli.core.dependency_graph import (
16
+ build_dependency_graph,
17
+ get_dependents,
18
+ parse_wp_dependencies,
19
+ )
20
+ from specify_cli.core.vcs import (
21
+ get_vcs,
22
+ VCSBackend,
23
+ VCSLockError,
24
+ )
25
+ from specify_cli.frontmatter import read_frontmatter, update_fields
26
+ from specify_cli.tasks_support import (
27
+ TaskCliError,
28
+ find_repo_root,
29
+ locate_work_package,
30
+ set_scalar,
31
+ build_document,
32
+ )
33
+ from specify_cli.workspace_context import WorkspaceContext, save_context
34
+ from specify_cli.core.multi_parent_merge import create_multi_parent_base
35
+ from specify_cli.core.context_validation import require_main_repo
36
+
37
+ console = Console()
38
+
39
+
40
+ def detect_feature_context(feature_flag: str | None = None) -> tuple[str, str]:
41
+ """Detect feature number and slug from current context.
42
+
43
+ Args:
44
+ feature_flag: Explicit feature slug from --feature flag (optional)
45
+
46
+ Returns:
47
+ Tuple of (feature_number, feature_slug)
48
+ Example: ("010", "010-workspace-per-wp")
49
+
50
+ Raises:
51
+ typer.Exit: If feature context cannot be detected
52
+ """
53
+ # Priority 1: Explicit --feature flag
54
+ if feature_flag:
55
+ match = re.match(r'^(\d{3})-(.+)$', feature_flag)
56
+ if match:
57
+ number = match.group(1)
58
+ return number, feature_flag
59
+ else:
60
+ console.print(f"[red]Error:[/red] Invalid feature format: {feature_flag}")
61
+ console.print("Expected format: ###-feature-name (e.g., 001-my-feature)")
62
+ raise typer.Exit(1)
63
+
64
+ # Priority 2: Try git branch
65
+ result = subprocess.run(
66
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
67
+ capture_output=True,
68
+ text=True,
69
+ check=False
70
+ )
71
+
72
+ if result.returncode == 0:
73
+ branch = result.stdout.strip()
74
+
75
+ # Pattern 1: WP branch (###-feature-name-WP##)
76
+ # Check this FIRST - more specific pattern
77
+ # Extract feature slug by removing -WP## suffix
78
+ match = re.match(r'^((\d{3})-.+)-WP\d{2}$', branch)
79
+ if match:
80
+ slug = match.group(1)
81
+ number = match.group(2)
82
+ return number, slug
83
+
84
+ # Pattern 2: Feature branch (###-feature-name)
85
+ match = re.match(r'^(\d{3})-(.+)$', branch)
86
+ if match:
87
+ number = match.group(1)
88
+ slug = branch
89
+ return number, slug
90
+
91
+ # Try current directory
92
+ cwd = Path.cwd()
93
+ # Look for kitty-specs/###-feature-name/ in path
94
+ for part in cwd.parts:
95
+ match = re.match(r'^(\d{3})-(.+)$', part)
96
+ if match:
97
+ number = match.group(1)
98
+ slug = part
99
+ return number, slug
100
+
101
+ # Try scanning kitty-specs/ for features (v0.11.0 workflow)
102
+ try:
103
+ repo_root = find_repo_root()
104
+ kitty_specs = repo_root / "kitty-specs"
105
+ if kitty_specs.exists():
106
+ # Find all feature directories
107
+ features = [
108
+ d.name for d in kitty_specs.iterdir()
109
+ if d.is_dir() and re.match(r'^\d{3}-', d.name)
110
+ ]
111
+
112
+ if len(features) == 1:
113
+ # Only one feature - use it automatically
114
+ match = re.match(r'^(\d{3})-(.+)$', features[0])
115
+ if match:
116
+ number = match.group(1)
117
+ slug = features[0]
118
+ return number, slug
119
+ elif len(features) > 1:
120
+ # Multiple features - need user to specify
121
+ console.print("[red]Error:[/red] Multiple features found:")
122
+ for f in sorted(features):
123
+ console.print(f" - {f}")
124
+ console.print("\nSpecify feature explicitly:")
125
+ console.print(" spec-kitty implement WP01 --feature 001-my-feature")
126
+ raise typer.Exit(1)
127
+ except TaskCliError:
128
+ # Not in a git repo, continue to generic error
129
+ pass
130
+
131
+ # Cannot detect
132
+ console.print("[red]Error:[/red] Cannot detect feature context")
133
+ console.print("Run this command from a feature branch or feature directory")
134
+ raise typer.Exit(1)
135
+
136
+
137
+ def find_wp_file(repo_root: Path, feature_slug: str, wp_id: str) -> Path:
138
+ """Find WP file in kitty-specs/###-feature/tasks/ directory.
139
+
140
+ Args:
141
+ repo_root: Repository root path
142
+ feature_slug: Feature slug (e.g., "010-workspace-per-wp")
143
+ wp_id: Work package ID (e.g., "WP01")
144
+
145
+ Returns:
146
+ Path to WP file
147
+
148
+ Raises:
149
+ FileNotFoundError: If WP file not found
150
+ """
151
+ tasks_dir = repo_root / "kitty-specs" / feature_slug / "tasks"
152
+ if not tasks_dir.exists():
153
+ raise FileNotFoundError(f"Tasks directory not found: {tasks_dir}")
154
+
155
+ # Search for WP##-*.md pattern
156
+ wp_files = list(tasks_dir.glob(f"{wp_id}-*.md"))
157
+ if not wp_files:
158
+ raise FileNotFoundError(f"WP file not found for {wp_id} in {tasks_dir}")
159
+
160
+ return wp_files[0]
161
+
162
+
163
+ def validate_workspace_path(workspace_path: Path, wp_id: str) -> bool:
164
+ """Ensure workspace path is available or reusable.
165
+
166
+ Args:
167
+ workspace_path: Path to workspace directory
168
+ wp_id: Work package ID
169
+
170
+ Returns:
171
+ True if workspace already exists and is valid (reusable)
172
+ False if workspace doesn't exist (should create)
173
+
174
+ Raises:
175
+ typer.Exit: If directory exists but is not a valid worktree
176
+ """
177
+ if not workspace_path.exists():
178
+ return False # Good - doesn't exist, should create
179
+
180
+ # Check if it's a valid git worktree
181
+ result = subprocess.run(
182
+ ["git", "rev-parse", "--git-dir"],
183
+ cwd=workspace_path,
184
+ capture_output=True,
185
+ check=False
186
+ )
187
+
188
+ if result.returncode == 0:
189
+ # Valid worktree exists
190
+ console.print(f"[cyan]Workspace for {wp_id} already exists[/cyan]")
191
+ console.print(f"Reusing: {workspace_path}")
192
+
193
+ # SECURITY CHECK: Detect symlinks to kitty-specs/ (bypass attempt)
194
+ kitty_specs_path = workspace_path / "kitty-specs"
195
+ if kitty_specs_path.is_symlink():
196
+ console.print()
197
+ console.print("[bold red]⚠️ SECURITY WARNING: kitty-specs/ is a symlink![/bold red]")
198
+ console.print(f" Target: {kitty_specs_path.resolve()}")
199
+ console.print(" This bypasses sparse-checkout isolation and can corrupt main repo state.")
200
+ console.print(f" Remove with: rm {kitty_specs_path}")
201
+ console.print()
202
+ raise typer.Exit(1)
203
+
204
+ return True # Reuse existing
205
+
206
+ # Directory exists but not a worktree
207
+ console.print(f"[red]Error:[/red] Directory exists but is not a valid worktree")
208
+ console.print(f"Path: {workspace_path}")
209
+ console.print(f"Remove manually: rm -rf {workspace_path}")
210
+ raise typer.Exit(1)
211
+
212
+
213
+ def check_base_branch_changed(workspace_path: Path, base_branch: str) -> bool:
214
+ """Check if base branch has commits not in current workspace.
215
+
216
+ Args:
217
+ workspace_path: Path to workspace directory
218
+ base_branch: Base branch name (e.g., "010-workspace-per-wp-WP01")
219
+
220
+ Returns:
221
+ True if base branch has new commits not in workspace
222
+ """
223
+ try:
224
+ # Get merge-base (common ancestor between workspace and base)
225
+ result = subprocess.run(
226
+ ["git", "merge-base", "HEAD", base_branch],
227
+ cwd=workspace_path,
228
+ capture_output=True,
229
+ text=True,
230
+ check=False,
231
+ )
232
+ if result.returncode != 0:
233
+ # Cannot determine merge-base (branches diverged too much or other issue)
234
+ return False
235
+
236
+ merge_base = result.stdout.strip()
237
+
238
+ # Get base branch tip
239
+ result = subprocess.run(
240
+ ["git", "rev-parse", base_branch],
241
+ cwd=workspace_path,
242
+ capture_output=True,
243
+ text=True,
244
+ check=False,
245
+ )
246
+ if result.returncode != 0:
247
+ return False
248
+
249
+ base_tip = result.stdout.strip()
250
+
251
+ # If merge-base != base tip, base has new commits
252
+ return merge_base != base_tip
253
+
254
+ except Exception:
255
+ # If git commands fail, assume no changes
256
+ return False
257
+
258
+
259
+ def resolve_primary_branch(repo_root: Path) -> str:
260
+ """Resolve the primary branch name (main or master).
261
+
262
+ Returns:
263
+ "main" if it exists, otherwise "master" if it exists.
264
+
265
+ Raises:
266
+ typer.Exit: If neither branch exists.
267
+ """
268
+ for candidate in ("main", "master"):
269
+ result = subprocess.run(
270
+ ["git", "rev-parse", "--verify", candidate],
271
+ cwd=repo_root,
272
+ capture_output=True,
273
+ check=False,
274
+ )
275
+ if result.returncode == 0:
276
+ return candidate
277
+
278
+ console.print("[red]Error:[/red] Neither 'main' nor 'master' branch exists.")
279
+ raise typer.Exit(1)
280
+
281
+
282
+ def display_rebase_warning(
283
+ workspace_path: Path,
284
+ wp_id: str,
285
+ base_branch: str,
286
+ feature_slug: str
287
+ ) -> None:
288
+ """Display warning about needing to rebase on changed base.
289
+
290
+ Args:
291
+ workspace_path: Path to workspace directory
292
+ wp_id: Work package ID (e.g., "WP02")
293
+ base_branch: Base branch name (e.g., "010-workspace-per-wp-WP01")
294
+ feature_slug: Feature slug (e.g., "010-workspace-per-wp")
295
+ """
296
+ console.print(f"\n[bold yellow]⚠️ Base branch {base_branch} has changed[/bold yellow]")
297
+ console.print(f"Your {wp_id} workspace may have outdated code from base\n")
298
+
299
+ console.print("[cyan]Recommended action:[/cyan]")
300
+ console.print(f" cd {workspace_path}")
301
+ console.print(f" git rebase {base_branch}")
302
+ console.print(" # Resolve any conflicts")
303
+ console.print(" git add .")
304
+ console.print(" git rebase --continue\n")
305
+
306
+ console.print("[yellow]This is a git limitation.[/yellow]")
307
+ console.print("Future jj integration will auto-rebase dependent workspaces.\n")
308
+
309
+
310
+ def check_for_dependents(
311
+ repo_root: Path,
312
+ feature_slug: str,
313
+ wp_id: str
314
+ ) -> None:
315
+ """Check if any WPs depend on this WP and warn if not yet done.
316
+
317
+ Args:
318
+ repo_root: Repository root path
319
+ feature_slug: Feature slug (e.g., "010-workspace-per-wp")
320
+ wp_id: Work package ID (e.g., "WP01")
321
+ """
322
+ feature_dir = repo_root / "kitty-specs" / feature_slug
323
+
324
+ # Build dependency graph
325
+ graph = build_dependency_graph(feature_dir)
326
+
327
+ # Get dependents
328
+ dependents = get_dependents(wp_id, graph)
329
+ if not dependents:
330
+ return # No dependents, no warnings needed
331
+
332
+ # Check if any dependents are incomplete (any lane except done)
333
+ incomplete_deps = []
334
+ for dep_id in dependents:
335
+ try:
336
+ dep_file = find_wp_file(repo_root, feature_slug, dep_id)
337
+ frontmatter, _ = read_frontmatter(dep_file)
338
+ lane = frontmatter.get("lane", "planned")
339
+
340
+ if lane in ["planned", "doing", "for_review"]:
341
+ incomplete_deps.append(dep_id)
342
+ except (FileNotFoundError, Exception):
343
+ # If we can't read the dependent's metadata, skip it
344
+ continue
345
+
346
+ if incomplete_deps:
347
+ console.print(f"\n[yellow]⚠️ Dependency Alert:[/yellow]")
348
+ console.print(f"{', '.join(incomplete_deps)} depend on {wp_id} (not yet done)")
349
+ console.print("If you modify this WP, dependent WPs will need manual rebase:")
350
+ for dep_id in incomplete_deps:
351
+ dep_workspace = f".worktrees/{feature_slug}-{dep_id}"
352
+ console.print(f" cd {dep_workspace} && git rebase {feature_slug}-{wp_id}")
353
+ console.print()
354
+
355
+
356
+ def _ensure_planning_artifacts_committed_git(
357
+ repo_root: Path,
358
+ feature_dir: Path,
359
+ feature_slug: str,
360
+ wp_id: str,
361
+ primary_branch: str,
362
+ ) -> None:
363
+ """Ensure planning artifacts are committed using git commands.
364
+
365
+ For git repos, checks that:
366
+ 1. We're on the primary branch (main/master)
367
+ 2. No uncommitted files exist in kitty-specs/$feature/
368
+
369
+ If uncommitted files exist and we're on the primary branch, auto-commits them.
370
+
371
+ Args:
372
+ repo_root: Repository root path
373
+ feature_dir: Path to feature directory (kitty-specs/###-feature/)
374
+ feature_slug: Feature slug (e.g., "001-my-feature")
375
+ wp_id: Work package ID (e.g., "WP01")
376
+ primary_branch: Primary branch name (main/master)
377
+
378
+ Raises:
379
+ typer.Exit: If not on primary branch or commit fails
380
+ """
381
+ # Check current branch
382
+ result = subprocess.run(
383
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
384
+ cwd=repo_root,
385
+ capture_output=True,
386
+ text=True,
387
+ check=False
388
+ )
389
+ current_branch = result.stdout.strip() if result.returncode == 0 else ""
390
+
391
+ # Check git status for untracked/modified files in feature directory
392
+ result = subprocess.run(
393
+ ["git", "status", "--porcelain", str(feature_dir)],
394
+ cwd=repo_root,
395
+ capture_output=True,
396
+ text=True,
397
+ check=False
398
+ )
399
+
400
+ if result.returncode == 0 and result.stdout.strip():
401
+ # Parse git status output - any file showing up needs to be committed
402
+ # Porcelain format: XY filename (X=staged, Y=working tree)
403
+ # Examples: ??(untracked), M (staged modified), MM(staged+modified), etc.
404
+ files_to_commit = []
405
+ for line in result.stdout.strip().split('\n'):
406
+ if line.strip():
407
+ # Get status code (first 2 chars) and filepath (rest after space)
408
+ if len(line) >= 3:
409
+ filepath = line[3:].strip()
410
+ # Any file with status means it's untracked, modified, or staged
411
+ # All of these should be included in the commit
412
+ files_to_commit.append(filepath)
413
+
414
+ if files_to_commit:
415
+ console.print(f"\n[cyan]Planning artifacts not committed:[/cyan]")
416
+ for f in files_to_commit:
417
+ console.print(f" {f}")
418
+
419
+ if current_branch != primary_branch:
420
+ console.print(
421
+ f"\n[red]Error:[/red] Planning artifacts must be committed on {primary_branch}."
422
+ )
423
+ console.print(f"Current branch: {current_branch}")
424
+ console.print(f"Run: git checkout {primary_branch}")
425
+ raise typer.Exit(1)
426
+
427
+ console.print(f"\n[cyan]Auto-committing to {primary_branch}...[/cyan]")
428
+
429
+ # Stage all files in feature directory
430
+ # Use -f to force-add files in kitty-specs/ which is in .gitignore
431
+ result = subprocess.run(
432
+ ["git", "add", "-f", str(feature_dir)],
433
+ cwd=repo_root,
434
+ capture_output=True,
435
+ text=True,
436
+ check=False
437
+ )
438
+ if result.returncode != 0:
439
+ console.print(f"[red]Error:[/red] Failed to stage files")
440
+ console.print(result.stderr)
441
+ raise typer.Exit(1)
442
+
443
+ # Commit with descriptive message
444
+ commit_msg = f"chore: Planning artifacts for {feature_slug}\n\nAuto-committed by spec-kitty before creating workspace for {wp_id}"
445
+ result = subprocess.run(
446
+ ["git", "commit", "-m", commit_msg],
447
+ cwd=repo_root,
448
+ capture_output=True,
449
+ text=True,
450
+ check=False
451
+ )
452
+ if result.returncode != 0:
453
+ console.print(f"[red]Error:[/red] Failed to commit")
454
+ console.print(result.stderr)
455
+ raise typer.Exit(1)
456
+
457
+ console.print(f"[green]✓[/green] Planning artifacts committed to {primary_branch}")
458
+
459
+
460
+ def _ensure_planning_artifacts_committed_jj(
461
+ repo_root: Path,
462
+ feature_dir: Path,
463
+ feature_slug: str,
464
+ wp_id: str,
465
+ primary_branch: str,
466
+ ) -> None:
467
+ """Verify planning artifacts exist for jj repos.
468
+
469
+ For jj repos, the working copy IS always a commit - there's no "uncommitted"
470
+ state like in git. We just need to verify the feature directory exists.
471
+
472
+ The user can run orchestration from any bookmark - we don't enforce being
473
+ on main. The planning artifacts just need to exist in the current revision.
474
+
475
+ Args:
476
+ repo_root: Repository root path
477
+ feature_dir: Path to feature directory (kitty-specs/###-feature/)
478
+ feature_slug: Feature slug (e.g., "001-my-feature")
479
+ wp_id: Work package ID (e.g., "WP01")
480
+ primary_branch: Primary branch name (main/master) - not enforced
481
+
482
+ Raises:
483
+ typer.Exit: If feature directory doesn't exist
484
+ """
485
+ # In jj, working copy IS a commit - no "uncommitted" state
486
+ # Just verify the feature directory exists
487
+ if not feature_dir.exists():
488
+ console.print(
489
+ f"\n[red]Error:[/red] Feature directory not found: {feature_dir}"
490
+ )
491
+ console.print("Run planning commands first (specify, plan, tasks)")
492
+ raise typer.Exit(1)
493
+
494
+ # Get current bookmark for display
495
+ result = subprocess.run(
496
+ ["jj", "log", "-r", "@", "--no-graph", "-T", "bookmarks"],
497
+ cwd=repo_root,
498
+ capture_output=True,
499
+ text=True,
500
+ check=False
501
+ )
502
+ current_bookmark = result.stdout.strip() if result.returncode == 0 else "unknown"
503
+ console.print(f"[green]✓[/green] Planning artifacts ready (on {current_bookmark or '@'})")
504
+
505
+
506
+ def _ensure_vcs_in_meta(feature_dir: Path, repo_root: Path) -> VCSBackend:
507
+ """Ensure VCS is selected and locked in meta.json.
508
+
509
+ Always locks to git (jj support removed due to sparse checkout incompatibility).
510
+
511
+ If a feature was created with jj, it will be automatically converted to git
512
+ with a warning message.
513
+
514
+ Args:
515
+ feature_dir: Path to the feature directory (kitty-specs/###-feature/)
516
+ repo_root: Repository root path (not used, but kept for compatibility)
517
+
518
+ Returns:
519
+ VCSBackend.GIT (always)
520
+
521
+ Raises:
522
+ typer.Exit: If meta.json is missing or malformed
523
+ """
524
+ meta_path = feature_dir / "meta.json"
525
+
526
+ if not meta_path.exists():
527
+ console.print(f"[red]Error:[/red] meta.json not found in {feature_dir}")
528
+ console.print("Run /spec-kitty.specify first to create feature structure")
529
+ raise typer.Exit(1)
530
+
531
+ try:
532
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
533
+ except json.JSONDecodeError as e:
534
+ console.print(f"[red]Error:[/red] Invalid JSON in meta.json: {e}")
535
+ raise typer.Exit(1)
536
+
537
+ # Check if VCS is already locked
538
+ if "vcs" in meta:
539
+ backend_str = meta["vcs"]
540
+ if backend_str == "jj":
541
+ console.print("[yellow]Warning:[/yellow] Feature was created with jj, but jj is no longer supported.")
542
+ console.print("[yellow]Converting to git...[/yellow]")
543
+ # Override to git
544
+ meta["vcs"] = "git"
545
+ meta["vcs_locked_at"] = datetime.now(timezone.utc).isoformat()
546
+ meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
547
+ return VCSBackend.GIT
548
+ # Already git
549
+ return VCSBackend.GIT
550
+
551
+ # VCS not yet locked - lock to git (only supported VCS)
552
+ meta["vcs"] = "git"
553
+ meta["vcs_locked_at"] = datetime.now(timezone.utc).isoformat()
554
+
555
+ # Write updated meta.json
556
+ meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
557
+
558
+ console.print("[cyan]→ VCS locked to git in meta.json[/cyan]")
559
+ return VCSBackend.GIT
560
+
561
+
562
+ @require_main_repo
563
+ def implement(
564
+ wp_id: str = typer.Argument(..., help="Work package ID (e.g., WP01)"),
565
+ base: str = typer.Option(None, "--base", help="Base WP to branch from (e.g., WP01)"),
566
+ feature: str = typer.Option(None, "--feature", help="Feature slug (e.g., 001-my-feature)"),
567
+ json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
568
+ ) -> None:
569
+ """Create workspace for work package implementation.
570
+
571
+ Creates a git worktree for the specified work package, branching from
572
+ main (for WPs with no dependencies) or from a base WP's branch.
573
+
574
+ Examples:
575
+ # Create workspace for WP01 (no dependencies)
576
+ spec-kitty implement WP01
577
+
578
+ # Create workspace for WP02, branching from WP01
579
+ spec-kitty implement WP02 --base WP01
580
+
581
+ # Explicit feature specification
582
+ spec-kitty implement WP01 --feature 001-my-feature
583
+
584
+ # JSON output for scripting
585
+ spec-kitty implement WP01 --json
586
+ """
587
+ # Context validation handled by @require_main_repo decorator
588
+ tracker = StepTracker(f"Implement {wp_id}")
589
+ tracker.add("detect", "Detect feature context")
590
+ tracker.add("validate", "Validate dependencies")
591
+ tracker.add("create", "Create workspace")
592
+ console.print()
593
+
594
+ # Step 1: Detect feature context
595
+ tracker.start("detect")
596
+ try:
597
+ repo_root = find_repo_root()
598
+ feature_number, feature_slug = detect_feature_context(feature)
599
+ tracker.complete("detect", f"Feature: {feature_slug}")
600
+ except (TaskCliError, typer.Exit) as exc:
601
+ tracker.error("detect", str(exc) if isinstance(exc, TaskCliError) else "failed")
602
+ console.print(tracker.render())
603
+ raise typer.Exit(1)
604
+
605
+ # Step 2: Validate dependencies
606
+ tracker.start("validate")
607
+ auto_merge_base = False # Track if we're using auto-merge
608
+ try:
609
+ # Find WP file to read dependencies
610
+ wp_file = find_wp_file(repo_root, feature_slug, wp_id)
611
+ declared_deps = parse_wp_dependencies(wp_file)
612
+
613
+ # Multi-parent dependency handling
614
+ if len(declared_deps) > 1 and base is None:
615
+ # Auto-merge mode: Create merge commit combining all dependencies
616
+ console.print(f"\n[cyan]Multi-parent dependency detected:[/cyan]")
617
+ console.print(f" {wp_id} depends on: {', '.join(declared_deps)}")
618
+ console.print(f" Auto-creating merge base combining all dependencies...")
619
+ auto_merge_base = True
620
+ # Will create merge base after validation completes
621
+
622
+ # Single dependency handling
623
+ elif len(declared_deps) == 1 and base is None:
624
+ # Suggest base for single dependency
625
+ tracker.error("validate", "missing --base flag")
626
+ console.print(tracker.render())
627
+ console.print(f"\n[red]Error:[/red] {wp_id} depends on {declared_deps[0]}")
628
+ console.print(f"\nSpecify base workspace:")
629
+ console.print(f" spec-kitty implement {wp_id} --base {declared_deps[0]}")
630
+ raise typer.Exit(1)
631
+
632
+ # If --base provided, validate it matches declared dependencies
633
+ if base:
634
+ if base not in declared_deps and declared_deps:
635
+ console.print(f"[yellow]Warning:[/yellow] {wp_id} does not declare dependency on {base}")
636
+ console.print(f"Declared dependencies: {declared_deps}")
637
+ # Allow but warn (user might know better than parser)
638
+
639
+ # Validate base workspace exists
640
+ base_workspace = repo_root / ".worktrees" / f"{feature_slug}-{base}"
641
+ if not base_workspace.exists():
642
+ tracker.error("validate", f"base workspace {base} not found")
643
+ console.print(tracker.render())
644
+ console.print(f"\n[red]Error:[/red] Base workspace {base} does not exist")
645
+ console.print(f"Implement {base} first: spec-kitty implement {base}")
646
+ raise typer.Exit(1)
647
+
648
+ # Verify it's a valid worktree
649
+ result = subprocess.run(
650
+ ["git", "rev-parse", "--git-dir"],
651
+ cwd=base_workspace,
652
+ capture_output=True,
653
+ check=False
654
+ )
655
+ if result.returncode != 0:
656
+ tracker.error("validate", f"base workspace {base} invalid")
657
+ console.print(tracker.render())
658
+ console.print(f"[red]Error:[/red] {base_workspace} exists but is not a valid worktree")
659
+ raise typer.Exit(1)
660
+
661
+ tracker.complete("validate", f"Base: {base or 'main'}")
662
+ except (FileNotFoundError, typer.Exit) as exc:
663
+ if not isinstance(exc, typer.Exit):
664
+ tracker.error("validate", str(exc))
665
+ console.print(tracker.render())
666
+ raise typer.Exit(1)
667
+
668
+ # Step 2.5: Ensure planning artifacts are committed (v0.11.0 requirement)
669
+ # All planning must happen in primary branch and be committed BEFORE worktree creation
670
+ if base is None: # Only for first WP in feature (branches from main)
671
+ try:
672
+ # Detect VCS backend early to use appropriate commands
673
+ feature_dir = repo_root / "kitty-specs" / feature_slug
674
+ if not feature_dir.exists():
675
+ console.print(f"\n[red]Error:[/red] Feature directory not found: {feature_dir}")
676
+ console.print(f"Run /spec-kitty.specify first")
677
+ raise typer.Exit(1)
678
+
679
+ # Get VCS backend (auto-detect or from meta.json)
680
+ vcs = get_vcs(repo_root)
681
+ vcs_backend = vcs.backend
682
+
683
+ primary_branch = resolve_primary_branch(repo_root)
684
+
685
+ if vcs_backend == VCSBackend.GIT:
686
+ # Git path: check branch and status using git commands
687
+ _ensure_planning_artifacts_committed_git(
688
+ repo_root, feature_dir, feature_slug, wp_id, primary_branch
689
+ )
690
+ else:
691
+ # jj path: check status and commit using jj commands
692
+ _ensure_planning_artifacts_committed_jj(
693
+ repo_root, feature_dir, feature_slug, wp_id, primary_branch
694
+ )
695
+
696
+ except typer.Exit:
697
+ raise
698
+ except Exception as e:
699
+ console.print(f"\n[red]Error:[/red] Failed to validate planning artifacts: {e}")
700
+ raise typer.Exit(1)
701
+
702
+ # Step 3: Create workspace
703
+ tracker.start("create")
704
+ try:
705
+ # Determine workspace path and branch name
706
+ workspace_name = f"{feature_slug}-{wp_id}"
707
+ workspace_path = repo_root / ".worktrees" / workspace_name
708
+ branch_name = workspace_name # Same as workspace dir name
709
+
710
+ # Ensure VCS is locked in meta.json and get the backend to use
711
+ # (do this early so we can use VCS for all operations)
712
+ feature_dir = repo_root / "kitty-specs" / feature_slug
713
+ vcs_backend = _ensure_vcs_in_meta(feature_dir, repo_root)
714
+
715
+ # Get VCS implementation
716
+ vcs = get_vcs(repo_root, backend=vcs_backend)
717
+
718
+ # Check if workspace already exists using VCS abstraction
719
+ workspace_info = vcs.get_workspace_info(workspace_path)
720
+ if workspace_info is not None:
721
+ # Workspace exists and is valid, reuse it
722
+ tracker.complete("create", f"Reused: {workspace_path}")
723
+ console.print(tracker.render())
724
+
725
+ # Use VCS abstraction for stale detection
726
+ if workspace_info.is_stale:
727
+ if base:
728
+ base_branch = f"{feature_slug}-{base}"
729
+ display_rebase_warning(workspace_path, wp_id, base_branch, feature_slug)
730
+ else:
731
+ # No explicit base, but workspace is stale (base changed)
732
+ console.print(f"\n[yellow]⚠️ Workspace is stale (base has changed)[/yellow]")
733
+ if vcs_backend == VCSBackend.JUJUTSU:
734
+ console.print("Run [bold]jj workspace update-stale[/bold] to sync")
735
+ else:
736
+ console.print(f"Consider rebasing if needed")
737
+
738
+ # Check for dependent WPs (T079)
739
+ check_for_dependents(repo_root, feature_slug, wp_id)
740
+
741
+ return
742
+
743
+ # Validate workspace path doesn't exist as a non-workspace directory
744
+ if workspace_path.exists():
745
+ console.print(f"[red]Error:[/red] Directory exists but is not a valid workspace")
746
+ console.print(f"Path: {workspace_path}")
747
+ console.print(f"Remove manually: rm -rf {workspace_path}")
748
+ raise typer.Exit(1)
749
+
750
+ # Determine base branch
751
+ if auto_merge_base:
752
+ # Multi-parent: Create merge base combining all dependencies
753
+ merge_result = create_multi_parent_base(
754
+ feature_slug=feature_slug,
755
+ wp_id=wp_id,
756
+ dependencies=declared_deps,
757
+ repo_root=repo_root,
758
+ )
759
+
760
+ if not merge_result.success:
761
+ tracker.error("create", "merge base creation failed")
762
+ console.print(tracker.render())
763
+ console.print(f"\n[red]Error:[/red] Failed to create merge base")
764
+ console.print(f"Reason: {merge_result.error}")
765
+
766
+ if merge_result.conflicts:
767
+ console.print(f"\n[yellow]Conflicts in:[/yellow]")
768
+ for conflict_file in merge_result.conflicts:
769
+ console.print(f" - {conflict_file}")
770
+ console.print(f"\n[dim]Resolve conflicts manually, then re-run implement[/dim]")
771
+
772
+ raise typer.Exit(1)
773
+
774
+ # Use merge base branch
775
+ base_branch = merge_result.branch_name
776
+
777
+ elif base is None:
778
+ # No dependencies - branch from primary branch
779
+ base_branch = resolve_primary_branch(repo_root)
780
+ else:
781
+ # Has dependencies - branch from base WP's branch
782
+ base_branch = f"{feature_slug}-{base}"
783
+
784
+ # Validate base branch/workspace exists
785
+ base_workspace_path = repo_root / ".worktrees" / f"{feature_slug}-{base}"
786
+ base_workspace_info = vcs.get_workspace_info(base_workspace_path)
787
+ if base_workspace_info is None:
788
+ tracker.error("create", f"base workspace {base} not found")
789
+ console.print(tracker.render())
790
+ console.print(f"[red]Error:[/red] Base workspace {base} does not exist")
791
+ console.print(f"Implement {base} first: spec-kitty implement {base}")
792
+ raise typer.Exit(1)
793
+
794
+ # Use the base workspace's current branch for git, or the revision for jj
795
+ if vcs_backend == VCSBackend.GIT:
796
+ if base_workspace_info.current_branch:
797
+ base_branch = base_workspace_info.current_branch
798
+ # For git, verify the branch exists
799
+ result = subprocess.run(
800
+ ["git", "rev-parse", "--verify", base_branch],
801
+ cwd=repo_root,
802
+ capture_output=True,
803
+ check=False
804
+ )
805
+ if result.returncode != 0:
806
+ tracker.error("create", f"base branch {base_branch} not found")
807
+ console.print(tracker.render())
808
+ console.print(f"[red]Error:[/red] Base branch {base_branch} does not exist")
809
+ raise typer.Exit(1)
810
+
811
+ # Create workspace using VCS abstraction
812
+ # For git: sparse_exclude excludes kitty-specs/ from worktree
813
+ # For jj: no sparse-checkout needed (jj has different isolation model)
814
+ if vcs_backend == VCSBackend.GIT:
815
+ create_result = vcs.create_workspace(
816
+ workspace_path=workspace_path,
817
+ workspace_name=workspace_name,
818
+ base_branch=base_branch,
819
+ repo_root=repo_root,
820
+ sparse_exclude=["kitty-specs/"],
821
+ )
822
+ else:
823
+ # jj workspace creation
824
+ create_result = vcs.create_workspace(
825
+ workspace_path=workspace_path,
826
+ workspace_name=workspace_name,
827
+ base_branch=base_branch,
828
+ repo_root=repo_root,
829
+ )
830
+
831
+ if not create_result.success:
832
+ tracker.error("create", "workspace creation failed")
833
+ console.print(tracker.render())
834
+ console.print(f"\n[red]Error:[/red] Failed to create workspace")
835
+ console.print(f"Error: {create_result.error}")
836
+ raise typer.Exit(1)
837
+
838
+ # For git, confirm sparse-checkout was applied
839
+ if vcs_backend == VCSBackend.GIT:
840
+ console.print("[cyan]→ Sparse-checkout configured (kitty-specs/ excluded, agents read from main)[/cyan]")
841
+
842
+ # Step 3.5: Get base commit SHA for tracking
843
+ result = subprocess.run(
844
+ ["git", "rev-parse", base_branch],
845
+ cwd=repo_root,
846
+ capture_output=True,
847
+ text=True,
848
+ check=False
849
+ )
850
+ base_commit_sha = result.stdout.strip() if result.returncode == 0 else "unknown"
851
+
852
+ # Step 3.6: Update WP frontmatter with base tracking
853
+ try:
854
+ created_at = datetime.now(timezone.utc).isoformat()
855
+
856
+ # Update frontmatter with base tracking fields
857
+ update_fields(wp_file, {
858
+ "base_branch": base_branch,
859
+ "base_commit": base_commit_sha,
860
+ "created_at": created_at,
861
+ })
862
+
863
+ console.print(f"[cyan]→ Base tracking: {base_branch} @ {base_commit_sha[:7]}[/cyan]")
864
+ except Exception as e:
865
+ console.print(f"[yellow]Warning:[/yellow] Could not update base tracking in frontmatter: {e}")
866
+
867
+ # Step 3.7: Create workspace context file
868
+ try:
869
+ # Note if this was created via multi-parent merge
870
+ created_by = "implement-command"
871
+ if auto_merge_base:
872
+ created_by = "implement-command-multi-parent-merge"
873
+
874
+ context = WorkspaceContext(
875
+ wp_id=wp_id,
876
+ feature_slug=feature_slug,
877
+ worktree_path=str(workspace_path.relative_to(repo_root)),
878
+ branch_name=branch_name,
879
+ base_branch=base_branch,
880
+ base_commit=base_commit_sha,
881
+ dependencies=declared_deps,
882
+ created_at=created_at,
883
+ created_by=created_by,
884
+ vcs_backend=vcs_backend.value,
885
+ )
886
+
887
+ context_path = save_context(repo_root, context)
888
+ console.print(f"[cyan]→ Workspace context: {context_path.relative_to(repo_root)}[/cyan]")
889
+ except Exception as e:
890
+ console.print(f"[yellow]Warning:[/yellow] Could not create workspace context: {e}")
891
+
892
+ tracker.complete("create", f"Workspace: {workspace_path.relative_to(repo_root)}")
893
+
894
+ except typer.Exit:
895
+ console.print(tracker.render())
896
+ raise
897
+
898
+ # Step 4: Update WP lane to "doing" and auto-commit to main
899
+ # This enables multi-agent synchronization - all agents see the claim immediately
900
+ try:
901
+ import os
902
+
903
+ wp = locate_work_package(repo_root, feature_slug, wp_id)
904
+
905
+ # Only update if currently planned (avoid overwriting existing doing/review state)
906
+ current_lane = wp.lane or "planned"
907
+ if current_lane == "planned":
908
+ # Capture current shell PID for audit trail
909
+ shell_pid = str(os.getppid())
910
+
911
+ # Update lane and shell_pid in frontmatter
912
+ updated_front = set_scalar(wp.frontmatter, "lane", "doing")
913
+ updated_front = set_scalar(updated_front, "shell_pid", shell_pid)
914
+
915
+ # Build and write updated document
916
+ updated_doc = build_document(updated_front, wp.body, wp.padding)
917
+ wp.path.write_text(updated_doc, encoding="utf-8")
918
+
919
+ # Auto-commit to main branch
920
+ commit_msg = f"chore: {wp_id} claimed for implementation"
921
+ commit_result = subprocess.run(
922
+ ["git", "commit", str(wp.path.resolve()), "-m", commit_msg],
923
+ cwd=repo_root,
924
+ capture_output=True,
925
+ text=True,
926
+ check=False
927
+ )
928
+
929
+ if commit_result.returncode == 0:
930
+ console.print(f"[cyan]→ {wp_id} moved to 'doing' (committed to main)[/cyan]")
931
+ else:
932
+ # Commit failed - file might be unchanged or other issue
933
+ console.print(f"[yellow]Warning:[/yellow] Could not auto-commit lane change")
934
+ if commit_result.stderr:
935
+ console.print(f" {commit_result.stderr.strip()}")
936
+
937
+ except Exception as e:
938
+ # Non-fatal: workspace created but lane update failed
939
+ console.print(f"[yellow]Warning:[/yellow] Could not update WP status: {e}")
940
+
941
+ # Success
942
+ if json_output:
943
+ # JSON output for scripting
944
+ import json
945
+ print(json.dumps({
946
+ "workspace_path": str(workspace_path.relative_to(repo_root)),
947
+ "branch": branch_name,
948
+ "feature": feature_slug,
949
+ "wp_id": wp_id,
950
+ "base": base or "main",
951
+ "status": "created"
952
+ }))
953
+ else:
954
+ # Human-readable output
955
+ console.print(tracker.render())
956
+ console.print(f"\n[bold green]✓ Workspace created successfully[/bold green]")
957
+
958
+ # Check for dependent WPs after creation (T079)
959
+ check_for_dependents(repo_root, feature_slug, wp_id)
960
+
961
+ # CRITICAL: Explicit cd instruction to prevent writing to main
962
+ console.print()
963
+ console.print("[bold yellow]" + "=" * 72 + "[/bold yellow]")
964
+ console.print("[bold yellow]CRITICAL: Change to workspace directory before making any changes![/bold yellow]")
965
+ console.print("[bold yellow]" + "=" * 72 + "[/bold yellow]")
966
+ console.print()
967
+ console.print(f" [bold]cd {workspace_path}[/bold]")
968
+ console.print()
969
+ console.print("[dim]All file edits, writes, and commits MUST happen in this directory.[/dim]")
970
+ console.print("[dim]Writing to main repository instead of the workspace is a critical error.[/dim]")
971
+
972
+
973
+ __all__ = ["implement"]