Browse Source

feat(skills): add proactive git hygiene checks to git-ops

status.sh now detects and warns on:
- Main checkout sitting on a feature branch (should be on trunk,
  feature work belongs in worktrees)
- Merged branches not yet deleted

SKILL.md gets a Hygiene Checks section documenting all four anti-
patterns (feature branch, stale merges, WIP commits, large pile)
with severity ratings, detection logic, and remediation steps.
Checks 1+2 are automated in status.sh; 3+4 are prose guidance
for Claude to surface during status reads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
3b1a525411
2 changed files with 96 additions and 0 deletions
  1. 51 0
      skills/git-ops/SKILL.md
  2. 45 0
      skills/git-ops/scripts/status.sh

+ 51 - 0
skills/git-ops/SKILL.md

@@ -73,6 +73,57 @@ For T1 operations, format results cleanly and present directly. Use `delta` for
 - User asks about worktrees, prunable branches, drift, "what can we clean up" → `worktree-survey.sh`
 - Both scripts exit 0 if clean, 1 if attention needed, 2 if not-a-repo — composable.
 
+## Hygiene Checks (Proactive — Run During Every T1 Status)
+
+When running any status check, scan for these anti-patterns and surface them **before** the status output. Don't wait for the user to notice. The `status.sh` script handles checks 1 and 2 automatically; checks 3 and 4 are Claude's responsibility.
+
+### Anti-pattern 1: Main checkout on a feature branch 🔴
+
+**Signal:** In the main checkout (not a worktree) and `git branch --show-current` ≠ the repo's default branch (`main`/`master`/`trunk`).
+
+**Why it's bad:** The main checkout is the fallback workspace. Feature branches sitting there block clean status reads, confuse worktree operations, and make it unclear what "current" state is. Feature work belongs in dedicated worktrees.
+
+**Flag it:** Emit a prominent warning before the status output.
+
+**Fix:**
+```bash
+git checkout main                                              # return main to trunk
+git worktree add .claude/worktrees/<name> <feature-branch>   # move work to worktree
+```
+
+**Detecting main checkout vs worktree:**
+```bash
+GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
+# ".git"               → main checkout  → check applies
+# contains "worktrees" → inside a worktree → skip this check
+```
+
+### Anti-pattern 2: Stale merged branches 🟡
+
+**Signal:** `git branch --merged <default>` returns branches other than the trunk.
+
+**Why it's bad:** Merged-but-undeleted branches are noise that obscures what's actually in flight.
+
+**Flag it:** Report the count. Suggest `git branch cleanup` to review and delete.
+
+### Anti-pattern 3: WIP commits on a pushed branch 🟡
+
+**Signal:** `git log --oneline @{u}..HEAD` contains subject lines matching `wip|WIP|todo|TODO|fixme|FIXME|temp|TEMP|hack|HACK`.
+
+**Why it's bad:** WIP markers in pushed history signal unfinished work that shouldn't have left the local machine. Creates confusing history and blocks clean PRs.
+
+**Flag it:** List the offending commits and suggest an interactive rebase to squash or rename.
+
+### Anti-pattern 4: Large uncommitted pile 🟡
+
+**Signal:** staged + unstaged + untracked > 20 files.
+
+**Why it's bad:** Large uncommitted diffs are hard to review, easy to lose, and signal a broken "commit as you go" habit.
+
+**Flag it:** Note the total and suggest committing incrementally by logical unit.
+
+---
+
 ### Tier 2: Safe Writes - Dispatch to Agent
 
 Gather relevant context, then dispatch to `git-agent` (background, Sonnet).

+ 45 - 0
skills/git-ops/scripts/status.sh

@@ -95,6 +95,45 @@ if command -v gh >/dev/null 2>&1 && [ "$BRANCH" != "(detached)" ]; then
   fi
 fi
 
+# --- Hygiene checks ---------------------------------------------------------
+# Detect if we're in the main checkout or a worktree
+GIT_DIR_REL=$(git rev-parse --git-dir 2>/dev/null)
+IS_WORKTREE=false
+case "$GIT_DIR_REL" in
+  *worktrees*) IS_WORKTREE=true ;;
+esac
+
+# Detect the repo's default branch
+DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
+if [ -z "$DEFAULT_BRANCH" ]; then
+  for b in main master trunk develop; do
+    if git show-ref --verify --quiet "refs/heads/$b" 2>/dev/null; then
+      DEFAULT_BRANCH="$b"
+      break
+    fi
+  done
+fi
+
+HYGIENE_FLAGS=""
+
+# Check 1: main checkout on a feature branch
+if [ "$IS_WORKTREE" = false ] && \
+   [ -n "$DEFAULT_BRANCH" ] && \
+   [ "$BRANCH" != "$DEFAULT_BRANCH" ] && \
+   [ "$BRANCH" != "(detached)" ]; then
+  HYGIENE_FLAGS="${HYGIENE_FLAGS}HYGIENE[1]: main checkout is on '$BRANCH' (default: '$DEFAULT_BRANCH') — feature work belongs in worktrees\n"
+fi
+
+# Check 2: stale merged branches
+if [ -n "$DEFAULT_BRANCH" ]; then
+  MERGED_COUNT=$(git branch --merged "$DEFAULT_BRANCH" 2>/dev/null \
+    | grep -v "^\*\|^\s*${DEFAULT_BRANCH}$\|^\s*master$\|^\s*main$\|^\s*trunk$" \
+    | grep -c . 2>/dev/null || echo 0)
+  if [ "$MERGED_COUNT" -gt 0 ]; then
+    HYGIENE_FLAGS="${HYGIENE_FLAGS}HYGIENE[2]: $MERGED_COUNT merged branch(es) not yet deleted — run: git branch --merged $DEFAULT_BRANCH\n"
+  fi
+fi
+
 # --- Output -----------------------------------------------------------------
 echo "repo:    $REPO_ROOT"
 echo "branch:  $BRANCH"
@@ -118,6 +157,12 @@ if [ -n "$PR_LINE" ]; then
   echo "pr:      $PR_LINE"
 fi
 
+# Hygiene warnings
+if [ -n "$HYGIENE_FLAGS" ]; then
+  echo ""
+  printf "%b" "$HYGIENE_FLAGS" | sed 's/^/⚠  /'
+fi
+
 # Fetch failure warning
 if [ "$FETCH_OK" = false ]; then
   if [ "$fetch_age" -ge 0 ]; then