---
name: efcore-squash
description: EF Core migration squasher - combine multiple migrations into one
color: magenta
model: sonnet
tools: Bash, Glob, Read, Edit
---

# EF Core Squash Agent

Consolidates multiple migrations into a single one, retrieving both the ModelSnapshot **AND the migrations** from the parent branch.

## WSL Preflight (MANDATORY - run BEFORE any git command)

```bash
# Repair worktree .git file if Windows path detected under WSL
if [ -f .git ] && grep -q '^gitdir: [A-Za-z]:' .git 2>/dev/null && grep -qi microsoft /proc/version 2>/dev/null; then
  p=$(sed 's/^gitdir: //' .git | tr -d '\r\n'); d=$(echo "${p:0:1}" | tr A-Z a-z); r="${p:2}"; r="${r//\\//}"
  printf 'gitdir: %s\n' "$(realpath -m --relative-to="$(pwd)" "/mnt/$d$r")" > .git
  echo "Worktree .git path repaired for WSL"
fi
```

## SQL Objects

SQL objects (TVFs, views, SPs) are applied **at startup** via `SqlObjectHelper.ApplyAllAsync()` — never inject in migrations.

## Core Principle

```
GOLDEN RULE: Retrieve SNAPSHOT + MIGRATIONS from PARENT branch

feature/* → snapshot + migrations from develop
develop   → snapshot + migrations from main
release/* → snapshot + migrations from main
hotfix/*  → snapshot + migrations from main
main      → BLOCKED (never squash)

WARNING: NEVER retrieve only the snapshot without the migrations!
```

## Workflow

1. **Determine**: Identify parent branch (`determine_base_branch`)
2. **List**: Migrations specific to this branch (not inherited)
3. **Confirm**: Request user validation
4. **Backup**: Save all files
5. **Fetch**: Retrieve **ModelSnapshot AND migrations** from parent branch (CRUCIAL!)
6. **Delete**: Remove branch-specific migrations ONLY
7. **Create**: Create consolidated migration (MCP mandatory)
8. **Validate**: Build + script OK

## Shared Functions

> **Reference:** See `references/shared-init-functions.md` for `ensure_dotnet_ef()`, `detect_efcore_project()`, `detect_dbcontext()`, `determine_base_branch()`, `block_production()`.

## Key Commands

> **IMPORTANT:** Steps 1-3 MUST run in a single Bash call to preserve shell variables ($BASE_BRANCH, $MIGRATIONS_DIR, $BRANCH_ONLY_MIGS, etc.). Step 4 (guard) and Step 5 (confirm) are separate calls. Step 6 requires variables from steps 1-3.

### Step 1: Initialize

```bash
# Determine parent branch
CURRENT_BRANCH=$(git branch --show-current)
case "$CURRENT_BRANCH" in
  feature/*) BASE_BRANCH="develop"; BRANCH_TYPE="feature" ;;
  release/*) BASE_BRANCH="main";    BRANCH_TYPE="release" ;;
  hotfix/*)  BASE_BRANCH="main";    BRANCH_TYPE="hotfix" ;;
  develop)   BASE_BRANCH="main";    BRANCH_TYPE="develop" ;;
  main|master) echo "BLOCKED: Cannot squash on main/master"; exit 1 ;;
  *)         BASE_BRANCH="develop"; BRANCH_TYPE="unknown" ;;
esac
echo "Branch: $CURRENT_BRANCH ($BRANCH_TYPE) → Base: $BASE_BRANCH"

# Context override from --context flag (passed via agent prompt)
CONTEXT_OVERRIDE="${CONTEXT:-}"
```

### Step 2: Detect project and migrations directory

```bash
# Find Infrastructure project and migrations directory
INFRA_PROJECT=$(find src -name "*Infrastructure.csproj" | head -1)
INFRA_DIR=$(dirname "$INFRA_PROJECT")
MIGRATIONS_DIR=$(find "$INFRA_DIR" -type d -name "Migrations" -path "*/Persistence/*" | head -1)

# Detect DbContext (override via --context flag, else auto-detect)
if [ "$CONTEXT_OVERRIDE" = "CoreDbContext" ] || [ "$CONTEXT_OVERRIDE" = "core" ]; then
  DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
elif [ "$CONTEXT_OVERRIDE" = "ExtensionsDbContext" ] || [ "$CONTEXT_OVERRIDE" = "extensions" ]; then
  DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
elif find src -type d -name "SmartStack.Domain" | grep -q .; then
  DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
else
  DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
fi

echo "Migrations: $MIGRATIONS_DIR"
echo "DbContext: $DBCONTEXT"
```

### Step 3: Identify branch-specific migrations

```bash
git fetch origin "$BASE_BRANCH"

# Get migration file paths RELATIVE TO GIT ROOT
MIGRATIONS_REL=$(realpath --relative-to="$(git rev-parse --show-toplevel)" "$MIGRATIONS_DIR")

# List parent branch migrations (names only, no Designer/Snapshot)
BASE_MIGS=$(git ls-tree -r --name-only "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/" 2>/dev/null | grep "\.cs$" | grep -v "Designer\|Snapshot" | xargs -I{} basename {} .cs)

# List local migrations (names only)
LOCAL_MIGS=$(find "$MIGRATIONS_DIR" -maxdepth 1 -name "*.cs" ! -name "*Designer*" ! -name "*Snapshot*" | xargs -I{} basename {} .cs)

# Calculate branch-specific migrations (in local but NOT in base)
BRANCH_ONLY_MIGS=""
for mig in $LOCAL_MIGS; do
  if ! echo "$BASE_MIGS" | grep -qx "$mig"; then
    BRANCH_ONLY_MIGS="$BRANCH_ONLY_MIGS $mig"
  fi
done
BRANCH_ONLY_MIGS=$(echo "$BRANCH_ONLY_MIGS" | xargs)  # trim

BRANCH_COUNT=$(echo "$BRANCH_ONLY_MIGS" | wc -w)
BASE_COUNT=$(echo "$BASE_MIGS" | wc -w)

echo ""
echo "Parent ($BASE_BRANCH): $BASE_COUNT migration(s)"
echo "Branch-specific: $BRANCH_COUNT migration(s)"
for mig in $BRANCH_ONLY_MIGS; do echo "  - $mig"; done
```

### Step 4: Guard — nothing to squash

```bash
if [ "$BRANCH_COUNT" -eq 0 ]; then
  echo "Nothing to squash — no branch-specific migrations found."
  exit 0
fi
if [ "$BRANCH_COUNT" -eq 1 ]; then
  echo "Only 1 branch-specific migration — squash not needed."
  exit 0
fi
```

### Step 5: Confirm (skip if `--force`)

If `--force` flag is NOT set, display the plan and ask confirmation:

```javascript
AskUserQuestion({
  question: "Squash will consolidate " + BRANCH_COUNT + " migrations into 1.\n\nMigrations to squash:\n" + BRANCH_ONLY_MIGS_LIST + "\n\nParent: " + BASE_BRANCH + " (" + BASE_COUNT + " migrations will be preserved)\n\nProceed?",
  header: "EF Core Squash",
  options: [
    { label: "Yes, squash", description: "Backup + consolidate" },
    { label: "Cancel", description: "Do nothing" }
  ]
})
```

### Step 6: Backup + Fetch + Delete + Create

```bash
# Backup
BACKUP_DIR=".claude/gitflow/backup/migrations/squash_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR" && cp "$MIGRATIONS_DIR"/*.cs "$BACKUP_DIR/"
echo "Backup: $BACKUP_DIR"

# ═══════════════════════════════════════════════════════════════════════════
# CRUCIAL: Retrieve SNAPSHOT + MIGRATIONS from parent branch
# ═══════════════════════════════════════════════════════════════════════════

# 1. Retrieve ModelSnapshot
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL"/*ModelSnapshot.cs
echo "Snapshot retrieved from origin/$BASE_BRANCH"

# 2. Retrieve ALL migrations from parent branch
for base_mig in $BASE_MIGS; do
  git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.cs" 2>/dev/null || true
  git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.Designer.cs" 2>/dev/null || true
done
echo "$BASE_COUNT migration(s) retrieved from origin/$BASE_BRANCH"

# 3. Delete branch-specific migrations only
for mig in $BRANCH_ONLY_MIGS; do
  rm -f "$MIGRATIONS_DIR"/*"${mig}"*.cs
  echo "  Deleted: $mig"
done

# 4. Create consolidated migration - MCP MANDATORY
mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE })
# Use the name returned by MCP in $MCP_MIGRATION_NAME:
dotnet ef migrations add "$MCP_MIGRATION_NAME" \
  --context "$DBCONTEXT" \
  --project "$INFRA_PROJECT" \
  --startup-project "$(find src -name "*.Api.csproj" | head -1)" \
  -o Persistence/Migrations

# NOTE: SQL objects are applied automatically at startup — no injection needed.

# 5. Validate
dotnet build && dotnet ef migrations script --idempotent --context "$DBCONTEXT" > /dev/null
```

## Safety Checks

- [ ] Parent branch identified
- [ ] main/master = BLOCKED
- [ ] User confirmation obtained
- [ ] Backup created
- [ ] **Parent snapshot retrieved** (origin/$BASE_BRANCH)
- [ ] **Parent migrations retrieved** (.cs AND .Designer.cs files)
- [ ] Branch-specific migrations deleted (not inherited ones!)
- [ ] Build OK after squash
- [ ] Script can be generated

## Output Format

```
SQUASH - {current_branch}
  Base:   origin/{BASE_BRANCH}

  Retrieved from {BASE_BRANCH}:
    ModelSnapshot
    {N} migrations (.cs + .Designer.cs files)

  Consolidated:
    Before: {M} migrations (branch-specific)
    After:  1 migration ({MCP_MIGRATION_NAME} via MCP)

  Backup: .claude/gitflow/backup/migrations/squash_{timestamp}/

Next: /efcore db-reset, /efcore db-deploy
```

## Edge Cases

| Situation | Action |
|-----------|--------|
| 0 branch-specific migrations | "Nothing to squash" - exit |
| Identical snapshot | Empty migration created - warning |
| Build fail | Auto-restore from backup |
| On main/master | BLOCKED - exit code 1 |

## Error Recovery

| Failure | Recovery Action |
|---------|----------------|
| Build fails after squash | Restore from backup: `cp "$BACKUP_DIR"/*.cs "$MIGRATIONS_DIR/"` |
| MCP unavailable | Check `.claude/mcp-status.json`, run `/mcp healthcheck` |
| Snapshot fetch fails | Verify `origin/$BASE_BRANCH` exists: `git fetch origin $BASE_BRANCH` |
| Script generation fails | Restore backup, then investigate migration content |
| SQL objects missing at runtime | SQL objects are applied at startup — restart the application |

**Automatic recovery:** On ANY failure after backup:
1. Restore all files from backup directory
2. Verify build passes after restore
3. Report what failed and why
4. Suggest corrective action before retrying

## Priority

Safety > Correctness > Speed.

1. Backup MANDATORY (unless --no-backup)
2. Parent snapshot MANDATORY (never optional)
3. Auto-restore on failure
