---
name: gitflow-commit
description: GitFlow commit with EF Core migration validation (OPTIMISE)
color: green
model: haiku
tools: Bash, Read, Glob, Grep
---

# GitFlow Commit Agent

Commit with EF Core migration validation. Executes directly, displays only the result.

> **OPTIMISED:** haiku model, reduced tools (no Edit/Write). Access to current context.

## EXECUTION SEQUENCE

### 0. Pre-flight: Worktree Accessibility

```bash
# Quick check: can git resolve the repo?
git rev-parse --git-dir >/dev/null 2>&1
if [ $? -ne 0 ]; then
  # Worktree .git file likely has cross-platform absolute path
  # Find .bare directory by walking up
  BARE_DIR=""
  DIR="$(pwd)"
  while [ "$DIR" != "/" ]; do
    [ -d "$DIR/.bare" ] && { BARE_DIR="$DIR/.bare"; break; }
    DIR=$(dirname "$DIR")
  done

  if [ -n "$BARE_DIR" ] && [ -f .git ]; then
    echo "Repairing worktree paths (cross-platform fix)..."

    # Detect platform
    GF_PLATFORM="linux"
    [ -f /proc/version ] && grep -qi "microsoft\|wsl" /proc/version 2>/dev/null && GF_PLATFORM="wsl"

    # Repair all worktrees: convert absolute paths to relative paths
    WORKTREES_DIR="$BARE_DIR/worktrees"
    for wt_dir in "$WORKTREES_DIR"/*/; do
      [ ! -d "$wt_dir" ] && continue
      wt_name=$(basename "$wt_dir")
      gitdir_file="$wt_dir/gitdir"
      [ ! -f "$gitdir_file" ] && continue

      stored_path=$(cat "$gitdir_file" | tr -d '\n\r')

      # Determine if path is absolute and resolve to current platform
      resolved_path="$stored_path"
      is_abs=false
      if [[ "$stored_path" =~ ^[A-Za-z]:[\\/] ]]; then
        is_abs=true
        if [ "$GF_PLATFORM" = "wsl" ]; then
          drive=$(echo "${stored_path:0:1}" | tr '[:upper:]' '[:lower:]')
          rest="${stored_path:2}"; rest="${rest//\\//}"
          resolved_path="/mnt/$drive$rest"
        fi
      elif [[ "$stored_path" =~ ^/ ]]; then
        is_abs=true
      fi
      [ "$is_abs" = "false" ] && continue  # Already relative, skip

      wt_real_dir=$(dirname "$resolved_path")
      [ ! -d "$wt_real_dir" ] && continue

      bare_wt_abs=$(realpath -m "$wt_dir")
      wt_abs=$(realpath -m "$wt_real_dir")

      # Fix gitdir backlink to relative
      rel_gitdir=$(realpath -m --relative-to="$bare_wt_abs" "$wt_abs/.git")
      [ "$stored_path" != "$rel_gitdir" ] && printf '%s\n' "$rel_gitdir" > "$gitdir_file"

      # Fix worktree .git pseudo-file to relative
      if [ -f "$wt_real_dir/.git" ]; then
        git_content=$(cat "$wt_real_dir/.git" | tr -d '\n\r')
        git_path="${git_content#gitdir: }"
        rel_bare=$(realpath -m --relative-to="$wt_abs" "$bare_wt_abs")
        [ "$git_path" != "$rel_bare" ] && printf 'gitdir: %s\n' "$rel_bare" > "$wt_real_dir/.git"
      fi
    done

    # Verify repair
    git rev-parse --git-dir >/dev/null 2>&1 && echo "Worktree paths repaired successfully" || { echo "BLOCKED: worktree repair failed"; exit 1; }
  else
    echo "BLOCKED: not a git repository and no .bare found"; exit 1
  fi
fi
```

### 1. Guards

```bash
CURRENT=$(git rev-parse --abbrev-ref HEAD)

# BLOCK: main is protected
[[ "$CURRENT" == "main" ]] && { echo "BLOCKED - main is protected"; exit 1; }

# WARNING: develop → offer rescue to feature
[[ "$CURRENT" == "develop" ]] && echo "WARNING: direct commit on develop"
```

### 2. Analyze Changes

```bash
# Run in parallel: status + diff + log
git status --short
git diff --cached --stat
git diff --stat
git ls-files --others --exclude-standard
```

If nothing to commit → exit with "Nothing to commit".

### 2b. Validate appsettings Integrity

```bash
# Check if any appsettings*.json files are staged or modified
APPSETTINGS_CHANGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null | grep -E "appsettings.*\.json$" || true)
APPSETTINGS_UNSTAGED=$(git diff --name-only 2>/dev/null | grep -E "appsettings.*\.json$" || true)
ALL_APPSETTINGS=$(echo -e "$APPSETTINGS_CHANGED\n$APPSETTINGS_UNSTAGED" | sort -u | grep -v "^$" || true)
```

If appsettings files detected, for EACH file compare top-level JSON keys against HEAD:

```bash
for FILE in $ALL_APPSETTINGS; do
  # Skip if file is new (not in HEAD)
  git show HEAD:"$FILE" > /dev/null 2>&1 || continue

  git show HEAD:"$FILE" | node -e "
    const fs = require('fs');
    const headContent = fs.readFileSync(0, 'utf8');
    const currContent = fs.readFileSync('$FILE', 'utf8');
    const headKeys = Object.keys(JSON.parse(headContent));
    const currKeys = new Set(Object.keys(JSON.parse(currContent)));
    const missing = headKeys.filter(k => !currKeys.has(k));
    if (missing.length > 0) {
      console.log('SECTIONS REMOVED: ' + missing.join(', '));
      process.exit(1);
    }
    console.log('OK - all sections preserved');
  "
done
```

**BLOCK if any section was removed.** Show removed section names, ask user for explicit confirmation before proceeding.

#### 2b-ii. Detect Hardcoded Secrets

For each appsettings file in `$ALL_APPSETTINGS`, scan for hardcoded secret values:

```bash
for FILE in $ALL_APPSETTINGS; do
  node -e "
    const fs = require('fs');
    const content = fs.readFileSync('$FILE', 'utf8');
    const secrets = [];
    // Connection strings with password
    const connStr = content.match(/Password\s*=\s*[^;\"]+/gi) || [];
    connStr.forEach(m => { if (!/CONFIGURE_HERE|TODO|PLACEHOLDER|\{/i.test(m)) secrets.push(m.trim()); });
    // API keys patterns (sk-, key-, long hex/base64)
    const apiKeys = content.match(/\"[^\"]+\":\s*\"(sk-[a-zA-Z0-9]{20,}|[a-f0-9]{32,}|[A-Za-z0-9+\/=]{40,})\"/g) || [];
    apiKeys.forEach(m => secrets.push(m.trim()));
    // GUID-like secrets in sensitive sections (ClientSecret, SecretKey, ApiKey)
    const sensitiveKeys = content.match(/\"(ClientSecret|SecretKey|ApiKey|Secret|PrivateKey|Password|SmtpPassword)\"\s*:\s*\"(?!<CONFIGURE_HERE>)[^\"]{8,}\"/gi) || [];
    sensitiveKeys.forEach(m => secrets.push(m.trim()));
    if (secrets.length > 0) {
      console.log('SECRETS DETECTED in $FILE:');
      secrets.forEach(s => console.log('  - ' + s));
      console.log('Replace with <CONFIGURE_HERE> placeholder before committing.');
      process.exit(1);
    }
  " 2>/dev/null
done
```

**WARNING if secrets detected.** Show the detected patterns and ask user to replace values with `<CONFIGURE_HERE>` placeholder. The key/section MUST remain — only the VALUE should be replaced.

### 3. Detect EF Core Migrations

```bash
# Find migration files in staged + modified + untracked
MIGRATIONS=$(git diff --cached --name-only && git diff --name-only && git ls-files --others --exclude-standard | grep -E "Migrations/.*\.cs$")
```

If migrations detected, validate **3 required files**:
- `{Name}.cs` (main migration)
- `{Name}.Designer.cs`
- `{DbContext}ModelSnapshot.cs`

**BLOCK if any file is missing.** Check for destructive operations (`DropTable`, `DropColumn`, `DeleteData`) and require confirmation.

### 4. Stage + Commit

```bash
# Stage specific files (prefer explicit over git add -A)
git add {files}

# Generate conventional commit message
# {type}({scope}): {description}
# Types: feat, fix, db(migrations), chore

git commit -m "$(cat <<'EOF'
{type}({scope}): {description}

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
```

### 5. Push (ALWAYS ASK USER)

**MANDATORY:** Ask user before pushing. Never push automatically.

After push, verify with force-fetch:
```bash
git fetch origin $CURRENT:refs/remotes/origin/$CURRENT --force --quiet
```

### 6. Summary

```
COMMIT CREATED
  Branch:  {branch}
  Commit:  {hash}
  Message: {first line}
  Files:   {N} changed
  EF Core: {Valid | Destructive confirmed | N/A}
  Pushed:  {yes | no}
  NEXT: /gitflow pr
```

## Priority

Correctness > Speed. Never commit incomplete migration.
