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,653 @@
1
+ """Core merge execution logic.
2
+
3
+ Provides the main entry point for merge operations, orchestrating
4
+ pre-flight validation, conflict forecasting, ordering, and execution.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Callable
14
+
15
+ from specify_cli.cli import StepTracker
16
+ from specify_cli.cli.helpers import console
17
+ from specify_cli.core.git_ops import run_command
18
+ from specify_cli.merge.ordering import (
19
+ MergeOrderError,
20
+ display_merge_order,
21
+ get_merge_order,
22
+ )
23
+ from specify_cli.merge.preflight import (
24
+ PreflightResult,
25
+ display_preflight_result,
26
+ run_preflight,
27
+ )
28
+ from specify_cli.merge.status_resolver import get_conflicted_files, resolve_status_conflicts
29
+ from specify_cli.merge.forecast import (
30
+ display_conflict_forecast,
31
+ predict_conflicts,
32
+ )
33
+ from specify_cli.merge.state import (
34
+ MergeState,
35
+ clear_state,
36
+ save_state,
37
+ )
38
+
39
+ __all__ = [
40
+ "execute_merge",
41
+ "execute_legacy_merge",
42
+ "MergeResult",
43
+ "MergeExecutionError",
44
+ ]
45
+
46
+
47
+ class MergeExecutionError(Exception):
48
+ """Error during merge execution."""
49
+
50
+ pass
51
+
52
+
53
+ @dataclass
54
+ class MergeResult:
55
+ """Result of merge execution."""
56
+
57
+ success: bool
58
+ merged_wps: list[str] = field(default_factory=list)
59
+ failed_wp: str | None = None
60
+ error: str | None = None
61
+ preflight_result: PreflightResult | None = None
62
+
63
+
64
+ def execute_merge(
65
+ wp_workspaces: list[tuple[Path, str, str]],
66
+ feature_slug: str,
67
+ feature_dir: Path | None,
68
+ target_branch: str,
69
+ strategy: str,
70
+ repo_root: Path,
71
+ merge_root: Path,
72
+ tracker: StepTracker,
73
+ delete_branch: bool = True,
74
+ remove_worktree: bool = True,
75
+ push: bool = False,
76
+ dry_run: bool = False,
77
+ on_wp_merged: Callable[[str], None] | None = None,
78
+ resume_state: MergeState | None = None,
79
+ ) -> MergeResult:
80
+ """Execute merge for all WPs with preflight and ordering.
81
+
82
+ This is the main entry point for workspace-per-WP merges, coordinating:
83
+ 1. Pre-flight validation (all worktrees clean, target not diverged)
84
+ 2. Dependency-based ordering (topological sort)
85
+ 3. Sequential merge execution with state persistence
86
+ 4. Cleanup (worktree removal, branch deletion)
87
+ 5. State cleared on success
88
+
89
+ Args:
90
+ wp_workspaces: List of (worktree_path, wp_id, branch_name) tuples
91
+ feature_slug: Feature identifier (e.g., "010-feature-name")
92
+ feature_dir: Path to feature directory (for dependency info), or None
93
+ target_branch: Branch to merge into (e.g., "main")
94
+ strategy: "merge", "squash", or "rebase"
95
+ repo_root: Repository root path
96
+ merge_root: Directory to execute merge from (main repo)
97
+ tracker: StepTracker for progress display
98
+ delete_branch: Whether to delete branches after merge
99
+ remove_worktree: Whether to remove worktrees after merge
100
+ push: Whether to push to remote after merge
101
+ dry_run: If True, show what would be done without executing
102
+ on_wp_merged: Callback after each WP merges (for state updates)
103
+ resume_state: Existing MergeState to resume from (if --resume)
104
+
105
+ Returns:
106
+ MergeResult with success status and details
107
+ """
108
+ result = MergeResult(success=False)
109
+
110
+ if not wp_workspaces:
111
+ result.error = "No WP workspaces provided"
112
+ return result
113
+
114
+ # Step 1: Run preflight checks
115
+ tracker.start("preflight")
116
+ preflight_result = run_preflight(
117
+ feature_slug=feature_slug,
118
+ target_branch=target_branch,
119
+ repo_root=repo_root,
120
+ wp_workspaces=wp_workspaces,
121
+ )
122
+ result.preflight_result = preflight_result
123
+ display_preflight_result(preflight_result, console)
124
+
125
+ if not preflight_result.passed:
126
+ tracker.error("preflight", "validation failed")
127
+ result.error = "Pre-flight validation failed"
128
+ return result
129
+ tracker.complete("preflight", "all checks passed")
130
+
131
+ # Step 2: Determine merge order based on dependencies
132
+ if feature_dir and feature_dir.exists():
133
+ try:
134
+ ordered_workspaces = get_merge_order(wp_workspaces, feature_dir)
135
+ display_merge_order(ordered_workspaces, console)
136
+ except MergeOrderError as e:
137
+ tracker.error("preflight", f"ordering failed: {e}")
138
+ result.error = str(e)
139
+ return result
140
+ else:
141
+ # No feature dir - use as-is (already sorted by WP ID)
142
+ ordered_workspaces = sorted(wp_workspaces, key=lambda x: x[1])
143
+ console.print("\n[dim]Merge order: numerical (no dependency info)[/dim]")
144
+
145
+ # Step 3: Validate all WP workspaces are ready
146
+ tracker.start("verify")
147
+ errors = []
148
+ for wt_path, wp_id, branch in ordered_workspaces:
149
+ is_valid, error_msg = _validate_wp_ready(repo_root, wt_path, branch)
150
+ if not is_valid:
151
+ errors.append(f" - {wp_id}: {error_msg}")
152
+
153
+ if errors:
154
+ tracker.error("verify", "WP workspaces not ready")
155
+ result.error = "WP workspaces not ready:\n" + "\n".join(errors)
156
+ return result
157
+
158
+ tracker.complete("verify", f"validated {len(ordered_workspaces)} workspaces")
159
+
160
+ # Step 4: Dry run - show what would be done
161
+ if dry_run:
162
+ # Predict conflicts before showing dry-run steps
163
+ predictions = predict_conflicts(ordered_workspaces, target_branch, repo_root)
164
+ display_conflict_forecast(predictions, console)
165
+
166
+ _show_dry_run(
167
+ ordered_workspaces,
168
+ target_branch,
169
+ strategy,
170
+ feature_slug,
171
+ push,
172
+ remove_worktree,
173
+ delete_branch,
174
+ )
175
+ result.success = True
176
+ result.merged_wps = [wp_id for _, wp_id, _ in ordered_workspaces]
177
+ return result
178
+
179
+ # Initialize or use resume state
180
+ if resume_state:
181
+ state = resume_state
182
+ # Filter ordered_workspaces to only remaining WPs
183
+ remaining_set = set(state.remaining_wps)
184
+ ordered_workspaces = [
185
+ (wt_path, wp_id, branch)
186
+ for wt_path, wp_id, branch in ordered_workspaces
187
+ if wp_id in remaining_set
188
+ ]
189
+ console.print(f"[cyan]Resuming from {state.completed_wps[-1] if state.completed_wps else 'start'}[/cyan]")
190
+ else:
191
+ state = MergeState(
192
+ feature_slug=feature_slug,
193
+ target_branch=target_branch,
194
+ wp_order=[wp_id for _, wp_id, _ in ordered_workspaces],
195
+ strategy=strategy,
196
+ )
197
+ save_state(state, repo_root)
198
+
199
+ # Step 5: Checkout and update target branch
200
+ tracker.start("checkout")
201
+ try:
202
+ os.chdir(merge_root)
203
+ _, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
204
+ if target_status.strip():
205
+ raise MergeExecutionError(
206
+ f"Target repository at {merge_root} has uncommitted changes."
207
+ )
208
+ run_command(["git", "checkout", target_branch])
209
+ tracker.complete("checkout", f"using {merge_root}")
210
+ except Exception as exc:
211
+ tracker.error("checkout", str(exc))
212
+ result.error = f"Checkout failed: {exc}"
213
+ return result
214
+
215
+ tracker.start("pull")
216
+ try:
217
+ run_command(["git", "pull", "--ff-only"])
218
+ tracker.complete("pull")
219
+ except Exception as exc:
220
+ tracker.error("pull", str(exc))
221
+ result.error = f"Pull failed: {exc}. You may need to resolve conflicts manually."
222
+ return result
223
+
224
+ # Step 6: Merge all WP branches in dependency order
225
+ tracker.start("merge")
226
+ try:
227
+ for wt_path, wp_id, branch in ordered_workspaces:
228
+ # Set current WP and save state before merge
229
+ state.set_current_wp(wp_id)
230
+ save_state(state, repo_root)
231
+
232
+ console.print(f"[cyan]Merging {wp_id} ({branch})...[/cyan]")
233
+
234
+ if strategy == "squash":
235
+ merge_code, _, _ = run_command(
236
+ ["git", "merge", "--squash", branch],
237
+ check_return=False,
238
+ capture=True,
239
+ )
240
+ conflict_error = _resolve_merge_conflicts(repo_root, wp_id)
241
+ if conflict_error:
242
+ state.set_pending_conflicts(True)
243
+ save_state(state, repo_root)
244
+ result.error = conflict_error
245
+ return result
246
+ run_command(
247
+ ["git", "commit", "-m", f"Merge {wp_id} from {feature_slug}"]
248
+ )
249
+ elif strategy == "rebase":
250
+ result.error = "Rebase strategy not supported for workspace-per-WP."
251
+ tracker.skip("merge", "rebase not supported")
252
+ return result
253
+ else: # merge (default)
254
+ merge_code, _, _ = run_command(
255
+ [
256
+ "git",
257
+ "merge",
258
+ "--no-ff",
259
+ branch,
260
+ "-m",
261
+ f"Merge {wp_id} from {feature_slug}",
262
+ ],
263
+ check_return=False,
264
+ capture=True,
265
+ )
266
+ conflict_error = _resolve_merge_conflicts(repo_root, wp_id)
267
+ if conflict_error:
268
+ state.set_pending_conflicts(True)
269
+ save_state(state, repo_root)
270
+ result.error = conflict_error
271
+ return result
272
+ if merge_code != 0:
273
+ run_command(
274
+ ["git", "commit", "-m", f"Merge {wp_id} from {feature_slug}"]
275
+ )
276
+
277
+ # Mark WP complete and save state
278
+ state.mark_wp_complete(wp_id)
279
+ save_state(state, repo_root)
280
+
281
+ result.merged_wps.append(wp_id)
282
+ console.print(f"[green]\u2713[/green] {wp_id} merged")
283
+
284
+ if on_wp_merged:
285
+ on_wp_merged(wp_id)
286
+
287
+ tracker.complete("merge", f"merged {len(ordered_workspaces)} work packages")
288
+ except Exception as exc:
289
+ tracker.error("merge", str(exc))
290
+ result.failed_wp = wp_id if "wp_id" in dir() else None
291
+ result.error = f"Merge failed: {exc}"
292
+ # Save state on error for resume
293
+ state.set_pending_conflicts(True)
294
+ save_state(state, repo_root)
295
+ return result
296
+
297
+ # Step 7: Push if requested
298
+ if push:
299
+ tracker.start("push")
300
+ try:
301
+ run_command(["git", "push", "origin", target_branch])
302
+ tracker.complete("push")
303
+ except Exception as exc:
304
+ tracker.error("push", str(exc))
305
+ console.print(
306
+ f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed."
307
+ )
308
+ console.print(f"Run manually: git push origin {target_branch}")
309
+
310
+ # Step 8: Remove worktrees
311
+ if remove_worktree:
312
+ tracker.start("worktree")
313
+ failed_removals = []
314
+ for wt_path, wp_id, branch in ordered_workspaces:
315
+ try:
316
+ run_command(["git", "worktree", "remove", str(wt_path), "--force"])
317
+ console.print(f"[green]\u2713[/green] Removed worktree: {wp_id}")
318
+ except Exception:
319
+ failed_removals.append((wp_id, wt_path))
320
+
321
+ if failed_removals:
322
+ tracker.error(
323
+ "worktree", f"could not remove {len(failed_removals)} worktrees"
324
+ )
325
+ console.print(
326
+ f"\n[yellow]Warning:[/yellow] Could not remove some worktrees:"
327
+ )
328
+ for wp_id, wt_path in failed_removals:
329
+ console.print(f" {wp_id}: git worktree remove {wt_path}")
330
+ else:
331
+ tracker.complete("worktree", f"removed {len(ordered_workspaces)} worktrees")
332
+
333
+ # Step 9: Delete branches
334
+ if delete_branch:
335
+ tracker.start("branch")
336
+ failed_deletions = []
337
+ for wt_path, wp_id, branch in ordered_workspaces:
338
+ try:
339
+ run_command(["git", "branch", "-d", branch])
340
+ console.print(f"[green]\u2713[/green] Deleted branch: {branch}")
341
+ except Exception:
342
+ # Try force delete
343
+ try:
344
+ run_command(["git", "branch", "-D", branch])
345
+ console.print(f"[green]\u2713[/green] Force deleted branch: {branch}")
346
+ except Exception:
347
+ failed_deletions.append((wp_id, branch))
348
+
349
+ if failed_deletions:
350
+ tracker.error(
351
+ "branch", f"could not delete {len(failed_deletions)} branches"
352
+ )
353
+ console.print(
354
+ f"\n[yellow]Warning:[/yellow] Could not delete some branches:"
355
+ )
356
+ for wp_id, branch in failed_deletions:
357
+ console.print(f" {wp_id}: git branch -D {branch}")
358
+ else:
359
+ tracker.complete("branch", f"deleted {len(ordered_workspaces)} branches")
360
+
361
+ # Clear state on successful completion
362
+ clear_state(repo_root)
363
+
364
+ result.success = True
365
+ return result
366
+
367
+
368
+ def execute_legacy_merge(
369
+ current_branch: str,
370
+ target_branch: str,
371
+ strategy: str,
372
+ merge_root: Path,
373
+ feature_worktree_path: Path,
374
+ tracker: StepTracker,
375
+ push: bool = False,
376
+ remove_worktree: bool = True,
377
+ delete_branch: bool = True,
378
+ dry_run: bool = False,
379
+ in_worktree: bool = False,
380
+ ) -> MergeResult:
381
+ """Execute legacy single-worktree merge flow.
382
+
383
+ Args:
384
+ current_branch: Current feature branch name
385
+ target_branch: Branch to merge into
386
+ strategy: "merge", "squash", or "rebase"
387
+ merge_root: Repository root to run merge commands from
388
+ feature_worktree_path: Worktree path to remove (if applicable)
389
+ tracker: StepTracker for progress display
390
+ push: Whether to push to remote after merge
391
+ remove_worktree: Whether to remove worktree after merge
392
+ delete_branch: Whether to delete branch after merge
393
+ dry_run: If True, show what would be done without executing
394
+ in_worktree: Whether caller is in a worktree context
395
+
396
+ Returns:
397
+ MergeResult with success status and details
398
+ """
399
+ result = MergeResult(success=False)
400
+
401
+ tracker.start("verify")
402
+ try:
403
+ _, status_output, _ = run_command(["git", "status", "--porcelain"], capture=True)
404
+ if status_output.strip():
405
+ tracker.error("verify", "uncommitted changes")
406
+ result.error = "Working directory has uncommitted changes."
407
+ return result
408
+ tracker.complete("verify", "clean working directory")
409
+ except Exception as exc:
410
+ tracker.error("verify", str(exc))
411
+ result.error = str(exc)
412
+ return result
413
+
414
+ if dry_run:
415
+ console.print(tracker.render())
416
+ console.print("\n[cyan]Dry run - would execute:[/cyan]")
417
+ checkout_prefix = f"(from {merge_root}) " if in_worktree else ""
418
+ steps = [
419
+ f"{checkout_prefix}git checkout {target_branch}",
420
+ "git pull --ff-only",
421
+ ]
422
+ if strategy == "squash":
423
+ steps.extend(
424
+ [
425
+ f"git merge --squash {current_branch}",
426
+ f"git commit -m 'Merge feature {current_branch}'",
427
+ ]
428
+ )
429
+ elif strategy == "rebase":
430
+ steps.append(f"git merge --ff-only {current_branch} (after rebase)")
431
+ else:
432
+ steps.append(f"git merge --no-ff {current_branch}")
433
+ if push:
434
+ steps.append(f"git push origin {target_branch}")
435
+ if in_worktree and remove_worktree:
436
+ steps.append(f"git worktree remove {feature_worktree_path}")
437
+ if delete_branch:
438
+ steps.append(f"git branch -d {current_branch}")
439
+ for idx, step in enumerate(steps, start=1):
440
+ console.print(f" {idx}. {step}")
441
+ result.success = True
442
+ return result
443
+
444
+ tracker.start("checkout")
445
+ try:
446
+ if in_worktree:
447
+ console.print(
448
+ f"[cyan]Detected worktree. Merge operations will run from {merge_root}[/cyan]"
449
+ )
450
+ os.chdir(merge_root)
451
+ _, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
452
+ if target_status.strip():
453
+ raise MergeExecutionError(
454
+ f"Target repository at {merge_root} has uncommitted changes."
455
+ )
456
+ run_command(["git", "checkout", target_branch])
457
+ tracker.complete("checkout", f"using {merge_root}")
458
+ except Exception as exc:
459
+ tracker.error("checkout", str(exc))
460
+ result.error = f"Checkout failed: {exc}"
461
+ return result
462
+
463
+ tracker.start("pull")
464
+ try:
465
+ run_command(["git", "pull", "--ff-only"])
466
+ tracker.complete("pull")
467
+ except Exception as exc:
468
+ tracker.error("pull", str(exc))
469
+ result.error = (
470
+ f"Pull failed: {exc}. You may need to resolve conflicts manually."
471
+ )
472
+ return result
473
+
474
+ tracker.start("merge")
475
+ try:
476
+ if strategy == "squash":
477
+ run_command(["git", "merge", "--squash", current_branch])
478
+ run_command(["git", "commit", "-m", f"Merge feature {current_branch}"])
479
+ tracker.complete("merge", "squashed")
480
+ elif strategy == "rebase":
481
+ console.print(
482
+ "\n[yellow]Note:[/yellow] Rebase strategy requires manual intervention."
483
+ )
484
+ console.print(
485
+ f"Please run: git checkout {current_branch} && git rebase {target_branch}"
486
+ )
487
+ tracker.skip("merge", "requires manual rebase")
488
+ result.success = True
489
+ return result
490
+ else:
491
+ run_command(
492
+ ["git", "merge", "--no-ff", current_branch, "-m", f"Merge feature {current_branch}"]
493
+ )
494
+ tracker.complete("merge", "merged with merge commit")
495
+ except Exception as exc:
496
+ tracker.error("merge", str(exc))
497
+ result.error = f"Merge failed: {exc}"
498
+ return result
499
+
500
+ if push:
501
+ tracker.start("push")
502
+ try:
503
+ run_command(["git", "push", "origin", target_branch])
504
+ tracker.complete("push")
505
+ except Exception as exc:
506
+ tracker.error("push", str(exc))
507
+ console.print(
508
+ f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed."
509
+ )
510
+ console.print(f"Run manually: git push origin {target_branch}")
511
+
512
+ if in_worktree and remove_worktree:
513
+ tracker.start("worktree")
514
+ try:
515
+ run_command(
516
+ ["git", "worktree", "remove", str(feature_worktree_path), "--force"]
517
+ )
518
+ tracker.complete("worktree", f"removed {feature_worktree_path}")
519
+ except Exception as exc:
520
+ tracker.error("worktree", str(exc))
521
+ console.print(
522
+ f"\n[yellow]Warning:[/yellow] Could not remove worktree."
523
+ )
524
+ console.print(f"Run manually: git worktree remove {feature_worktree_path}")
525
+
526
+ if delete_branch:
527
+ tracker.start("branch")
528
+ try:
529
+ run_command(["git", "branch", "-d", current_branch])
530
+ tracker.complete("branch", f"deleted {current_branch}")
531
+ except Exception as exc:
532
+ try:
533
+ run_command(["git", "branch", "-D", current_branch])
534
+ tracker.complete("branch", f"force deleted {current_branch}")
535
+ except Exception:
536
+ tracker.error("branch", str(exc))
537
+ console.print(tracker.render())
538
+ console.print(
539
+ f"\n[yellow]Warning:[/yellow] Could not delete branch {current_branch}."
540
+ )
541
+ console.print(f"Run manually: git branch -d {current_branch}")
542
+
543
+ console.print(tracker.render())
544
+ console.print(
545
+ f"\n[bold green]✓ Feature {current_branch} successfully merged into {target_branch}[/bold green]"
546
+ )
547
+ result.success = True
548
+ return result
549
+
550
+
551
+ def _validate_wp_ready(
552
+ repo_root: Path, worktree_path: Path, branch_name: str
553
+ ) -> tuple[bool, str]:
554
+ """Validate WP workspace is ready to merge.
555
+
556
+ Args:
557
+ repo_root: Repository root
558
+ worktree_path: Path to worktree
559
+ branch_name: Branch name to verify
560
+
561
+ Returns:
562
+ Tuple of (is_valid, error_message)
563
+ """
564
+ # Check 1: Branch exists in git
565
+ result = subprocess.run(
566
+ ["git", "rev-parse", "--verify", branch_name],
567
+ cwd=str(repo_root),
568
+ capture_output=True,
569
+ check=False,
570
+ )
571
+ if result.returncode != 0:
572
+ return False, f"Branch {branch_name} does not exist"
573
+
574
+ # Check 2: No uncommitted changes in worktree
575
+ result = subprocess.run(
576
+ ["git", "status", "--porcelain"],
577
+ cwd=str(worktree_path),
578
+ capture_output=True,
579
+ text=True,
580
+ )
581
+ if result.stdout.strip():
582
+ return False, f"Worktree {worktree_path.name} has uncommitted changes"
583
+
584
+ return True, ""
585
+
586
+
587
+ def _show_dry_run(
588
+ ordered_workspaces: list[tuple[Path, str, str]],
589
+ target_branch: str,
590
+ strategy: str,
591
+ feature_slug: str,
592
+ push: bool,
593
+ remove_worktree: bool,
594
+ delete_branch: bool,
595
+ ) -> None:
596
+ """Display dry run output showing what would be executed.
597
+
598
+ Args:
599
+ ordered_workspaces: Ordered list of (path, wp_id, branch) tuples
600
+ target_branch: Target branch name
601
+ strategy: Merge strategy
602
+ feature_slug: Feature identifier
603
+ push: Whether push is enabled
604
+ remove_worktree: Whether worktree removal is enabled
605
+ delete_branch: Whether branch deletion is enabled
606
+ """
607
+ console.print("\n[cyan]Dry run - would execute:[/cyan]")
608
+ steps = [
609
+ f"git checkout {target_branch}",
610
+ "git pull --ff-only",
611
+ ]
612
+
613
+ for wt_path, wp_id, branch in ordered_workspaces:
614
+ if strategy == "squash":
615
+ steps.extend(
616
+ [
617
+ f"git merge --squash {branch}",
618
+ f"git commit -m 'Merge {wp_id} from {feature_slug}'",
619
+ ]
620
+ )
621
+ else:
622
+ steps.append(
623
+ f"git merge --no-ff {branch} -m 'Merge {wp_id} from {feature_slug}'"
624
+ )
625
+
626
+ if push:
627
+ steps.append(f"git push origin {target_branch}")
628
+
629
+ if remove_worktree:
630
+ for wt_path, wp_id, branch in ordered_workspaces:
631
+ steps.append(f"git worktree remove {wt_path}")
632
+
633
+ if delete_branch:
634
+ for wt_path, wp_id, branch in ordered_workspaces:
635
+ steps.append(f"git branch -d {branch}")
636
+
637
+ for idx, step in enumerate(steps, start=1):
638
+ console.print(f" {idx}. {step}")
639
+
640
+
641
+ def _resolve_merge_conflicts(repo_root: Path, wp_id: str) -> str | None:
642
+ """Resolve status file conflicts and return error if any remain."""
643
+ conflicted = get_conflicted_files(repo_root)
644
+ if not conflicted:
645
+ return None
646
+
647
+ resolve_status_conflicts(repo_root)
648
+ remaining = get_conflicted_files(repo_root)
649
+ if not remaining:
650
+ return None
651
+
652
+ files = "\n".join(f" - {path.relative_to(repo_root)}" for path in remaining)
653
+ return f"Merge for {wp_id} has unresolved conflicts:\n{files}"