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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- 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")
|