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,755 @@
1
+ """Merge command implementation.
2
+
3
+ Merges completed work packages into target branch with VCS abstraction support.
4
+ Supports both git and jujutsu backends through the VCS abstraction layer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import warnings
13
+ from pathlib import Path
14
+
15
+ import typer
16
+
17
+ from specify_cli.cli import StepTracker
18
+ from specify_cli.cli.helpers import check_version_compatibility, console, show_banner
19
+ from specify_cli.core.git_ops import run_command
20
+ from specify_cli.core.vcs import VCSBackend, get_vcs
21
+ from specify_cli.core.context_validation import require_main_repo
22
+ from specify_cli.merge.executor import execute_legacy_merge, execute_merge
23
+ from specify_cli.merge.preflight import (
24
+ display_preflight_result,
25
+ run_preflight,
26
+ )
27
+ from specify_cli.merge.state import (
28
+ MergeState,
29
+ abort_git_merge,
30
+ clear_state,
31
+ detect_git_merge_state,
32
+ get_state_path,
33
+ load_state,
34
+ )
35
+ from specify_cli.tasks_support import TaskCliError, find_repo_root
36
+
37
+
38
+ def get_main_repo_root(repo_root: Path) -> Path:
39
+ """Get the main repository root, even if called from a worktree.
40
+
41
+ If repo_root is a worktree, find its main repository.
42
+ Otherwise, return repo_root as-is.
43
+ """
44
+ git_dir = repo_root / ".git"
45
+
46
+ # If .git is a directory, we're in the main repo
47
+ if git_dir.is_dir():
48
+ return repo_root
49
+
50
+ # If .git is a file, we're in a worktree - read it to find main repo
51
+ if git_dir.is_file():
52
+ git_file_content = git_dir.read_text().strip()
53
+ # Format: "gitdir: /path/to/main/repo/.git/worktrees/feature-name"
54
+ if git_file_content.startswith("gitdir: "):
55
+ gitdir_path = Path(git_file_content[8:]) # Remove "gitdir: " prefix
56
+ # Go up from .git/worktrees/feature-name to main repo root
57
+ # gitdir_path points to: /main/repo/.git/worktrees/feature-name
58
+ # We want: /main/repo
59
+ if "worktrees" in gitdir_path.parts:
60
+ # Find the .git parent
61
+ main_git_dir = gitdir_path
62
+ while main_git_dir.name != ".git":
63
+ main_git_dir = main_git_dir.parent
64
+ if main_git_dir == main_git_dir.parent:
65
+ # Reached root without finding .git
66
+ break
67
+ return main_git_dir.parent
68
+
69
+ # Fallback: return as-is
70
+ return repo_root
71
+
72
+
73
+ def detect_worktree_structure(repo_root: Path, feature_slug: str) -> str:
74
+ """Detect if feature uses legacy or workspace-per-WP model.
75
+
76
+ Returns: "legacy", "workspace-per-wp", or "none"
77
+
78
+ IMPORTANT: This function must work correctly when called from within a worktree.
79
+ repo_root may be a worktree directory, so we need to find the main repo first.
80
+ """
81
+ # Get the main repository root (handles case where repo_root is a worktree)
82
+ main_repo = get_main_repo_root(repo_root)
83
+ worktrees_dir = main_repo / ".worktrees"
84
+
85
+ if not worktrees_dir.exists():
86
+ return "none"
87
+
88
+ # Look for workspace-per-WP pattern FIRST (takes precedence per spec)
89
+ # Pattern: .worktrees/###-feature-WP##/
90
+ wp_pattern = list(worktrees_dir.glob(f"{feature_slug}-WP*"))
91
+ if wp_pattern:
92
+ return "workspace-per-wp"
93
+
94
+ # Look for legacy pattern: .worktrees/###-feature/
95
+ legacy_pattern = worktrees_dir / feature_slug
96
+ if legacy_pattern.exists() and legacy_pattern.is_dir():
97
+ return "legacy"
98
+
99
+ return "none"
100
+
101
+
102
+ def extract_wp_id(worktree_path: Path) -> str | None:
103
+ """Extract WP ID from worktree directory name.
104
+
105
+ Example: .worktrees/010-feature-WP01/ → WP01
106
+ """
107
+ name = worktree_path.name
108
+ match = re.search(r'-(WP\d{2})$', name)
109
+ if match:
110
+ return match.group(1)
111
+ return None
112
+
113
+
114
+ def find_wp_worktrees(repo_root: Path, feature_slug: str) -> list[tuple[Path, str, str]]:
115
+ """Find all WP worktrees for a feature.
116
+
117
+ Returns: List of (worktree_path, wp_id, branch_name) tuples, sorted by WP ID.
118
+
119
+ IMPORTANT: This function must work correctly when called from within a worktree.
120
+ """
121
+ # Get the main repository root (handles case where repo_root is a worktree)
122
+ main_repo = get_main_repo_root(repo_root)
123
+ worktrees_dir = main_repo / ".worktrees"
124
+ pattern = f"{feature_slug}-WP*"
125
+
126
+ wp_worktrees = sorted(worktrees_dir.glob(pattern))
127
+
128
+ wp_workspaces = []
129
+ for wt_path in wp_worktrees:
130
+ wp_id = extract_wp_id(wt_path)
131
+ if wp_id:
132
+ branch_name = wt_path.name # Directory name = branch name
133
+ wp_workspaces.append((wt_path, wp_id, branch_name))
134
+
135
+ return wp_workspaces
136
+
137
+
138
+ def extract_feature_slug(branch_name: str) -> str:
139
+ """Extract feature slug from a WP branch name.
140
+
141
+ Example: 010-workspace-per-wp-WP01 → 010-workspace-per-wp
142
+ """
143
+ match = re.match(r'(.*?)-WP\d{2}$', branch_name)
144
+ if match:
145
+ return match.group(1)
146
+ return branch_name # Return as-is for legacy branches
147
+
148
+
149
+ def validate_wp_ready_for_merge(repo_root: Path, worktree_path: Path, branch_name: str) -> tuple[bool, str]:
150
+ """Validate WP workspace is ready to merge."""
151
+ # Check 1: Branch exists in git (check from repo root)
152
+ result = subprocess.run(
153
+ ["git", "rev-parse", "--verify", branch_name],
154
+ cwd=str(repo_root),
155
+ capture_output=True,
156
+ check=False
157
+ )
158
+ if result.returncode != 0:
159
+ return False, f"Branch {branch_name} does not exist"
160
+
161
+ # Check 2: No uncommitted changes in worktree
162
+ result = subprocess.run(
163
+ ["git", "status", "--porcelain"],
164
+ cwd=str(worktree_path),
165
+ capture_output=True,
166
+ text=True
167
+ )
168
+ if result.stdout.strip():
169
+ return False, f"Worktree {worktree_path.name} has uncommitted changes"
170
+
171
+ return True, ""
172
+
173
+
174
+ def merge_workspace_per_wp(
175
+ repo_root: Path,
176
+ merge_root: Path,
177
+ feature_slug: str,
178
+ current_branch: str,
179
+ target_branch: str,
180
+ strategy: str,
181
+ delete_branch: bool,
182
+ remove_worktree: bool,
183
+ push: bool,
184
+ dry_run: bool,
185
+ tracker: StepTracker,
186
+ resume_state: MergeState | None = None,
187
+ ) -> None:
188
+ """Handle merge for workspace-per-WP features.
189
+
190
+ IMPORTANT: repo_root may be a worktree directory. All worktree detection
191
+ and operations use get_main_repo_root() to find the actual main repository.
192
+ """
193
+ # Get the main repository root (handles case where repo_root is a worktree)
194
+ main_repo = get_main_repo_root(repo_root)
195
+
196
+ # Find all WP worktrees (this function also uses get_main_repo_root internally)
197
+ wp_workspaces = find_wp_worktrees(repo_root, feature_slug)
198
+
199
+ # Filter out already-completed WPs if resuming
200
+ if resume_state and resume_state.completed_wps:
201
+ completed_set = set(resume_state.completed_wps)
202
+ wp_workspaces = [
203
+ (wt_path, wp_id, branch)
204
+ for wt_path, wp_id, branch in wp_workspaces
205
+ if wp_id not in completed_set
206
+ ]
207
+ console.print(f"[cyan]Resuming merge:[/cyan] {len(resume_state.completed_wps)} WPs already merged")
208
+
209
+ if not wp_workspaces:
210
+ console.print(tracker.render())
211
+ console.print(f"\n[yellow]Warning:[/yellow] No WP worktrees found for feature {feature_slug}")
212
+ console.print("Feature may already be merged or not yet implemented")
213
+ raise typer.Exit(1)
214
+
215
+ console.print(f"\n[cyan]Workspace-per-WP feature detected:[/cyan] {len(wp_workspaces)} work packages")
216
+ for wt_path, wp_id, branch in wp_workspaces:
217
+ console.print(f" - {wp_id}: {branch}")
218
+
219
+ # Validate all WP workspaces are ready
220
+ console.print(f"\n[cyan]Validating all WP workspaces...[/cyan]")
221
+ errors = []
222
+ for wt_path, wp_id, branch in wp_workspaces:
223
+ is_valid, error_msg = validate_wp_ready_for_merge(main_repo, wt_path, branch)
224
+ if not is_valid:
225
+ errors.append(f" - {wp_id}: {error_msg}")
226
+
227
+ if errors:
228
+ tracker.error("verify", "WP workspaces not ready")
229
+ console.print(tracker.render())
230
+ console.print(f"\n[red]Cannot merge:[/red] WP workspaces not ready")
231
+ for err in errors:
232
+ console.print(err)
233
+ raise typer.Exit(1)
234
+
235
+ console.print(f"[green]✓[/green] All WP workspaces validated")
236
+
237
+ # Dry run: show what would be done
238
+ if dry_run:
239
+ console.print(tracker.render())
240
+ console.print("\n[cyan]Dry run - would execute:[/cyan]")
241
+ steps = [
242
+ f"git checkout {target_branch}",
243
+ "git pull --ff-only",
244
+ ]
245
+ for wt_path, wp_id, branch in wp_workspaces:
246
+ if strategy == "squash":
247
+ steps.extend([
248
+ f"git merge --squash {branch}",
249
+ f"git commit -m 'Merge {wp_id} from {feature_slug}'",
250
+ ])
251
+ else:
252
+ steps.append(f"git merge --no-ff {branch} -m 'Merge {wp_id} from {feature_slug}'")
253
+
254
+ if push:
255
+ steps.append(f"git push origin {target_branch}")
256
+
257
+ if remove_worktree:
258
+ for wt_path, wp_id, branch in wp_workspaces:
259
+ steps.append(f"git worktree remove {wt_path}")
260
+
261
+ if delete_branch:
262
+ for wt_path, wp_id, branch in wp_workspaces:
263
+ steps.append(f"git branch -d {branch}")
264
+
265
+ for idx, step in enumerate(steps, start=1):
266
+ console.print(f" {idx}. {step}")
267
+ return
268
+
269
+ # Checkout and update target branch
270
+ tracker.start("checkout")
271
+ try:
272
+ console.print(f"[cyan]Operating from {merge_root}[/cyan]")
273
+ os.chdir(merge_root)
274
+ _, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
275
+ if target_status.strip():
276
+ raise RuntimeError(f"Target repository at {merge_root} has uncommitted changes.")
277
+ run_command(["git", "checkout", target_branch])
278
+ tracker.complete("checkout", f"using {merge_root}")
279
+ except Exception as exc:
280
+ tracker.error("checkout", str(exc))
281
+ console.print(tracker.render())
282
+ raise typer.Exit(1)
283
+
284
+ tracker.start("pull")
285
+ try:
286
+ run_command(["git", "pull", "--ff-only"])
287
+ tracker.complete("pull")
288
+ except Exception as exc:
289
+ tracker.error("pull", str(exc))
290
+ console.print(tracker.render())
291
+ console.print(f"\n[yellow]Warning:[/yellow] Could not fast-forward {target_branch}.")
292
+ console.print("You may need to resolve conflicts manually.")
293
+ raise typer.Exit(1)
294
+
295
+ # Merge all WP branches
296
+ tracker.start("merge")
297
+ try:
298
+ for wt_path, wp_id, branch in wp_workspaces:
299
+ console.print(f"[cyan]Merging {wp_id} ({branch})...[/cyan]")
300
+
301
+ if strategy == "squash":
302
+ run_command(["git", "merge", "--squash", branch])
303
+ run_command(["git", "commit", "-m", f"Merge {wp_id} from {feature_slug}"])
304
+ elif strategy == "rebase":
305
+ console.print("\n[yellow]Note:[/yellow] Rebase strategy not supported for workspace-per-WP.")
306
+ console.print("Use 'merge' or 'squash' strategy instead.")
307
+ tracker.skip("merge", "rebase not supported for workspace-per-WP")
308
+ console.print(tracker.render())
309
+ raise typer.Exit(1)
310
+ else: # merge (default)
311
+ run_command(["git", "merge", "--no-ff", branch, "-m", f"Merge {wp_id} from {feature_slug}"])
312
+
313
+ console.print(f"[green]✓[/green] {wp_id} merged")
314
+
315
+ tracker.complete("merge", f"merged {len(wp_workspaces)} work packages")
316
+ except Exception as exc:
317
+ tracker.error("merge", str(exc))
318
+ console.print(tracker.render())
319
+ console.print(f"\n[red]Merge failed.[/red] Resolve conflicts and try again.")
320
+ raise typer.Exit(1)
321
+
322
+ # Push if requested
323
+ if push:
324
+ tracker.start("push")
325
+ try:
326
+ run_command(["git", "push", "origin", target_branch])
327
+ tracker.complete("push")
328
+ except Exception as exc:
329
+ tracker.error("push", str(exc))
330
+ console.print(tracker.render())
331
+ console.print(f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed.")
332
+ console.print(f"Run manually: git push origin {target_branch}")
333
+
334
+ # Remove worktrees
335
+ if remove_worktree:
336
+ tracker.start("worktree")
337
+ failed_removals = []
338
+ for wt_path, wp_id, branch in wp_workspaces:
339
+ try:
340
+ run_command(["git", "worktree", "remove", str(wt_path), "--force"])
341
+ console.print(f"[green]✓[/green] Removed worktree: {wp_id}")
342
+ except Exception as exc:
343
+ failed_removals.append((wp_id, wt_path))
344
+
345
+ if failed_removals:
346
+ tracker.error("worktree", f"could not remove {len(failed_removals)} worktrees")
347
+ console.print(tracker.render())
348
+ console.print(f"\n[yellow]Warning:[/yellow] Could not remove some worktrees:")
349
+ for wp_id, wt_path in failed_removals:
350
+ console.print(f" {wp_id}: git worktree remove {wt_path}")
351
+ else:
352
+ tracker.complete("worktree", f"removed {len(wp_workspaces)} worktrees")
353
+
354
+ # Delete branches
355
+ if delete_branch:
356
+ tracker.start("branch")
357
+ failed_deletions = []
358
+ for wt_path, wp_id, branch in wp_workspaces:
359
+ try:
360
+ run_command(["git", "branch", "-d", branch])
361
+ console.print(f"[green]✓[/green] Deleted branch: {branch}")
362
+ except Exception:
363
+ # Try force delete
364
+ try:
365
+ run_command(["git", "branch", "-D", branch])
366
+ console.print(f"[green]✓[/green] Force deleted branch: {branch}")
367
+ except Exception:
368
+ failed_deletions.append((wp_id, branch))
369
+
370
+ if failed_deletions:
371
+ tracker.error("branch", f"could not delete {len(failed_deletions)} branches")
372
+ console.print(tracker.render())
373
+ console.print(f"\n[yellow]Warning:[/yellow] Could not delete some branches:")
374
+ for wp_id, branch in failed_deletions:
375
+ console.print(f" {wp_id}: git branch -D {branch}")
376
+ else:
377
+ tracker.complete("branch", f"deleted {len(wp_workspaces)} branches")
378
+
379
+ console.print(tracker.render())
380
+ console.print(f"\n[bold green]✓ Feature {feature_slug} ({len(wp_workspaces)} WPs) successfully merged into {target_branch}[/bold green]")
381
+
382
+
383
+ @require_main_repo
384
+ def merge(
385
+ strategy: str = typer.Option("merge", "--strategy", help="Merge strategy: merge, squash, or rebase"),
386
+ delete_branch: bool = typer.Option(True, "--delete-branch/--keep-branch", help="Delete feature branch after merge"),
387
+ remove_worktree: bool = typer.Option(True, "--remove-worktree/--keep-worktree", help="Remove feature worktree after merge"),
388
+ push: bool = typer.Option(False, "--push", help="Push to origin after merge"),
389
+ target_branch: str = typer.Option("main", "--target", help="Target branch to merge into"),
390
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without executing"),
391
+ feature: str = typer.Option(None, "--feature", help="Feature slug when merging from main branch"),
392
+ resume: bool = typer.Option(False, "--resume", help="Resume an interrupted merge from saved state"),
393
+ abort: bool = typer.Option(False, "--abort", help="Abort and clear merge state"),
394
+ ) -> None:
395
+ """Merge a completed feature branch into the target branch and clean up resources.
396
+
397
+ For workspace-per-WP features (0.11.0+), merges all WP branches
398
+ (010-feature-WP01, 010-feature-WP02, etc.) to main in sequence.
399
+
400
+ For legacy features (0.10.x), merges single feature branch.
401
+
402
+ Use --resume to continue an interrupted merge from saved state.
403
+ Use --abort to clear merge state and abort any in-progress git merge.
404
+ """
405
+ show_banner()
406
+
407
+ # Handle --abort flag early (before any other processing)
408
+ if abort:
409
+ try:
410
+ repo_root = find_repo_root()
411
+ except TaskCliError as exc:
412
+ console.print(f"[red]Error:[/red] {exc}")
413
+ raise typer.Exit(1)
414
+
415
+ main_repo = get_main_repo_root(repo_root)
416
+ state = load_state(main_repo)
417
+
418
+ if state is None:
419
+ console.print("[yellow]No merge state to abort[/yellow]")
420
+ else:
421
+ clear_state(main_repo)
422
+ console.print(f"[green]✓[/green] Merge state cleared for {state.feature_slug}")
423
+ console.print(f" Progress was: {len(state.completed_wps)}/{len(state.wp_order)} WPs complete")
424
+
425
+ # Also abort git merge if in progress
426
+ if abort_git_merge(main_repo):
427
+ console.print("[green]✓[/green] Git merge aborted")
428
+
429
+ raise typer.Exit(0)
430
+
431
+ # Handle --resume flag
432
+ resume_state: MergeState | None = None
433
+ if resume:
434
+ try:
435
+ repo_root = find_repo_root()
436
+ except TaskCliError as exc:
437
+ console.print(f"[red]Error:[/red] {exc}")
438
+ raise typer.Exit(1)
439
+
440
+ main_repo = get_main_repo_root(repo_root)
441
+ resume_state = load_state(main_repo)
442
+
443
+ if resume_state is None:
444
+ state_path = get_state_path(main_repo)
445
+ if state_path.exists():
446
+ clear_state(main_repo)
447
+ console.print("[yellow]⚠ Invalid merge state file cleared[/yellow]")
448
+ console.print("[red]Error:[/red] No merge state to resume")
449
+ console.print("Run 'spec-kitty merge --feature <slug>' to start a new merge.")
450
+ raise typer.Exit(1)
451
+
452
+ console.print(f"[cyan]Resuming merge of {resume_state.feature_slug}[/cyan]")
453
+ console.print(f" Progress: {len(resume_state.completed_wps)}/{len(resume_state.wp_order)} WPs")
454
+ console.print(f" Remaining: {', '.join(resume_state.remaining_wps)}")
455
+
456
+ # Check for pending git merge
457
+ if detect_git_merge_state(main_repo):
458
+ console.print("[yellow]⚠ Git merge in progress - resolve conflicts first[/yellow]")
459
+ console.print("Then run 'spec-kitty merge --resume' again.")
460
+ raise typer.Exit(1)
461
+
462
+ # Set feature from state and override options
463
+ feature = resume_state.feature_slug
464
+ target_branch = resume_state.target_branch
465
+ strategy = resume_state.strategy
466
+
467
+ tracker = StepTracker("Feature Merge")
468
+ tracker.add("detect", "Detect current feature and branch")
469
+ tracker.add("preflight", "Pre-flight validation")
470
+ tracker.add("verify", "Verify merge readiness")
471
+ tracker.add("checkout", f"Switch to {target_branch}")
472
+ tracker.add("pull", f"Update {target_branch}")
473
+ tracker.add("merge", "Merge feature branch")
474
+ if push: tracker.add("push", "Push to origin")
475
+ if remove_worktree: tracker.add("worktree", "Remove feature worktree")
476
+ if delete_branch: tracker.add("branch", "Delete feature branch")
477
+ console.print()
478
+
479
+ try:
480
+ repo_root = find_repo_root()
481
+ except TaskCliError as exc:
482
+ console.print(f"[red]Error:[/red] {exc}")
483
+ raise typer.Exit(1)
484
+
485
+ check_version_compatibility(repo_root, "merge")
486
+
487
+ # Detect VCS backend
488
+ try:
489
+ vcs = get_vcs(repo_root)
490
+ vcs_backend = vcs.backend
491
+ except Exception:
492
+ # Fall back to git if VCS detection fails
493
+ vcs_backend = VCSBackend.GIT
494
+
495
+ # Show VCS backend info
496
+ console.print(f"[dim]VCS Backend: git[/dim]")
497
+
498
+ feature_worktree_path = merge_root = repo_root
499
+ tracker.start("detect")
500
+ try:
501
+ _, current_branch, _ = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture=True)
502
+ if current_branch == target_branch:
503
+ # Check if --feature flag was provided
504
+ if feature:
505
+ # Validate feature exists by checking for worktrees
506
+ main_repo = get_main_repo_root(repo_root)
507
+ worktrees_dir = main_repo / ".worktrees"
508
+ wp_pattern = list(worktrees_dir.glob(f"{feature}-WP*")) if worktrees_dir.exists() else []
509
+
510
+ if not wp_pattern:
511
+ tracker.error("detect", f"no WP worktrees found for {feature}")
512
+ console.print(tracker.render())
513
+ console.print(f"\n[red]Error:[/red] No WP worktrees found for feature '{feature}'.")
514
+ console.print("Check the feature slug or create workspaces first.")
515
+ raise typer.Exit(1)
516
+
517
+ # Use the provided feature slug and continue
518
+ feature_slug = feature
519
+ tracker.complete("detect", f"using --feature {feature_slug}")
520
+
521
+ # Get WP workspaces for preflight and merge
522
+ wp_workspaces = find_wp_worktrees(repo_root, feature_slug)
523
+
524
+ # Run preflight checks
525
+ tracker.skip("verify", "handled in preflight")
526
+ tracker.start("preflight")
527
+ preflight_result = run_preflight(
528
+ feature_slug=feature_slug,
529
+ target_branch=target_branch,
530
+ repo_root=main_repo,
531
+ wp_workspaces=wp_workspaces,
532
+ )
533
+ display_preflight_result(preflight_result, console)
534
+
535
+ if not preflight_result.passed:
536
+ tracker.error("preflight", "validation failed")
537
+ console.print(tracker.render())
538
+ raise typer.Exit(1)
539
+ tracker.complete("preflight", "all checks passed")
540
+
541
+ # Proceed directly to workspace-per-wp merge
542
+ merge_workspace_per_wp(
543
+ repo_root=repo_root,
544
+ merge_root=merge_root,
545
+ feature_slug=feature_slug,
546
+ current_branch=current_branch,
547
+ target_branch=target_branch,
548
+ strategy=strategy,
549
+ delete_branch=delete_branch,
550
+ remove_worktree=remove_worktree,
551
+ push=push,
552
+ dry_run=dry_run,
553
+ tracker=tracker,
554
+ resume_state=resume_state,
555
+ )
556
+ return
557
+ else:
558
+ tracker.error("detect", f"already on {target_branch}")
559
+ console.print(tracker.render())
560
+ console.print(f"\n[red]Error:[/red] Already on {target_branch} branch.")
561
+ console.print("Use --feature <slug> to specify the feature to merge.")
562
+ raise typer.Exit(1)
563
+
564
+ _, git_dir_output, _ = run_command(["git", "rev-parse", "--git-dir"], capture=True)
565
+ git_dir_path = Path(git_dir_output).resolve()
566
+ in_worktree = "worktrees" in git_dir_path.parts
567
+ if in_worktree:
568
+ merge_root = git_dir_path.parents[2]
569
+ if not merge_root.exists():
570
+ raise RuntimeError(f"Primary repository path not found: {merge_root}")
571
+ tracker.complete(
572
+ "detect",
573
+ f"on {current_branch}" + (f" (worktree → operating from {merge_root})" if in_worktree else ""),
574
+ )
575
+ except Exception as exc:
576
+ tracker.error("detect", str(exc))
577
+ console.print(tracker.render())
578
+ raise typer.Exit(1)
579
+
580
+ # Detect workspace structure and extract feature slug
581
+ feature_slug = extract_feature_slug(current_branch)
582
+ structure = detect_worktree_structure(repo_root, feature_slug)
583
+
584
+ # Branch to workspace-per-WP merge if detected
585
+ if structure == "workspace-per-wp":
586
+ tracker.skip("verify", "handled in preflight")
587
+ # Get main repo for preflight
588
+ main_repo = get_main_repo_root(repo_root)
589
+ wp_workspaces = find_wp_worktrees(repo_root, feature_slug)
590
+
591
+ # Run preflight checks
592
+ tracker.start("preflight")
593
+ preflight_result = run_preflight(
594
+ feature_slug=feature_slug,
595
+ target_branch=target_branch,
596
+ repo_root=main_repo,
597
+ wp_workspaces=wp_workspaces,
598
+ )
599
+ display_preflight_result(preflight_result, console)
600
+
601
+ if not preflight_result.passed:
602
+ tracker.error("preflight", "validation failed")
603
+ console.print(tracker.render())
604
+ raise typer.Exit(1)
605
+ tracker.complete("preflight", "all checks passed")
606
+
607
+ merge_workspace_per_wp(
608
+ repo_root=repo_root,
609
+ merge_root=merge_root,
610
+ feature_slug=feature_slug,
611
+ current_branch=current_branch,
612
+ target_branch=target_branch,
613
+ strategy=strategy,
614
+ delete_branch=delete_branch,
615
+ remove_worktree=remove_worktree,
616
+ push=push,
617
+ dry_run=dry_run,
618
+ tracker=tracker,
619
+ resume_state=resume_state,
620
+ )
621
+ return
622
+
623
+ # Continue with legacy merge logic for single worktree
624
+ # Skip preflight for legacy merges (single worktree validation is done above in verify step)
625
+ tracker.skip("preflight", "legacy single-worktree merge")
626
+ tracker.start("verify")
627
+ try:
628
+ _, status_output, _ = run_command(["git", "status", "--porcelain"], capture=True)
629
+ if status_output.strip():
630
+ tracker.error("verify", "uncommitted changes")
631
+ console.print(tracker.render())
632
+ console.print(f"\n[red]Error:[/red] Working directory has uncommitted changes.")
633
+ console.print("Commit or stash your changes before merging.")
634
+ raise typer.Exit(1)
635
+ tracker.complete("verify", "clean working directory")
636
+ except Exception as exc:
637
+ tracker.error("verify", str(exc))
638
+ console.print(tracker.render())
639
+ raise typer.Exit(1)
640
+
641
+ merge_root, feature_worktree_path = merge_root.resolve(), feature_worktree_path.resolve()
642
+ if dry_run:
643
+ console.print(tracker.render())
644
+ console.print("\n[cyan]Dry run - would execute:[/cyan]")
645
+ checkout_prefix = f"(from {merge_root}) " if in_worktree else ""
646
+ steps = [
647
+ f"{checkout_prefix}git checkout {target_branch}",
648
+ "git pull --ff-only",
649
+ ]
650
+ if strategy == "squash":
651
+ steps.extend([
652
+ f"git merge --squash {current_branch}",
653
+ f"git commit -m 'Merge feature {current_branch}'",
654
+ ])
655
+ elif strategy == "rebase":
656
+ steps.append(f"git merge --ff-only {current_branch} (after rebase)")
657
+ else:
658
+ steps.append(f"git merge --no-ff {current_branch}")
659
+ if push:
660
+ steps.append(f"git push origin {target_branch}")
661
+ if in_worktree and remove_worktree:
662
+ steps.append(f"git worktree remove {feature_worktree_path}")
663
+ if delete_branch:
664
+ steps.append(f"git branch -d {current_branch}")
665
+ for idx, step in enumerate(steps, start=1):
666
+ console.print(f" {idx}. {step}")
667
+ return
668
+
669
+ tracker.start("checkout")
670
+ try:
671
+ if in_worktree:
672
+ console.print(f"[cyan]Detected worktree. Merge operations will run from {merge_root}[/cyan]")
673
+ os.chdir(merge_root)
674
+ _, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
675
+ if target_status.strip():
676
+ raise RuntimeError(f"Target repository at {merge_root} has uncommitted changes.")
677
+ run_command(["git", "checkout", target_branch])
678
+ tracker.complete("checkout", f"using {merge_root}")
679
+ except Exception as exc:
680
+ tracker.error("checkout", str(exc))
681
+ console.print(tracker.render())
682
+ raise typer.Exit(1)
683
+
684
+ tracker.start("pull")
685
+ try:
686
+ run_command(["git", "pull", "--ff-only"])
687
+ tracker.complete("pull")
688
+ except Exception as exc:
689
+ tracker.error("pull", str(exc))
690
+ console.print(tracker.render())
691
+ console.print(f"\n[yellow]Warning:[/yellow] Could not fast-forward {target_branch}.")
692
+ console.print("You may need to resolve conflicts manually.")
693
+ raise typer.Exit(1)
694
+
695
+ tracker.start("merge")
696
+ try:
697
+ if strategy == "squash":
698
+ run_command(["git", "merge", "--squash", current_branch])
699
+ run_command(["git", "commit", "-m", f"Merge feature {current_branch}"])
700
+ tracker.complete("merge", "squashed")
701
+ elif strategy == "rebase":
702
+ console.print("\n[yellow]Note:[/yellow] Rebase strategy requires manual intervention.")
703
+ console.print(f"Please run: git checkout {current_branch} && git rebase {target_branch}")
704
+ tracker.skip("merge", "requires manual rebase")
705
+ console.print(tracker.render())
706
+ raise typer.Exit(0)
707
+ else:
708
+ run_command(["git", "merge", "--no-ff", current_branch, "-m", f"Merge feature {current_branch}"])
709
+ tracker.complete("merge", "merged with merge commit")
710
+ except Exception as exc:
711
+ tracker.error("merge", str(exc))
712
+ console.print(tracker.render())
713
+ console.print(f"\n[red]Merge failed.[/red] You may need to resolve conflicts.")
714
+ raise typer.Exit(1)
715
+
716
+ if push:
717
+ tracker.start("push")
718
+ try:
719
+ run_command(["git", "push", "origin", target_branch])
720
+ tracker.complete("push")
721
+ except Exception as exc:
722
+ tracker.error("push", str(exc))
723
+ console.print(tracker.render())
724
+ console.print(f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed.")
725
+ console.print(f"Run manually: git push origin {target_branch}")
726
+
727
+ if in_worktree and remove_worktree:
728
+ tracker.start("worktree")
729
+ try:
730
+ run_command(["git", "worktree", "remove", str(feature_worktree_path), "--force"])
731
+ tracker.complete("worktree", f"removed {feature_worktree_path}")
732
+ except Exception as exc:
733
+ tracker.error("worktree", str(exc))
734
+ console.print(tracker.render())
735
+ console.print(f"\n[yellow]Warning:[/yellow] Could not remove worktree.")
736
+ console.print(f"Run manually: git worktree remove {feature_worktree_path}")
737
+
738
+ if delete_branch:
739
+ tracker.start("branch")
740
+ try:
741
+ run_command(["git", "branch", "-d", current_branch])
742
+ tracker.complete("branch", f"deleted {current_branch}")
743
+ except Exception as exc:
744
+ try:
745
+ run_command(["git", "branch", "-D", current_branch])
746
+ tracker.complete("branch", f"force deleted {current_branch}")
747
+ except Exception:
748
+ tracker.error("branch", str(exc))
749
+ console.print(tracker.render())
750
+ console.print(f"\n[yellow]Warning:[/yellow] Could not delete branch {current_branch}.")
751
+ console.print(f"Run manually: git branch -d {current_branch}")
752
+
753
+ console.print(tracker.render())
754
+ console.print(f"\n[bold green]✓ Feature {current_branch} successfully merged into {target_branch}[/bold green]")
755
+ __all__ = ["merge"]