Implementation Plan: Fix Merge Target Branch Resolution
Branch: 049-fix-merge-target-resolution | Date: 2026-03-10 | Spec: spec.md Input: Feature specification from kitty-specs/049-fix-merge-target-resolution/spec.md
Summary
Top-level spec-kitty merge ignores feature meta.json target_branch when --feature is provided, defaulting to resolve_primary_branch() instead. This hotfix ports the correct resolution logic (get_feature_target_branch()) to the top-level merge command, establishing spec-kitty merge --feature <slug> as the single canonical merge API. The merge command template is aligned so both frontmatter and body reference only this canonical path. spec-kitty agent feature merge remains as an undocumented compatibility shim.
Technical Context
Language/Version: Python 3.11+ (existing spec-kitty codebase) Primary Dependencies: typer, pathlib, json (all already imported in merge.py) Storage: Filesystem only (meta.json in kitty-specs/) Testing: pytest (existing test infrastructure) Target Platform: CLI tool (macOS, Linux) Project Type: Single project (Python CLI) Performance Goals: <100ms added to merge startup (NFR-001) Constraints: Reuse get_feature_target_branch() from feature_detection.py (C-004); no new dependencies (C-003) Scale/Scope: 3 files changed, ~15 lines of runtime code, ~80 lines of tests
Constitution Check
Skipped — governance assets not available.
Project Structure
Documentation (this feature)
kitty-specs/049-fix-merge-target-resolution/
├── spec.md # Feature specification
├── plan.md # This file
├── data-model.md # Not needed (no new entities)
├── quickstart.md # Developer quickstart
├── checklists/
│ └── requirements.md # Spec quality checklist
└── tasks.md # Generated by /spec-kitty.tasks
Source Code (repository root)
src/specify_cli/
├── cli/commands/
│ └── merge.py # FIX: lines 721-724, target resolution
├── core/
│ ├── feature_detection.py # REUSE: get_feature_target_branch() (lines 645-696)
│ └── git_ops.py # UNCHANGED: resolve_primary_branch() (lines 262-319)
└── missions/software-dev/
└── command-templates/
└── merge.md # FIX: align frontmatter + body to canonical spec-kitty merge
tests/specify_cli/cli/commands/
└── test_merge_target_resolution.py # NEW: regression tests
Structure Decision: All changes are in existing source tree. One new test file. No structural changes.
Detailed Design
Change 1: Fix top-level merge target resolution (merge.py)
Current code (lines 721-724):
# Resolve target branch dynamically if not specified
if target_branch is None:
from specify_cli.core.git_ops import resolve_primary_branch
target_branch = resolve_primary_branch(repo_root)
Fixed code:
# Resolve target branch dynamically if not specified
if target_branch is None:
if feature:
from specify_cli.core.feature_detection import get_feature_target_branch
target_branch = get_feature_target_branch(repo_root, feature)
else:
from specify_cli.core.git_ops import resolve_primary_branch
target_branch = resolve_primary_branch(repo_root)
This is a direct port of the pattern from feature.py lines 1337-1343. When --feature is provided and --target is not, resolve from meta.json. When neither is provided, fall back to resolve_primary_branch().
Branch existence validation (after resolution):
# After target_branch is resolved, validate it exists
if feature and target_branch:
result = subprocess.run(
["git", "rev-parse", "--verify", f"refs/heads/{target_branch}"],
capture_output=True, cwd=repo_root,
)
if result.returncode != 0:
# Also check remote
result_remote = subprocess.run(
["git", "rev-parse", "--verify", f"refs/remotes/origin/{target_branch}"],
capture_output=True, cwd=repo_root,
)
if result_remote.returncode != 0:
error_msg = (
f"Target branch '{target_branch}' from meta.json does not exist "
f"locally or on origin. Check kitty-specs/{feature}/meta.json."
)
# Use existing error handling pattern in merge.py
This implements FR-006 (hard error for nonexistent target branch, no silent fallback).
Change 2: Align merge command template (merge.md)
Architecture decision: spec-kitty merge --feature <slug> is the single canonical merge API. One documented path prevents prompt drift and nondeterministic LLM tool choice. spec-kitty agent feature merge remains as an undocumented compatibility shim but is never referenced in templates or prompts.
Current template has split guidance: frontmatter references agent feature merge, body references spec-kitty merge. After this fix, both frontmatter and body reference only the canonical path.
Fixed template:
---
description: Merge a completed feature into the target branch and clean up worktree
---
# /spec-kitty.merge - Deterministic Merge
## Required Execution Sequence
1. Generate deterministic merge plan first:
\```bash
spec-kitty merge --feature <feature-slug> --dry-run --json
\```
2. Confirm effective merge tips from JSON (`effective_wp_branches`).
3. Execute the actual merge once:
\```bash
spec-kitty merge --feature <feature-slug>
\```
Both frontmatter and body now reference the same canonical command. No mention of agent feature merge anywhere in the template.
Change 3: Regression tests
Test file: tests/specify_cli/cli/commands/test_merge_target_resolution.py
Test cases (NFR-002: 4+ test cases required):
| # | Test | Setup | Expected |
|---|---|---|---|
| 1 | Feature targets 2.x | meta.json with target_branch: "2.x", --feature provided, no --target | Resolves to "2.x" |
| 2 | Feature targets main | meta.json with target_branch: "main", --feature provided, no --target | Resolves to "main" |
| 3 | Missing meta.json | No meta.json, --feature provided, no --target | Falls back to resolve_primary_branch() |
| 4 | Explicit --target overrides | meta.json with target_branch: "2.x", --feature + --target main | Resolves to "main" (explicit wins) |
| 5 | Nonexistent target branch | meta.json with target_branch: "nonexistent", --feature provided | Hard error with clear message |
| 6 | No --feature flag | No --feature provided | Uses resolve_primary_branch() (backward compat) |
| 7 | Malformed meta.json | Invalid JSON in meta.json, --feature provided | Falls back to resolve_primary_branch() |
Test strategy: Unit tests with mocked filesystem (tmp_path) and mocked git commands. Tests exercise the target resolution logic directly, not the full merge flow.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Regression in no-feature merge path | Low | High | Test case #6 explicitly covers backward compat |
get_feature_target_branch import adds latency | Very low | Low | Function is a single file read; NFR-001 <100ms easily met |
| Template change breaks non-Claude agents | Low | Medium | All 12 agents get the same template source; test with Claude + Codex + OpenCode |
Complexity Tracking
No constitution violations. No complexity justifications needed.
Dependencies
get_feature_target_branch()infeature_detection.py(lines 645-696): Already correct, battle-tested via agent merge path. No changes needed.resolve_primary_branch()ingit_ops.py(lines 262-319): Unchanged, used as fallback.- Merge template source at
src/specify_cli/missions/software-dev/command-templates/merge.md: Single source, propagated to all 12 agents via migrations.