Implementation Plan: Smarter Feature Merge with Pre-flight and Auto-cleanup

Branch: 017-smarter-feature-merge-with-preflight | Date: 2025-01-18 | Spec: spec.md Input: Feature specification from kitty-specs/017-smarter-feature-merge-with-preflight/spec.md

Summary

Enhance the spec-kitty merge command to provide pre-flight validation, conflict forecasting, dependency-ordered merging, automatic status file conflict resolution, and resume capability. The current ~590-line merge.py will be refactored into a modular architecture with dedicated components for each capability.

Key Technical Decisions:

  • Status file resolution: Post-merge Python cleanup (not git merge drivers) - detects conflict markers in status files and resolves them before committing
  • Merge state persistence: JSON file at .kittify/merge-state.json
  • Architecture: Extract merge logic into src/specify_cli/merge/ subpackage

Technical Context

Language/Version: Python 3.11+ (existing spec-kitty codebase) Primary Dependencies: typer (CLI), rich (console output), ruamel.yaml (frontmatter parsing), pathlib (file operations) Storage: Filesystem only (.kittify/merge-state.json for state persistence) Testing: pytest with existing test infrastructure Target Platform: macOS, Linux (CLI tool) Project Type: Single Python package (existing structure) Performance Goals: Handle features with 10+ WPs without noticeable delay Constraints: Must work with existing git installations (no custom merge drivers required) Scale/Scope: Features with up to 20 WPs, repositories of any size

Constitution Check

No constitution file exists. Proceeding with standard Python best practices.

  • Follow existing code patterns in src/specify_cli/
  • Use existing utilities: dependency_graph.py, git_ops.py, frontmatter.py
  • Maintain backward compatibility with existing --dry-run, --keep-branch, etc. flags
  • Add new flags additively (no breaking changes to CLI interface)

Project Structure

Documentation (this feature)

kitty-specs/017-smarter-feature-merge-with-preflight/
├── spec.md              # Feature specification
├── plan.md              # This file
├── data-model.md        # Merge state and conflict forecast entities
├── quickstart.md        # Quick reference for new merge features
└── tasks/               # Work package files (created by /spec-kitty.tasks)

Source Code (repository root)

src/specify_cli/
├── cli/
│   └── commands/
│       └── merge.py           # MODIFY: Slim down, delegate to merge/ subpackage
├── merge/                     # NEW: Merge subpackage
│   ├── __init__.py           # Public API exports
│   ├── preflight.py          # Pre-flight validation checks
│   ├── forecast.py           # Conflict prediction
│   ├── ordering.py           # Topological sort, merge order
│   ├── status_resolver.py    # Status file auto-resolution
│   ├── state.py              # Merge state persistence/resume
│   └── executor.py           # Core merge execution logic
├── core/
│   └── dependency_graph.py    # MODIFY: Add topological_sort()
└── ...

tests/
├── specify_cli/
│   ├── test_merge/            # NEW: Test directory for merge subpackage
│   │   ├── test_preflight.py
│   │   ├── test_forecast.py
│   │   ├── test_ordering.py
│   │   ├── test_status_resolver.py
│   │   └── test_state.py
│   └── test_cli/
│       └── test_merge_workspace_per_wp.py  # MODIFY: Add integration tests
└── ...

Structure Decision: Create new src/specify_cli/merge/ subpackage to modularize the merge logic. This keeps merge.py as a thin CLI wrapper while allowing unit testing of individual components.

Component Design

1. Pre-flight Validation (merge/preflight.py)

@dataclass
class PreflightResult:
    passed: bool
    wp_statuses: list[WPStatus]  # (wp_id, path, clean: bool, error: str | None)
    target_diverged: bool
    target_divergence_msg: str | None
    errors: list[str]
    warnings: list[str]

def run_preflight(
    feature_slug: str,
    target_branch: str,
    repo_root: Path,
) -> PreflightResult:
    """Check all WPs and target branch before merge."""

Responsibilities:

  • FR-001: Check all WP worktrees for uncommitted changes
  • FR-002: Verify target branch can fast-forward to origin
  • FR-003: Collect all issues into single result object
  • FR-004: Return result without modifying any branches

2. Conflict Forecast (merge/forecast.py)

@dataclass
class ConflictPrediction:
    file_path: str
    conflicting_wps: list[str]  # WP IDs that touch this file
    confidence: str  # "certain", "likely", "possible"

def predict_conflicts(
    wp_workspaces: list[tuple[Path, str, str]],
    target_branch: str,
    repo_root: Path,
) -> list[ConflictPrediction]:
    """Predict which files will conflict based on git diff analysis."""

Responsibilities:

  • FR-005: Compare each WP's changes against target and other WPs
  • FR-006: Group conflicts by file path
  • FR-007: Return in merge order

Algorithm: 1. For each WP, run git diff --name-only <target>...<wp_branch> to get modified files 2. Build file → [WPs] mapping 3. Files touched by 2+ WPs are conflict candidates 4. Run git merge-tree for definite conflict detection (git 2.38+)

3. Merge Ordering (merge/ordering.py)

def get_merge_order(
    wp_workspaces: list[tuple[Path, str, str]],
    feature_dir: Path,
) -> list[tuple[Path, str, str]]:
    """Return WPs in dependency order (topological sort)."""

Responsibilities:

  • FR-008: Parse dependencies from WP frontmatter (reuse dependency_graph.py)
  • FR-009: Topological sort
  • FR-010: Detect and report cycles
  • FR-011: Fall back to numerical order if no dependencies

Implementation: Add topological_sort() to core/dependency_graph.py:

def topological_sort(graph: dict[str, list[str]]) -> list[str]:
    """Kahn's algorithm for topological ordering."""

4. Status File Resolver (merge/status_resolver.py)

@dataclass
class ResolutionResult:
    file_path: Path
    resolved: bool
    resolution_type: str  # "lane", "checkbox", "history", "manual_required"
    original_conflicts: int
    resolved_conflicts: int

def resolve_status_conflicts(repo_root: Path) -> list[ResolutionResult]:
    """Auto-resolve conflicts in status files after merge."""

Responsibilities:

  • FR-012: Only process files matching kitty-specs/*/tasks/.md or kitty-specs/**/tasks.md
  • FR-013: Resolve lane: by "more done" value (done > for_review > doing > planned)
  • FR-014: Resolve checkboxes by preferring [x]
  • FR-015: Resolve history: by chronological concatenation
  • FR-016: Leave non-status files untouched

Algorithm: 1. After each WP merge, check git diff --name-only --diff-filter=U for conflicted files 2. For each conflicted file matching status patterns: a. Parse conflict markers b. Apply resolution rules based on content type c. Write resolved content d. git add the file 3. If all conflicts resolved, proceed; else pause for manual resolution

5. Merge State (merge/state.py)

@dataclass
class MergeState:
    feature_slug: str
    target_branch: str
    wp_order: list[str]  # WP IDs in merge order
    completed_wps: list[str]
    current_wp: str | None
    has_pending_conflicts: bool
    started_at: str  # ISO timestamp
    last_updated: str

def save_state(state: MergeState, repo_root: Path) -> None:
    """Persist to .kittify/merge-state.json"""

def load_state(repo_root: Path) -> MergeState | None:
    """Load existing state or None if no merge in progress"""

def clear_state(repo_root: Path) -> None:
    """Remove state file after completion or abort"""

Responsibilities:

  • FR-021: Persist state during multi-WP merge
  • FR-022: Support --resume flag
  • FR-023: Clear on success or explicit abort
  • FR-024: Detect active git merge state

6. Merge Executor (merge/executor.py)

def execute_merge(
    feature_slug: str,
    target_branch: str,
    wp_workspaces: list[tuple[Path, str, str]],
    strategy: str,
    repo_root: Path,
    state: MergeState,
    tracker: StepTracker,
) -> bool:
    """Execute merge for all WPs with state tracking."""

Responsibilities:

  • Orchestrate preflight, ordering, merge, resolution, cleanup
  • Update state after each WP
  • Handle interruptions gracefully

CLI Changes

Update merge.py to add new flags:

def merge(
    # Existing flags (unchanged)
    strategy: str = typer.Option("merge", ...),
    delete_branch: bool = typer.Option(True, ...),
    remove_worktree: bool = typer.Option(True, ...),
    push: bool = typer.Option(False, ...),
    target_branch: str = typer.Option("main", ...),
    dry_run: bool = typer.Option(False, ...),

    # New flags
    resume: bool = typer.Option(False, "--resume", help="Resume interrupted merge"),
    single: bool = typer.Option(False, "--single", help="Merge only current WP (legacy behavior)"),
    feature: str = typer.Option(None, "--feature", help="Feature slug (when running from main)"),
) -> None:

Complexity Tracking

AspectDecisionRationale
New subpackagemerge/ with 6 modulesExisting merge.py is 590 lines; modularization enables unit testing and clearer separation
State persistenceJSON fileSimple, human-readable, no new dependencies
Conflict forecastgit merge-tree + diff analysisGit-native approach, no custom algorithms needed
Status resolutionPost-merge PythonNon-invasive, works without git configuration

Dependencies on Existing Code

ModuleUsage
core/dependency_graph.pyReuse build_dependency_graph(), detect_cycles(), add topological_sort()
core/git_ops.pyReuse run_command() for git operations
frontmatter.pyReuse read_frontmatter() for WP dependency parsing
cli/helpers.pyReuse console, StepTracker, show_banner()
tasks_support.pyReuse find_repo_root(), TaskCliError

Test Strategy

ComponentTest TypeKey Scenarios
preflight.pyUnitClean worktrees, dirty worktrees, diverged target, mixed states
forecast.pyUnitNo conflicts, single conflict, multi-WP conflict, status file detection
ordering.pyUnitLinear chain, fan-out, diamond, cycles, no dependencies
status_resolver.pyUnitLane conflicts, checkbox conflicts, history merge, non-status files
state.pyUnitSave/load/clear, resume from various points
IntegrationIntegrationFull merge flow, interrupted merge, resume, cleanup

Risk Mitigation

RiskMitigation
git merge-tree not available (< 2.38)Fall back to diff-based heuristic with "possible" confidence
Status resolution misidentifies contentStrict pattern matching on frontmatter YAML structure
State file corruptionValidate JSON on load, clear and restart if invalid
Cleanup fails mid-mergeContinue with remaining cleanup, report failures at end

Non-Goals (Confirmed Out of Scope)

  • JJ-specific merge strategies
  • SQLite or external status storage
  • Auto-rebase of remaining worktrees
  • Octopus merge for independent WPs
  • External merge tool integration