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,215 @@
1
+ """Conflict prediction for merge dry-run.
2
+
3
+ Implements FR-005 through FR-007: predicting which files will conflict
4
+ during merge and identifying auto-resolvable status files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import fnmatch
10
+ import subprocess
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ __all__ = [
18
+ "ConflictPrediction",
19
+ "predict_conflicts",
20
+ "is_status_file",
21
+ "build_file_wp_mapping",
22
+ "display_conflict_forecast",
23
+ ]
24
+
25
+
26
+ # Patterns for status files that can be auto-resolved
27
+ STATUS_FILE_PATTERNS = [
28
+ "kitty-specs/*/tasks/*.md", # WP files: kitty-specs/017-feature/tasks/WP01.md
29
+ "kitty-specs/*/tasks.md", # Main tasks: kitty-specs/017-feature/tasks.md
30
+ "kitty-specs/*/*/tasks/*.md", # Nested: kitty-specs/features/017/tasks/WP01.md
31
+ "kitty-specs/*/*/tasks.md", # Nested main
32
+ ]
33
+
34
+
35
+ @dataclass
36
+ class ConflictPrediction:
37
+ """Predicted conflict for a file.
38
+
39
+ Attributes:
40
+ file_path: Path to the file that may conflict
41
+ conflicting_wps: List of WP IDs that modify this file
42
+ is_status_file: True if file matches status file pattern
43
+ confidence: Prediction confidence ("certain", "likely", "possible")
44
+ """
45
+
46
+ file_path: str
47
+ conflicting_wps: list[str]
48
+ is_status_file: bool
49
+ confidence: str # "certain", "likely", "possible"
50
+
51
+ @property
52
+ def auto_resolvable(self) -> bool:
53
+ """Status files can be auto-resolved."""
54
+ return self.is_status_file
55
+
56
+
57
+ def is_status_file(file_path: str) -> bool:
58
+ """Check if file matches status file patterns.
59
+
60
+ Status files contain lane/checkbox/history that can be auto-resolved
61
+ during merge because their content is procedurally generated.
62
+
63
+ Args:
64
+ file_path: Path to check (relative to repo root)
65
+
66
+ Returns:
67
+ True if file matches a status file pattern
68
+ """
69
+ for pattern in STATUS_FILE_PATTERNS:
70
+ if fnmatch.fnmatch(file_path, pattern):
71
+ return True
72
+ return False
73
+
74
+
75
+ def build_file_wp_mapping(
76
+ wp_workspaces: list[tuple[Path, str, str]],
77
+ target_branch: str,
78
+ repo_root: Path,
79
+ ) -> dict[str, list[str]]:
80
+ """Build mapping of file paths to WPs that modify them.
81
+
82
+ Uses git diff to identify which files each WP branch modifies
83
+ relative to the target branch.
84
+
85
+ Args:
86
+ wp_workspaces: List of (worktree_path, wp_id, branch_name) tuples
87
+ target_branch: Branch being merged into (e.g., "main")
88
+ repo_root: Repository root for running git commands
89
+
90
+ Returns:
91
+ Dict mapping file_path → [wp_ids that modify it]
92
+ """
93
+ file_to_wps: dict[str, list[str]] = {}
94
+
95
+ for _, wp_id, branch_name in wp_workspaces:
96
+ try:
97
+ result = subprocess.run(
98
+ ["git", "diff", "--name-only", f"{target_branch}...{branch_name}"],
99
+ cwd=str(repo_root),
100
+ capture_output=True,
101
+ text=True,
102
+ check=False,
103
+ )
104
+
105
+ if result.returncode == 0:
106
+ for line in result.stdout.strip().split("\n"):
107
+ if line:
108
+ if line not in file_to_wps:
109
+ file_to_wps[line] = []
110
+ file_to_wps[line].append(wp_id)
111
+ except Exception:
112
+ continue # Skip this WP if diff fails
113
+
114
+ return file_to_wps
115
+
116
+
117
+ def predict_conflicts(
118
+ wp_workspaces: list[tuple[Path, str, str]],
119
+ target_branch: str,
120
+ repo_root: Path,
121
+ ) -> list[ConflictPrediction]:
122
+ """Predict which files will conflict during merge.
123
+
124
+ Identifies files modified by multiple WPs, which may result in
125
+ merge conflicts. Status files are marked as auto-resolvable.
126
+
127
+ Args:
128
+ wp_workspaces: Ordered list of (worktree_path, wp_id, branch_name) tuples
129
+ target_branch: Branch being merged into (e.g., "main")
130
+ repo_root: Repository root for running git commands
131
+
132
+ Returns:
133
+ List of ConflictPrediction for files with potential conflicts
134
+ """
135
+ file_to_wps = build_file_wp_mapping(wp_workspaces, target_branch, repo_root)
136
+
137
+ predictions = []
138
+ for file_path, wp_ids in sorted(file_to_wps.items()):
139
+ if len(wp_ids) >= 2:
140
+ predictions.append(
141
+ ConflictPrediction(
142
+ file_path=file_path,
143
+ conflicting_wps=wp_ids,
144
+ is_status_file=is_status_file(file_path),
145
+ confidence="possible", # Can enhance with merge-tree in future
146
+ )
147
+ )
148
+
149
+ return predictions
150
+
151
+
152
+ def display_conflict_forecast(
153
+ predictions: list[ConflictPrediction],
154
+ console: Console,
155
+ ) -> None:
156
+ """Display conflict predictions with Rich formatting.
157
+
158
+ Groups predictions into auto-resolvable (status files) and
159
+ manual-required categories for clear user guidance.
160
+
161
+ Args:
162
+ predictions: List of ConflictPrediction objects
163
+ console: Rich Console instance for output
164
+ """
165
+ if not predictions:
166
+ console.print("\n[green]No conflicts predicted[/green]\n")
167
+ return
168
+
169
+ console.print("\n[bold]Conflict Forecast[/bold]\n")
170
+
171
+ auto_resolvable = [p for p in predictions if p.auto_resolvable]
172
+ manual_required = [p for p in predictions if not p.auto_resolvable]
173
+
174
+ # Summary counts
175
+ total = len(predictions)
176
+ auto_count = len(auto_resolvable)
177
+ manual_count = len(manual_required)
178
+
179
+ console.print(f"[dim]Found {total} potential conflict(s): {auto_count} auto-resolvable, {manual_count} manual[/dim]\n")
180
+
181
+ # Create table for conflicts
182
+ if manual_required:
183
+ table = Table(show_header=True, header_style="bold yellow")
184
+ table.add_column("File")
185
+ table.add_column("WPs")
186
+ table.add_column("Confidence")
187
+
188
+ for pred in manual_required:
189
+ wps = ", ".join(pred.conflicting_wps)
190
+ table.add_row(pred.file_path, wps, pred.confidence)
191
+
192
+ console.print("[yellow]May require manual resolution:[/yellow]")
193
+ console.print(table)
194
+ console.print()
195
+
196
+ if auto_resolvable:
197
+ table = Table(show_header=True, header_style="bold dim")
198
+ table.add_column("Status File")
199
+ table.add_column("WPs")
200
+
201
+ for pred in auto_resolvable:
202
+ wps = ", ".join(pred.conflicting_wps)
203
+ table.add_row(pred.file_path, wps)
204
+
205
+ console.print("[dim]Auto-resolvable (status files):[/dim]")
206
+ console.print(table)
207
+ console.print()
208
+
209
+ # Summary guidance
210
+ if manual_count == 0:
211
+ console.print("[green]All conflicts can be auto-resolved.[/green]\n")
212
+ else:
213
+ console.print(
214
+ f"[yellow]Prepare to resolve {manual_count} conflict(s) manually during merge.[/yellow]\n"
215
+ )
@@ -0,0 +1,126 @@
1
+ """Merge ordering based on WP dependencies.
2
+
3
+ Implements FR-008 through FR-011: determining merge order via topological
4
+ sort of the dependency graph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ from specify_cli.core.dependency_graph import (
13
+ build_dependency_graph,
14
+ detect_cycles,
15
+ topological_sort,
16
+ )
17
+
18
+ __all__ = ["get_merge_order", "MergeOrderError", "has_dependency_info", "display_merge_order"]
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class MergeOrderError(Exception):
24
+ """Error determining merge order."""
25
+
26
+ pass
27
+
28
+
29
+ def has_dependency_info(graph: dict[str, list[str]]) -> bool:
30
+ """Check if any WP has declared dependencies.
31
+
32
+ Args:
33
+ graph: Dependency graph mapping WP ID to list of dependencies
34
+
35
+ Returns:
36
+ True if at least one WP has non-empty dependencies
37
+ """
38
+ return any(deps for deps in graph.values())
39
+
40
+
41
+ def get_merge_order(
42
+ wp_workspaces: list[tuple[Path, str, str]],
43
+ feature_dir: Path,
44
+ ) -> list[tuple[Path, str, str]]:
45
+ """Return WPs in dependency order (topological sort).
46
+
47
+ Determines the optimal merge order based on WP dependencies declared
48
+ in frontmatter. WPs with dependencies will be merged after their
49
+ dependencies.
50
+
51
+ Args:
52
+ wp_workspaces: List of (worktree_path, wp_id, branch_name) tuples
53
+ feature_dir: Path to feature directory containing tasks/
54
+
55
+ Returns:
56
+ Same tuples reordered by dependency (dependencies first)
57
+
58
+ Raises:
59
+ MergeOrderError: If circular dependency detected
60
+ """
61
+ if not wp_workspaces:
62
+ return []
63
+
64
+ # Build WP ID → workspace mapping
65
+ wp_map = {wp_id: (path, wp_id, branch) for path, wp_id, branch in wp_workspaces}
66
+
67
+ # Build dependency graph from task frontmatter
68
+ graph = build_dependency_graph(feature_dir)
69
+
70
+ # Check for missing WPs in graph (may have no frontmatter)
71
+ for wp_id in wp_map:
72
+ if wp_id not in graph:
73
+ graph[wp_id] = [] # No dependencies
74
+
75
+ # Check if we have any dependency info
76
+ if not has_dependency_info(graph):
77
+ # No dependency info - fall back to numerical order with warning
78
+ logger.warning(
79
+ "No dependency information found in WP frontmatter. "
80
+ "Falling back to numerical order (WP01, WP02, ...)."
81
+ )
82
+ return sorted(wp_workspaces, key=lambda x: x[1]) # Sort by wp_id
83
+
84
+ # Detect cycles - show full cycle path in error
85
+ cycles = detect_cycles(graph)
86
+ if cycles:
87
+ # Format the cycle path clearly: WP01 → WP02 → WP03 → WP01
88
+ cycle = cycles[0]
89
+ cycle_str = " → ".join(cycle)
90
+ raise MergeOrderError(
91
+ f"Circular dependency detected: {cycle_str}\n"
92
+ "Fix the dependencies in the WP frontmatter to remove this cycle."
93
+ )
94
+
95
+ # Topological sort
96
+ try:
97
+ ordered_ids = topological_sort(graph)
98
+ except ValueError as e:
99
+ raise MergeOrderError(str(e)) from e
100
+
101
+ # Filter to only WPs we have workspaces for, maintaining order
102
+ result = []
103
+ for wp_id in ordered_ids:
104
+ if wp_id in wp_map:
105
+ result.append(wp_map[wp_id])
106
+
107
+ return result
108
+
109
+
110
+ def display_merge_order(
111
+ ordered_workspaces: list[tuple[Path, str, str]],
112
+ console,
113
+ ) -> None:
114
+ """Display the merge order to the user.
115
+
116
+ Args:
117
+ ordered_workspaces: Ordered list of (path, wp_id, branch) tuples
118
+ console: Rich Console for output
119
+ """
120
+ if not ordered_workspaces:
121
+ return
122
+
123
+ console.print("\n[bold]Merge Order[/bold] (dependency-based):\n")
124
+ for i, (_, wp_id, branch) in enumerate(ordered_workspaces, 1):
125
+ console.print(f" {i}. {wp_id} ({branch})")
126
+ console.print()
@@ -0,0 +1,230 @@
1
+ """Pre-flight validation for merge operations.
2
+
3
+ Implements FR-001 through FR-004: checking worktree status and target branch
4
+ divergence before any merge operation begins.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ from specify_cli.core.dependency_graph import build_dependency_graph
17
+
18
+ __all__ = [
19
+ "WPStatus",
20
+ "PreflightResult",
21
+ "check_worktree_status",
22
+ "check_target_divergence",
23
+ "run_preflight",
24
+ "display_preflight_result",
25
+ ]
26
+
27
+
28
+ @dataclass
29
+ class WPStatus:
30
+ """Status of a single WP worktree during pre-flight."""
31
+
32
+ wp_id: str
33
+ worktree_path: Path
34
+ branch_name: str
35
+ is_clean: bool
36
+ error: str | None = None
37
+
38
+
39
+ @dataclass
40
+ class PreflightResult:
41
+ """Result of pre-merge validation checks."""
42
+
43
+ passed: bool
44
+ wp_statuses: list[WPStatus] = field(default_factory=list)
45
+ target_diverged: bool = False
46
+ target_divergence_msg: str | None = None
47
+ errors: list[str] = field(default_factory=list)
48
+ warnings: list[str] = field(default_factory=list)
49
+
50
+
51
+ def check_worktree_status(worktree_path: Path, wp_id: str, branch_name: str) -> WPStatus:
52
+ """Check if a worktree has uncommitted changes.
53
+
54
+ Args:
55
+ worktree_path: Path to the worktree directory
56
+ wp_id: Work package ID (e.g., "WP01")
57
+ branch_name: Name of the branch
58
+
59
+ Returns:
60
+ WPStatus with is_clean=True if no uncommitted changes
61
+ """
62
+ try:
63
+ result = subprocess.run(
64
+ ["git", "status", "--porcelain"],
65
+ cwd=str(worktree_path),
66
+ capture_output=True,
67
+ text=True,
68
+ check=False,
69
+ )
70
+ is_clean = not result.stdout.strip()
71
+ error = None if is_clean else f"Uncommitted changes in {worktree_path.name}"
72
+ return WPStatus(
73
+ wp_id=wp_id,
74
+ worktree_path=worktree_path,
75
+ branch_name=branch_name,
76
+ is_clean=is_clean,
77
+ error=error,
78
+ )
79
+ except Exception as e:
80
+ return WPStatus(
81
+ wp_id=wp_id,
82
+ worktree_path=worktree_path,
83
+ branch_name=branch_name,
84
+ is_clean=False,
85
+ error=str(e),
86
+ )
87
+
88
+
89
+ def check_target_divergence(target_branch: str, repo_root: Path) -> tuple[bool, str | None]:
90
+ """Check if target branch has diverged from origin.
91
+
92
+ Args:
93
+ target_branch: Name of the target branch (e.g., "main")
94
+ repo_root: Path to the repository root
95
+
96
+ Returns:
97
+ Tuple of (has_diverged, remediation_message)
98
+ - has_diverged: True if local branch is behind origin
99
+ - remediation_message: Instructions for fixing divergence
100
+ """
101
+ try:
102
+ # Fetch latest refs (optional, may fail if offline)
103
+ subprocess.run(
104
+ ["git", "fetch", "origin", target_branch],
105
+ cwd=str(repo_root),
106
+ capture_output=True,
107
+ check=False,
108
+ )
109
+
110
+ # Count commits ahead/behind
111
+ result = subprocess.run(
112
+ ["git", "rev-list", "--left-right", "--count", f"{target_branch}...origin/{target_branch}"],
113
+ cwd=str(repo_root),
114
+ capture_output=True,
115
+ text=True,
116
+ check=False,
117
+ )
118
+
119
+ if result.returncode != 0:
120
+ return False, None # No remote tracking, assume OK
121
+
122
+ parts = result.stdout.strip().split()
123
+ if len(parts) != 2:
124
+ return False, None # Unexpected output, assume OK
125
+
126
+ ahead, behind = map(int, parts)
127
+
128
+ if behind > 0:
129
+ return True, f"{target_branch} is {behind} commit(s) behind origin. Run: git checkout {target_branch} && git pull"
130
+
131
+ return False, None
132
+
133
+ except Exception:
134
+ return False, None # Assume OK if check fails
135
+
136
+
137
+ def run_preflight(
138
+ feature_slug: str,
139
+ target_branch: str,
140
+ repo_root: Path,
141
+ wp_workspaces: list[tuple[Path, str, str]],
142
+ ) -> PreflightResult:
143
+ """Run all pre-flight checks before merge.
144
+
145
+ Args:
146
+ feature_slug: Feature identifier (e.g., "017-smarter-feature-merge")
147
+ target_branch: Branch to merge into (e.g., "main")
148
+ repo_root: Repository root path
149
+ wp_workspaces: List of (worktree_path, wp_id, branch_name) tuples
150
+
151
+ Returns:
152
+ PreflightResult with all check outcomes
153
+ """
154
+ result = PreflightResult(passed=True)
155
+
156
+ # Check for missing worktrees based on tasks in kitty-specs
157
+ expected_graph = build_dependency_graph(repo_root / "kitty-specs" / feature_slug)
158
+ expected_wps = set(expected_graph.keys())
159
+ discovered_wps = {wp_id for _, wp_id, _ in wp_workspaces}
160
+ missing_wps = sorted(expected_wps - discovered_wps)
161
+ if missing_wps:
162
+ result.passed = False
163
+ for wp_id in missing_wps:
164
+ expected_path = repo_root / ".worktrees" / f"{feature_slug}-{wp_id}"
165
+ error = f"Missing worktree for {wp_id}. Expected at {expected_path.name}. Run: spec-kitty agent workflow implement {wp_id}"
166
+ result.wp_statuses.append(
167
+ WPStatus(
168
+ wp_id=wp_id,
169
+ worktree_path=expected_path,
170
+ branch_name=f"{feature_slug}-{wp_id}",
171
+ is_clean=False,
172
+ error=error,
173
+ )
174
+ )
175
+ result.errors.append(error)
176
+
177
+ # Check all worktrees
178
+ for wt_path, wp_id, branch in wp_workspaces:
179
+ status = check_worktree_status(wt_path, wp_id, branch)
180
+ result.wp_statuses.append(status)
181
+ if not status.is_clean:
182
+ result.passed = False
183
+ result.errors.append(status.error or f"{wp_id} has uncommitted changes")
184
+
185
+ # Check target divergence
186
+ diverged, msg = check_target_divergence(target_branch, repo_root)
187
+ result.target_diverged = diverged
188
+ result.target_divergence_msg = msg
189
+ if diverged:
190
+ result.passed = False
191
+ result.errors.append(msg or f"{target_branch} has diverged from origin")
192
+
193
+ return result
194
+
195
+
196
+ def display_preflight_result(result: PreflightResult, console: Console) -> None:
197
+ """Display pre-flight results with Rich formatting.
198
+
199
+ Args:
200
+ result: PreflightResult to display
201
+ console: Rich Console instance for output
202
+ """
203
+ console.print("\n[bold]Pre-flight Check[/bold]\n")
204
+
205
+ # WP status table
206
+ table = Table(show_header=True, header_style="bold")
207
+ table.add_column("WP")
208
+ table.add_column("Status")
209
+ table.add_column("Issue")
210
+
211
+ for status in result.wp_statuses:
212
+ icon = "[green]✓[/green]" if status.is_clean else "[red]✗[/red]"
213
+ issue = status.error or ""
214
+ table.add_row(status.wp_id, icon, issue)
215
+
216
+ # Target branch status
217
+ if result.target_diverged:
218
+ table.add_row("Target", "[red]✗[/red]", result.target_divergence_msg or "Diverged")
219
+ else:
220
+ table.add_row("Target", "[green]✓[/green]", "Up to date")
221
+
222
+ console.print(table)
223
+
224
+ if not result.passed:
225
+ console.print("\n[bold red]Pre-flight failed.[/bold red] Fix these issues before merging:\n")
226
+ for i, error in enumerate(result.errors, 1):
227
+ console.print(f" {i}. {error}")
228
+ console.print()
229
+ else:
230
+ console.print("\n[green]Pre-flight passed.[/green] Ready to merge.\n")