Claude Skill

git-ops

Git + worktree orchestrator: status survey, per-worktree triage, commits, PRs, branches, releases, rebases — reads inline, writes to a background agent. Triggers on: git status, anything to commit, anything to push, commit, push, create PR, rebase, release, tag, changelog, worktr

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_git-ops-3dfaf0b.zip · 27 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/git-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Git Ops

Intelligent git operations orchestrator. Routes read-only queries inline for speed, dispatches write operations to a background Sonnet agent (git-agent) to free the main session.

Architecture

User intent (commit, PR, rebase, status, etc.)
    |
    +---> Tier 1: Read-only (status, log, diff, blame)
    |       |
    |       +---> Execute INLINE via Bash (fast, no subagent)
    |
    +---> Tier 2: Safe writes (commit, push, tag, PR, stash)
    |       |
    |       +---> Gather context from conversation
    |       +---> Dispatch to git-agent (background, Sonnet)
    |       |       +---> Fallback: general-purpose with inlined protocol
    |       +---> Agent executes and reports back
    |
    +---> Tier 3: Destructive (rebase, reset, force-push, branch -D)
            |
            +---> Dispatch to git-agent (background, Sonnet)
            |       +---> Fallback: general-purpose with inlined protocol
            +---> Agent produces PREFLIGHT REPORT (does NOT execute)
            +---> Orchestrator relays preflight to user
            +---> On confirmation: re-dispatch with execute authority

Safety Tiers

Tier 1: Read-Only - Run Inline

No subagent needed. Execute directly via Bash for instant results.

Operation Command
Status (rich) bash $HOME/.claude/skills/git-ops/scripts/status.sh — one-shot HEAD + sync + tree + worktrees + branches + PR
Worktree survey bash $HOME/.claude/skills/git-ops/scripts/worktree-survey.sh — per-worktree state, drift detection, prunable/WIP/ghost/orphan triage
Land-all plan bash $HOME/.claude/skills/git-ops/scripts/land-all.sh [--porcelain] — classifies every branch as LANDABLE/STALE/WIP/ACTIVE/MERGED for a batch land (see "Land all" below)
Status (bare) git status --short
Log git log --oneline -20
Diff (unstaged) git diff --stat
Diff (staged) git diff --cached --stat
Diff (full) git diff [file] or git diff --cached [file]
Branch list git branch -v
Remote branches git branch -rv
Stash list git stash list
Blame git blame [file]
Show commit git show [hash] --stat
Reflog git reflog --oneline -20
Tags git tag --list --sort=-v:refname
Worktree list git worktree list
PR list gh pr list
PR status gh pr view [N]
Issue list gh issue list
CI checks gh pr checks [N]
Run status gh run list --limit 5

For T1 operations, format results cleanly and present directly. Use delta for diffs when available.

When to reach for the bundled scripts:

  • User asks "status", "where are we", "anything to commit", "anything to push" → status.sh
  • 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:

git checkout main                                              # return main to trunk
git worktree add .claude/worktrees/<name> <feature-branch>   # move work to worktree

Detecting main checkout vs worktree:

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).

Context gathering before dispatch:

Operation Context to Gather
Commit What the user has been working on (from conversation), staged files, recent commit style
Push Current branch, tracking info, commits ahead of remote
PR create All commits on branch vs main, conversation context for description
Tag/release Commits since last tag, version pattern in use
Stash Current changes, user's stash message if provided
Cherry-pick Target commit details, current branch
Branch create Base branch, naming convention from recent branches
gh issue create User description, labels if mentioned

Dispatch template:

You are the git-agent handling a Tier 2 (safe write) operation.

## Domain Knowledge
For release or PR operations, read CI context first:
- Read: skills/ci-cd-ops/SKILL.md (release workflows, PR conventions)

## Context
- Current branch: {branch}
- Repository: {repo info}
- User intent: {what the user asked for}
- Conversation context: {relevant summary of what was being worked on}

## Operation
{specific operation details}

## Project Conventions
{commit style, branch naming, PR template if detected}

Execute the operation following your T2 protocol (verify state, execute, confirm, report).

Tier 3: Destructive - Preflight Required

Dispatch to git-agent with explicit instruction to produce a preflight report ONLY.

Dispatch template (preflight):

You are the git-agent handling a Tier 3 (destructive) operation.

## Context
- Current branch: {branch}
- Repository: {repo info}
- User intent: {what the user asked for}

## Operation
{specific operation details}

IMPORTANT: Do NOT execute this operation. Produce a T3 Preflight Report only.
Show exactly what will happen, what the risks are, and how to recover.

After user confirms: Re-dispatch with execute authority:

User has confirmed the Tier 3 operation after reviewing the preflight.

## Approved Operation
{exact operation from preflight}

## Confirmation
Proceed with execution. Follow T3 execution protocol:
1. Create a safety bookmark: note the current HEAD hash
2. Execute the operation
3. Verify the result
4. Report with the safety bookmark for recovery

Dispatch Mechanics

Background Agent (Default for T2/T3)

# Dispatch to git-agent, runs in background, Sonnet model
Agent(
    subagent_type="git-agent",
    model="sonnet",
    run_in_background=True,  # Frees main session
    prompt="..."             # From dispatch templates above
)

The main session continues working while the agent handles git operations. Results arrive asynchronously.

Foreground Agent (When Result Needed Immediately)

For operations where the user is waiting on the result (e.g., "commit this then let's move on"):

Agent(
    subagent_type="git-agent",
    model="sonnet",
    run_in_background=False,  # Wait for result
    prompt="..."
)

Worktree Isolation (Only When Requested)

When the user explicitly asks for worktree isolation (e.g., "do this in a separate worktree", "prepare a branch without touching my working tree"):

Agent(
    subagent_type="git-agent",
    model="sonnet",
    isolation="worktree",     # Isolated repo copy
    run_in_background=True,
    prompt="..."
)

Fallback: When git-agent Is Unavailable

If git-agent is not registered as a subagent type (e.g., plugin not installed, agent files missing), fall back to general-purpose with the git-agent identity inlined in the prompt.

Detection: If dispatching to git-agent fails or the subagent type is not listed in available agents, switch to fallback mode automatically.

Fallback dispatch template:

Agent(
    subagent_type="general-purpose",  # Fallback
    model="sonnet",
    run_in_background=True,
    prompt="""You are acting as a git operations agent. You are precise, safety-conscious,
and follow the three-tier safety system:
- T1 (read-only): execute freely
- T2 (safe writes): execute on instruction, verify before and after
- T3 (destructive): preflight report only unless explicitly told to execute

{original dispatch prompt here}
"""
)

Key differences from primary dispatch:

  • Uses general-purpose instead of git-agent subagent type
  • Inlines the safety tier protocol directly in the prompt (the agent won't have git-agent's system prompt)
  • Everything else stays the same - context gathering, templates, foreground/background choice

When to use each:

Condition Dispatch Method
git-agent available Primary: subagent_type="git-agent"
git-agent unavailable Fallback: subagent_type="general-purpose" with inlined protocol
No agent dispatch possible Last resort: execute T2 operations inline (main context)

The last-resort inline path should only be used for simple T2 operations (single commit, simple push). Complex workflows (PR creation, release, changelog) should always use an agent.

Extended Operations

Release Workflow

git-ops owns the local half of releases only — analysing commits, generating CHANGELOG content, creating the local tag. The remote half (push, gh release create, repo metadata) belongs to the github-ops skill.

When user asks to "create a release", "bump version", or "tag a release":

  1. Inline (T1): Check current version and commits since last tag

    git describe --tags --abbrev=0 2>/dev/null
    git log --oneline $(git describe --tags --abbrev=0 2>/dev/null)..HEAD
    
  2. Determine version bump:

    • feat: commits -> minor bump
    • fix: commits -> patch bump
    • BREAKING CHANGE: or !: -> requires explicit user approval (never auto-major)
    • Or use version specified by user
  3. Dispatch to git-agent (T2): Generate CHANGELOG content + create local tag.

  4. Hand off to github-ops for the remote half: push commits, push tag, create the GitHub release with notes, update repo metadata if warranted. Do not call gh release create from git-ops — that crosses the local/remote boundary. See skills/github-ops/SKILL.md mode update.

Changelog Generation

When user asks to "generate changelog" or "update CHANGELOG.md":

  1. Inline (T1): Gather commit history for the range
  2. Dispatch to git-agent (T2): Categorise commits, format as Keep a Changelog, write file

PR Workflow (Full Cycle)

When user says "create a PR" or "open a PR":

  1. Inline (T1): Check branch state, diff against main
  2. Gather context: What was the user working on? What does the conversation tell us about the goal?
  3. Dispatch to git-agent (T2): Create PR with contextual title and body
  4. Report: PR number, URL, summary

Branch Cleanup

When user asks to "clean up branches" or "delete merged branches":

  1. Inline (T1): List merged branches
    git branch --merged main | grep -v "main\|master\|\*"
    
  2. Show list to user - this is a T3 preflight (deletion)
  3. On confirmation: Dispatch to git-agent to delete them

Semantic Versioning Analysis

When user asks "what should the next version be":

  1. Inline (T1): Analyse commits since last tag
  2. Categorise by Conventional Commits
  3. Report recommended bump with reasoning

Conflict Resolution Support

When user encounters merge conflicts:

  1. Inline (T1): git status to show conflicted files
  2. Inline (T1): Read conflict markers in each file
  3. Present options: ours, theirs, manual resolution
  4. After resolution: Dispatch to git-agent (T2) for staging and continue

Worktree Operations

Worktrees are first-class in this skill. The classification is:

Op Tier How
Survey T1 bash scripts/worktree-survey.sh — read-only, reports per-worktree state + drift
New lane T2 (inline) bash scripts/new-lane.sh [--sibling] <slug> [base] — fast scripted provisioning: branch lane/<slug> in-repo at .claude/worktrees/<slug> (gitignored; --sibling for an outside <repo>-<slug> instead) + carries over gitignored env files. The one-command collision remedy — see "Lane provisioning" below
Create (bespoke) T2 git worktree add <path> -b <branch> via agent — for non-standard layouts the script doesn't cover
Land T2 Rebase worktree branch onto trunk + test + fast-forward. Multi-step procedure — see "Worktree Land Procedure" below
Prune (clean) T2 git worktree prune for ghost entries (registered but FS-missing). Always safe, no data loss possible
Remove T3 git worktree remove <path> — destroys filesystem state. Requires preflight + explicit confirm per worktree

Lane provisioning (the collision remedy)

scripts/new-lane.sh <slug> [base-branch] is the fast, model-invocable way to isolate parallel work — the remedy the peer-writer guards (session-start-unicode-scan.sh at boot, pre-write-peer-guard.sh mid-session) point you to. It:

  • creates branch lane/<slug> in-repo at <main>/.claude/worktrees/<slug> — the native Claude Code worktree location: tidy (no sibling dirs scattered across the parent) and gitignored so git add -A can't stage its gitlinks — off [base-branch] (default: current branch);
  • ensures the gitignore precondition: if .claude/worktrees/ isn't gitignored it adds the entry first (the in-repo location is only safe when ignored), so the default is safe in any repo;
  • --sibling places it outside the repo at <repo>/../<repo>-<slug> instead — use when you need structural isolation from repo-scoped destructive ops (git clean -ff, rm -rf <repo>) or in a repo that can't gitignore the dir;
  • anchors at the main worktree root, so invoking it from inside a lane won't nest worktrees;
  • carries over gitignored env files (.dev.vars, .env*, .secrets) the fresh worktree would otherwise lack, so the lane runs immediately;
  • prints the worktree path on stdout (everything else on stderr), so it composes: cd "$(bash scripts/new-lane.sh hotfix main)";
  • refuses if the branch or path already exists — never clobbers.

Lane work durability: committed lane work lives in the shared object store and survives even deletion of the worktree dir (recover via git worktree add <path> lane/<slug>); only uncommitted work is at risk from git clean -ff / rm -rf. Land early/often — see rules/worktree-boundaries.md.

Run it inline (deterministic, non-destructive); land the lane back via the Worktree Land Procedure below or fleet-ops. Reach for it whenever two sessions would otherwise share one checkout.

Survey-first discipline

Never recommend prune/remove without first running scripts/worktree-survey.sh and presenting the output to the user. The survey categorises each worktree as:

  • (trunk) — the main repo itself, never prune
  • PRUNABLE — merged into trunk, no uncommitted work, no unpushed commits → safe to remove
  • has WIP — uncommitted changes → commit or stash first, never auto-remove
  • unpushed — commits ahead of upstream → push or cherry-pick before remove
  • in-flight — not merged, not dirty → probably still in active use
  • GHOST — registered but filesystem gone → git worktree prune fixes
  • UNREGISTERED / orphan — filesystem dir with no git entry → DO NOT touch without explicit review

Worktree Land Procedure (T2)

For landing a branch from a worktree onto the trunk (rebase + test + ff):

  1. Verify preconditions: worktree clean, branch ahead of trunk, not already merged
  2. Fetch trunk, rebase worktree branch onto it
  3. Run project test command (detect from package.json / pyproject.toml / justfile)
  4. On test pass: fast-forward trunk to the rebased tip
  5. Do NOT push — that's a separate explicit step (and should go through push-gate)

Dispatch this to git-agent as a T2 operation with the worktree path + trunk name.

Land all — batch-land every pending lane (T1 plan → fleet-ops execution)

The front-door for "I've got 4-5 chips/sessions/worktrees, land the ones that are done." git-ops discovers and classifies; fleet-ops executes the sequential, test-gated landing. No duplicated landing logic — the two compose.

Triggers: "land everything", "land all my worktrees", "land the pending chips", "clean up and land what's done", "where are we and land it".

Procedure:

  1. Survey (T1, read-only). Run scripts/land-all.sh --porcelain (add --recent-days N if the user's lanes span longer than a week). Each candidate branch is classified:

    Status Meaning Default action
    LANDABLE clean, ahead, not merged, recent, no live writer land
    STALE clean + ahead but last commit > --recent-days old park (offer to prune/archive, or land explicitly)
    WIP uncommitted tracked changes park — commit in-lane first
    ACTIVE a session is writing it right now (recent file activity) never land — park
    MERGED already an ancestor of trunk (incl. nothing ahead) prune candidate
  2. Confirm the plan (AskUserQuestion, HARD RULE). Present the three groups — land these LANDABLE / park these WIP+ACTIVE+STALE / prune these MERGED — and get explicit go. Never skip this: landing is outward-facing on the trunk. Surface far behind (N) notes so the user knows which lands may conflict.

  3. Execute via fleet-ops. For the confirmed landable set:

    bash $HOME/.claude/skills/fleet-ops/scripts/fleet.sh track <landable-branches...>
    bash $HOME/.claude/skills/fleet-ops/scripts/fleet.sh land --all --running
    

    fleet-ops lands oldest-first, runs the test gate, auto-rebases the remaining lanes after each land, and marks any lane that hits a real conflict CONFLICT — it does not guess a resolution. This is a T2 write; dispatch through git-agent or run inline if the user is waiting.

  4. Escalate conflicts, don't auto-resolve. A CONFLICT lane stops being landed and is reported. Offer: resolve in the lane, skip it, or revert (fleet revert <branch>). Sequential + auto-rebase minimises conflicts (each lane rebases onto a trunk that already has the prior lands); genuine semantic conflicts are always the user's call.

  5. Offer cleanup (survey-first, T3). After landing, the MERGED branches and any now-landed worktrees are prune candidates. Follow Survey-first discipline + the T3 Remove preflight — never auto-git worktree remove; confirm per worktree. Respect rules/worktree-boundaries.md throughout: ACTIVE/orphan/unregistered trees are never touched.

Safety invariants: ACTIVE lanes (live peer writer) are never landed — this is the worktree-boundaries live-writer guard applied to landing. WIP needs an explicit commit first. STALE needs explicit opt-in (--recent-days or naming the branch). Only LANDABLE lands by default.

Boundaries (HARD RULE)

See rules/worktree-boundaries.md. Summary:

  • Never rm -rf .claude/worktrees/ — the orphan count in survey is informational, never a cleanup cue
  • Never git add -A when .claude/worktrees/ has untracked entries (sweeps gitlinks into commits)
  • Never decide another session's worktree is "orphaned" — ask first
  • Cross-project work stays cross-project; a worktree in repo X is never our concern when we're operating on repo Y

Decision Logic

When a git-related request arrives, follow this flow:

1. Classify the operation tier (T1/T2/T3)

2. If T1:
   - Execute inline via Bash
   - Format and present results
   - DONE

3. If T2:
   - Gather context (conversation, git state, conventions)
   - Decide foreground vs background:
     * User waiting on result? -> foreground
     * User continuing other work? -> background
   - Dispatch to git-agent with context
   - Relay result when received

4. If T3:
   - Gather context
   - Dispatch to git-agent for PREFLIGHT ONLY
   - Present preflight report to user
   - Wait for explicit confirmation
   - Re-dispatch with execute authority
   - Relay result

Quick Reference

Task Tier Inline/Agent
Check status (rich) T1 Inline (scripts/status.sh)
Worktree survey T1 Inline (scripts/worktree-survey.sh)
View diff T1 Inline
View log T1 Inline
List PRs T1 Inline
Commit T2 Agent
Push T2 Agent
Create PR T2 Agent
Create tag T2 Agent
Create release T2 Agent
Stash push/pop T2 Agent
Cherry-pick T2 Agent
Create branch T2 Agent
Create worktree T2 Agent
Land worktree T2 Agent (rebase + test + ff)
Prune ghost worktrees T2 Agent (git worktree prune)
Rebase T3 Agent (preflight)
Force push T3 Agent (preflight)
Reset --hard T3 Agent (preflight)
Delete branch T3 Agent (preflight)
Discard changes T3 Agent (preflight)
Merge to main T3 Agent (preflight)
Remove worktree T3 Agent (preflight per worktree)

Tools

Tool Purpose
git All git operations
gh GitHub CLI - PRs, issues, releases, actions
delta Syntax-highlighted diffs (if available)
lazygit Interactive TUI (suggest to user, not for agent)

Additional Resources

For detailed patterns, load:

  • ./references/rebase-patterns.md - Interactive rebase workflows and safety
  • ./references/stash-patterns.md - Stash operations and workflows
  • ./references/advanced-git.md - Bisect, cherry-pick, worktrees, reflog, conflicts
Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • advanced-git.md 4.4 KB
      # Advanced Git Operations
      
      Git bisect, cherry-pick, worktrees, reflog, and conflict resolution.
      
      ## Git Bisect
      
      Find the commit that introduced a bug using binary search.
      
      ### Basic Bisect
      
      ```bash
      # Start bisect
      git bisect start
      
      # Mark current commit as bad
      git bisect bad
      
      # Mark known good commit
      git bisect good v1.0.0
      
      # Git checks out middle commit, test it, then:
      git bisect good  # if this commit is OK
      git bisect bad   # if this commit has the bug
      
      # Repeat until git finds the culprit
      # "abc123 is the first bad commit"
      
      # End bisect session
      git bisect reset
      ```
      
      ### Automated Bisect
      
      ```bash
      # Run a test script automatically
      git bisect start HEAD v1.0.0
      git bisect run npm test
      # Git will find first failing commit automatically
      
      # With custom script
      git bisect run ./test-for-bug.sh
      # Script should exit 0 (good) or 1 (bad)
      ```
      
      ### Bisect Commands
      
      ```bash
      # Skip current commit (can't test it)
      git bisect skip
      
      # View bisect log
      git bisect log
      
      # Replay a bisect session
      git bisect replay bisect.log
      
      # Visualize remaining commits
      git bisect visualize
      ```
      
      ---
      
      ## Cherry-Pick
      
      Apply specific commits to current branch.
      
      ```bash
      # Apply single commit
      git cherry-pick abc123
      
      # Apply multiple commits
      git cherry-pick abc123 def456
      
      # Apply range of commits (exclusive start)
      git cherry-pick abc123..xyz789
      
      # Apply range (inclusive)
      git cherry-pick abc123^..xyz789
      ```
      
      ### Cherry-Pick Options
      
      ```bash
      # Stage only, don't commit
      git cherry-pick -n abc123
      
      # Edit commit message
      git cherry-pick -e abc123
      
      # Record original commit in message
      git cherry-pick -x abc123
      # Adds: "(cherry picked from commit abc123)"
      
      # Preserve original author
      git cherry-pick --signoff abc123
      ```
      
      ### Handling Conflicts
      
      ```bash
      # Continue after resolving conflicts
      git cherry-pick --continue
      
      # Abort cherry-pick
      git cherry-pick --abort
      
      # Skip current commit
      git cherry-pick --skip
      ```
      
      ---
      
      ## Worktrees
      
      Work on multiple branches simultaneously without stashing.
      
      ```bash
      # Create worktree for existing branch
      git worktree add ../project-hotfix hotfix-branch
      
      # Create worktree with new branch
      git worktree add ../project-feature -b new-feature
      
      # Create worktree from specific commit
      git worktree add ../project-v1 v1.0.0
      
      # List worktrees
      git worktree list
      
      # Remove worktree
      git worktree remove ../project-hotfix
      
      # Prune stale worktree info
      git worktree prune
      ```
      
      ### Worktree Workflow
      
      ```bash
      # Main repo at ~/project
      # Need to work on hotfix while keeping feature work
      cd ~/project
      git worktree add ~/project-hotfix hotfix-branch
      
      # Work in hotfix
      cd ~/project-hotfix
      # ... make fixes, commit, push ...
      
      # Back to main work
      cd ~/project
      git worktree remove ~/project-hotfix
      ```
      
      ---
      
      ## Reflog (Recovery)
      
      Find and recover "lost" commits.
      
      ```bash
      # Show reflog (all HEAD movements)
      git reflog
      
      # Show reflog for specific branch
      git reflog show feature-branch
      
      # Show reflog with dates
      git reflog --date=relative
      ```
      
      ### Recovery Scenarios
      
      #### Recover Deleted Branch
      
      ```bash
      git reflog
      # Find commit hash before deletion
      git checkout -b recovered-branch abc123
      ```
      
      #### Undo a Rebase
      
      ```bash
      git reflog
      # Find commit before rebase started (look for "rebase: start")
      git reset --hard HEAD@{5}
      ```
      
      #### Recover After Hard Reset
      
      ```bash
      git reflog
      # Find the commit you want
      git reset --hard HEAD@{1}
      ```
      
      #### Find Lost Commits
      
      ```bash
      # Show all dangling commits
      git fsck --lost-found
      
      # Check reflog for specific date
      git reflog --date=local | grep "Dec 15"
      ```
      
      ---
      
      ## Conflict Resolution
      
      ```bash
      # See which files have conflicts
      git status
      
      # View conflict markers in file
      cat file.txt
      # <<<<<<< HEAD
      # your changes
      # =======
      # their changes
      # >>>>>>> branch-name
      ```
      
      ### Resolution Strategies
      
      ```bash
      # Use merge tool
      git mergetool
      
      # Accept all changes from one side
      git checkout --ours file.txt    # Keep current branch
      git checkout --theirs file.txt  # Keep incoming branch
      
      # Accept both (for non-overlapping changes)
      git checkout --merge file.txt
      ```
      
      ### After Resolving
      
      ```bash
      # Stage resolved file
      git add file.txt
      
      # Continue operation
      git rebase --continue   # During rebase
      git merge --continue    # During merge
      git cherry-pick --continue  # During cherry-pick
      ```
      
      ### Conflict Prevention
      
      ```bash
      # Before merging, check for conflicts
      git merge --no-commit --no-ff feature-branch
      git diff --cached  # Review what would be merged
      git merge --abort  # If you don't want to proceed
      
      # Rebase frequently to avoid big conflicts
      git fetch origin
      git rebase origin/main
      ```
      
    • rebase-patterns.md 2.5 KB
      # Interactive Rebase Patterns
      
      Clean up commit history before merging.
      
      ## Basic Rebase
      
      ```bash
      # Rebase last N commits
      git rebase -i HEAD~5
      
      # Rebase onto main
      git rebase -i main
      
      # Commands in interactive rebase:
      # pick   - use commit as-is
      # reword - edit commit message
      # edit   - stop to amend commit
      # squash - meld into previous commit (keep message)
      # fixup  - meld into previous (discard message)
      # drop   - remove commit
      ```
      
      ## Common Rebase Workflows
      
      ### Squash Feature Commits
      
      ```bash
      # Squash all feature commits into one
      git rebase -i main
      # Change all but first 'pick' to 'squash'
      
      # Example edit:
      # pick abc123 Add feature base
      # squash def456 Fix typo
      # squash ghi789 Add tests
      # squash jkl012 Polish UI
      ```
      
      ### Reorder Commits
      
      ```bash
      git rebase -i HEAD~3
      # Move lines to change order
      
      # Before:
      # pick abc123 Add tests
      # pick def456 Add feature
      # pick ghi789 Fix bug
      
      # After (reordered):
      # pick def456 Add feature
      # pick ghi789 Fix bug
      # pick abc123 Add tests
      ```
      
      ### Edit a Commit Mid-History
      
      ```bash
      git rebase -i HEAD~5
      # Change 'pick' to 'edit' for target commit
      
      # Git stops at that commit
      # Make changes
      git add .
      git commit --amend
      git rebase --continue
      ```
      
      ### Split a Commit
      
      ```bash
      git rebase -i HEAD~3
      # Change 'pick' to 'edit' for commit to split
      
      # Git stops at that commit
      git reset HEAD^  # Undo commit but keep changes
      git add file1.js
      git commit -m "Part 1: Add file1"
      git add file2.js
      git commit -m "Part 2: Add file2"
      git rebase --continue
      ```
      
      ## Rebase Safety
      
      ```bash
      # Continue after resolving conflicts
      git rebase --continue
      
      # Skip current commit (use with caution)
      git rebase --skip
      
      # Abort if things go wrong
      git rebase --abort
      
      # Check reflog if you need to recover
      git reflog
      ```
      
      ## Rebase vs Merge
      
      | Scenario | Use |
      |----------|-----|
      | Feature branch → main | Rebase for clean history |
      | Shared/public branches | Merge (don't rewrite history) |
      | Long-lived feature branch | Rebase onto main periodically |
      | After code review changes | Rebase to clean up |
      
      ## Autosquash
      
      ```bash
      # Create fixup commit (will auto-squash)
      git commit --fixup=abc123
      
      # Create squash commit with message
      git commit --squash=abc123
      
      # Rebase with autosquash
      git rebase -i --autosquash main
      # Fixup commits auto-positioned after their targets
      ```
      
      ## Rebase Configuration
      
      ```bash
      # Enable autosquash by default
      git config --global rebase.autosquash true
      
      # Use vim for rebase editor
      git config --global sequence.editor vim
      
      # Preserve merge commits during rebase
      git rebase -i --rebase-merges main
      ```
      
    • stash-patterns.md 2.3 KB
      # Stash Patterns
      
      Save work temporarily without committing.
      
      ## Basic Stash Operations
      
      ```bash
      # Save current changes
      git stash
      
      # Save with description
      git stash push -m "WIP: feature X"
      
      # Stash including untracked files
      git stash -u
      
      # Stash including ignored files
      git stash -a
      
      # Stash only staged changes
      git stash push --staged
      ```
      
      ## Managing Stashes
      
      ```bash
      # List all stashes
      git stash list
      
      # Show stash contents (summary)
      git stash show stash@{0}
      
      # Show stash contents (full diff)
      git stash show -p stash@{0}
      
      # Show stash with stats
      git stash show --stat stash@{0}
      ```
      
      ## Applying Stashes
      
      ```bash
      # Apply most recent stash (keep in stash list)
      git stash apply
      
      # Apply and remove from list
      git stash pop
      
      # Apply specific stash
      git stash apply stash@{2}
      
      # Apply specific stash and drop
      git stash pop stash@{2}
      ```
      
      ## Removing Stashes
      
      ```bash
      # Drop specific stash
      git stash drop stash@{1}
      
      # Drop most recent stash
      git stash drop
      
      # Clear all stashes
      git stash clear
      ```
      
      ## Common Workflows
      
      ### Switch Branches Mid-Work
      
      ```bash
      # Mid-feature, need to switch branches
      git stash push -m "WIP: auth flow"
      git checkout hotfix-branch
      # ... fix bug, commit, push ...
      git checkout feature-branch
      git stash pop
      ```
      
      ### Partial Stashing
      
      ```bash
      # Stash specific files
      git stash push -m "WIP" -- file1.js file2.js
      
      # Interactive stash (select hunks)
      git stash push -p
      # y - stash this hunk
      # n - don't stash this hunk
      # s - split into smaller hunks
      # q - quit (stash selected hunks)
      ```
      
      ### Stash to Branch
      
      ```bash
      # Create branch from stash
      git stash branch new-feature stash@{0}
      # Creates branch, checks it out, applies stash, drops stash
      ```
      
      ### Apply Stash to Different Branch
      
      ```bash
      git checkout target-branch
      git stash apply stash@{1}
      # Resolve any conflicts
      git add .
      git commit
      ```
      
      ## Stash Conflicts
      
      ```bash
      # If stash apply/pop has conflicts
      git stash apply
      # CONFLICT messages appear
      
      # Resolve conflicts manually, then
      git add .
      git stash drop  # Remove the stash after resolving
      
      # Or abort and keep stash
      git checkout -- .  # Discard changes
      ```
      
      ## Tips
      
      ```bash
      # Always use descriptive messages
      git stash push -m "WIP: halfway through refactoring auth"
      
      # Check what's in stash before applying
      git stash show -p stash@{0}
      
      # Don't let stash list grow too long
      git stash list  # Review periodically
      git stash drop stash@{5}  # Clean old stashes
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
    • land-all.sh 8.7 KB
      #!/bin/bash
      # land-all.sh — read-only survey that classifies every branch/worktree by HOW it
      # should land on trunk, so the git-ops "land all" front-door can plan a batch
      # and hand the landable set to fleet-ops (sequential, test-gated landing).
      #
      # NEVER mutates. It inspects worktrees but never writes, moves, or deletes them.
      # Respects rules/worktree-boundaries.md. The actual landing is delegated to
      # fleet-ops — this script only decides what SHOULD land, park, or prune.
      #
      # Classification (per local branch, trunk excluded):
      #   LANDABLE  clean, ahead, not merged, recent, no live writer   → land it
      #   STALE     clean, ahead, not merged, but old (last commit >    → prune/archive
      #             --recent-days ago) — abandoned branch, not auto-landed  or land explicitly
      #   WIP       uncommitted tracked changes                        → commit first, park
      #   ACTIVE    a session is writing it right now (recent activity) → park, don't touch
      #   MERGED    already an ancestor of trunk (incl. nothing-ahead)  → prune candidate
      #
      # Usage:
      #   bash land-all.sh                     # human panel for current repo
      #   bash land-all.sh --porcelain         # TSV for the skill to parse (no header/summary)
      #   bash land-all.sh <repo-path> [flags]
      #
      # Flags:
      #   --porcelain          machine-readable TSV, one line per candidate:
      #                          STATUS \t BRANCH \t WORKTREE \t AHEAD \t BEHIND \t AGE \t NOTE
      #   --active-window N     seconds; a worktree touched within N seconds counts as
      #                         ACTIVE (live writer). Default 120. 0 disables detection.
      #   --recent-days N       a branch whose last commit is within N days is current
      #                         work (LANDABLE); older clean branches are STALE. Default 7.
      #
      # Exit codes:
      #   0  nothing landable (no LANDABLE candidates)
      #   1  at least one LANDABLE candidate (a batch land is available)
      #   2  not a git repo
      
      set -u
      
      REPO="$PWD"
      PORCELAIN=0
      ACTIVE_WINDOW=120
      RECENT_DAYS=7
      
      while [ $# -gt 0 ]; do
        case "$1" in
          --porcelain)     PORCELAIN=1; shift ;;
          --active-window) ACTIVE_WINDOW="${2:-120}"; shift 2 ;;
          --recent-days)   RECENT_DAYS="${2:-7}"; shift 2 ;;
          -h|--help)       sed -n '2,42p' "$0"; exit 0 ;;
          -*)              echo "unknown flag: $1" >&2; exit 2 ;;
          *)               REPO="$1"; shift ;;
        esac
      done
      
      if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "not-a-repo: $REPO" >&2
        exit 2
      fi
      
      REPO_ROOT=$(git -C "$REPO" rev-parse --show-toplevel)
      cd "$REPO_ROOT" || exit 2
      
      # Detect trunk branch (main preferred, then master).
      TRUNK="main"
      if ! git rev-parse --verify main >/dev/null 2>&1; then
        git rev-parse --verify master >/dev/null 2>&1 && TRUNK="master"
      fi
      
      NOW=$(date +%s)
      ACTIVE_THRESHOLD=$((NOW - ACTIVE_WINDOW))
      RECENT_THRESHOLD=$((NOW - RECENT_DAYS * 86400))
      
      # Which branch is checked out in the MAIN checkout (REPO_ROOT)?
      MAIN_BRANCH=$(git -C "$REPO_ROOT" symbolic-ref --short HEAD 2>/dev/null || echo "")
      
      # Map branch -> worktree path from `git worktree list --porcelain`.
      # (awk parses porcelain records; "branch refs/heads/" prefix is 18 chars.)
      TMP_WT=$(mktemp)
      git worktree list --porcelain 2>/dev/null | awk '
        /^worktree /{p=substr($0,10); next}
        /^branch /  {b=substr($0,19); if (b!="") print b"\t"p; next}
      ' > "$TMP_WT"
      
      worktree_for() {
        # Echo the worktree path checked out on branch $1 (empty if none).
        awk -F'\t' -v want="$1" '$1==want{print $2; exit}' "$TMP_WT"
      }
      
      # Is a worktree being actively written? True if any tracked-tree file was
      # modified within ACTIVE_WINDOW seconds. -print -quit stops at the first hit.
      # --active-window 0 disables live-writer detection entirely.
      worktree_active() {
        local wt=$1
        [ "$ACTIVE_WINDOW" -le 0 ] 2>/dev/null && return 1
        [ -n "$wt" ] && [ -d "$wt" ] || return 1
        local hit
        hit=$(find "$wt" -type f -not -path '*/.git/*' -newermt "@$ACTIVE_THRESHOLD" -print -quit 2>/dev/null)
        [ -n "$hit" ]
      }
      
      # Does a worktree have uncommitted TRACKED changes? (untracked files don't block a
      # land — the merge takes committed history — but we surface them in the note.)
      worktree_dirty() {
        local wt=$1
        [ -n "$wt" ] && [ -d "$wt" ] || return 1
        ! git -C "$wt" diff --quiet 2>/dev/null || ! git -C "$wt" diff --cached --quiet 2>/dev/null
      }
      
      # Counters
      N_LANDABLE=0; N_STALE=0; N_WIP=0; N_ACTIVE=0; N_MERGED=0
      
      emit() {
        # STATUS BRANCH WORKTREE AHEAD BEHIND AGE NOTE
        local status=$1 branch=$2 wt=$3 ahead=$4 behind=$5 age=$6 note=$7
        if [ "$PORCELAIN" -eq 1 ]; then
          printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$status" "$branch" "$wt" "$ahead" "$behind" "$age" "$note"
        else
          local disp_wt="$wt"
          [ -n "$wt" ] && disp_wt="${wt#$REPO_ROOT/}"
          [ -z "$disp_wt" ] && disp_wt="-"
          printf '%-10s %-26s %-30s %-9s %-10s %s\n' \
            "$status" "${branch:0:26}" "${disp_wt:0:30}" "+${ahead}/-${behind}" "$age" "$note"
        fi
      }
      
      [ "$PORCELAIN" -eq 0 ] && {
        printf '%-10s %-26s %-30s %-9s %-10s %s\n' "STATUS" "BRANCH" "WORKTREE" "A/B" "AGE" "NOTE"
        echo "────────────────────────────────────────────────────────────────────────────────────────────────────"
      }
      
      # Walk every local branch except trunk.
      while IFS= read -r branch; do
        [ -z "$branch" ] && continue
        [ "$branch" = "$TRUNK" ] && continue
      
        ahead=$(git rev-list --count "${TRUNK}..${branch}" 2>/dev/null || echo 0)
        behind=$(git rev-list --count "${branch}..${TRUNK}" 2>/dev/null || echo 0)
        age=$(git log -1 --format='%ar' "$branch" 2>/dev/null | sed 's/ ago//')
        [ -z "$age" ] && age="?"
        last_ct=$(git log -1 --format='%ct' "$branch" 2>/dev/null || echo 0)
        recent=false; [ "$last_ct" -ge "$RECENT_THRESHOLD" ] 2>/dev/null && recent=true
      
        wt=$(worktree_for "$branch")
      
        merged=false
        if git merge-base --is-ancestor "$branch" "$TRUNK" 2>/dev/null; then
          merged=true
        fi
      
        dirty=false; worktree_dirty "$wt" && dirty=true
        active=false; worktree_active "$wt" && active=true
      
        # Note annotations.
        note=""
        [ -n "$wt" ] && [ "$wt" = "$REPO_ROOT" ] && note="main checkout"
        if [ -n "$wt" ]; then
          untracked=$(git -C "$wt" ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
          [ "$untracked" -gt 0 ] && note="${note:+$note; }${untracked} untracked"
        fi
      
        # A branch far behind trunk will need a big rebase and likely conflicts —
        # surface it as a note whichever bucket it lands in.
        behind_note=""
        [ "$behind" -gt 40 ] && behind_note="far behind (${behind}) — expect conflicts"
      
        # Classify (order matters: merged before active/dirty; recency splits
        # clean-and-ahead into current LANDABLE vs abandoned STALE). A branch with
        # nothing ahead of trunk is necessarily an ancestor → caught by MERGED.
        if [ "$merged" = true ] || [ "$ahead" -eq 0 ]; then
          status="MERGED";  note="${note:+$note; }in trunk — prune candidate"; N_MERGED=$((N_MERGED+1))
        elif [ "$active" = true ]; then
          status="ACTIVE";  note="${note:+$note; }live writer <${ACTIVE_WINDOW}s — park"; N_ACTIVE=$((N_ACTIVE+1))
        elif [ "$dirty" = true ]; then
          status="WIP";     note="${note:+$note; }uncommitted — commit before landing"; N_WIP=$((N_WIP+1))
        elif [ "$recent" = true ]; then
          status="LANDABLE"; note="${note:+$note; }${behind_note}"; N_LANDABLE=$((N_LANDABLE+1))
        else
          status="STALE";   note="${note:+$note; }last commit >${RECENT_DAYS}d — abandoned?${behind_note:+; $behind_note}"; N_STALE=$((N_STALE+1))
        fi
      
        emit "$status" "$branch" "$wt" "$ahead" "$behind" "$age" "$note"
      done < <(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null)
      
      rm -f "$TMP_WT"
      
      if [ "$PORCELAIN" -eq 0 ]; then
        echo ""
        echo "Trunk: $TRUNK   Main checkout on: ${MAIN_BRANCH:-<detached>}   (recent = ≤${RECENT_DAYS}d)"
        echo "Summary: $N_LANDABLE landable · $N_STALE stale · $N_WIP WIP · $N_ACTIVE active · $N_MERGED merged"
        echo ""
        if [ "$N_LANDABLE" -gt 0 ]; then
          echo "  → $N_LANDABLE branch(es) ready to land. Hand them to fleet-ops:"
          echo "      fleet track <landable-branches...>   &&   fleet land --all --running"
        else
          echo "  → nothing current to land right now."
        fi
        [ "$N_STALE"  -gt 0 ] && echo "  · $N_STALE stale (clean but >${RECENT_DAYS}d old) — prune/archive, or --recent-days to include."
        [ "$N_ACTIVE" -gt 0 ] && echo "  ⚠ $N_ACTIVE active (a session is writing) — parked, never auto-landed."
        [ "$N_WIP"    -gt 0 ] && echo "  ⚠ $N_WIP with uncommitted work — commit in-lane first, then re-run."
        if [ "$MAIN_BRANCH" != "$TRUNK" ] && [ -n "$MAIN_BRANCH" ]; then
          echo "  ⚠ main checkout is on '$MAIN_BRANCH', not '$TRUNK' — landing checks out $TRUNK there."
        fi
      fi
      
      # Exit 1 signals "a batch land is available" (LANDABLE > 0); 0 = nothing to do.
      [ "$N_LANDABLE" -gt 0 ] && exit 1
      exit 0
      
    • new-lane.sh 4.8 KB
      #!/bin/bash
      # new-lane.sh — create an isolated git worktree + branch ("lane") for parallel work, carrying over
      # the gitignored env files a fresh worktree won't have. The one-command remedy when the peer-writer
      # guard fires, or any time you want to parallelise without two sessions sharing one checkout.
      # Part of the git-ops skill (Worktree Operations → "Lane provisioning").
      #
      # Usage:   new-lane.sh [--sibling] <slug> [base-branch]
      #   <slug>        short kebab name -> branch lane/<slug>
      #   [base-branch] what to branch from (default: the current branch)
      #   --sibling     place the worktree OUTSIDE the repo at <repo>/../<repo>-<slug>
      #                 (structural isolation; use for a repo that can't gitignore
      #                  .claude/worktrees/, or just before destructive cleanups)
      #
      # Default is IN-REPO: <main>/.claude/worktrees/<slug> — the native Claude Code worktree
      # location: tidy (no sibling dirs littering the parent), and gitignored so `git add -A`
      # can't stage its gitlinks (the precondition is verified/ensured below). Committed lane work
      # lives in the shared object store and survives even directory deletion; only UNCOMMITTED work
      # is at risk from `git clean -ff` / `rm -rf`, so land early/often. See rules/worktree-boundaries.md.
      #
      # Output contract (SKILL-RESOURCE-PROTOCOL): the new worktree's absolute path is the ONLY thing on
      # stdout (so a caller can `cd "$(new-lane.sh foo)"`); all human-facing messages go to stderr.
      # Exit: 0 ok | 2 usage/precondition | 1 conflict (branch/path exists).
      #
      # Examples:
      #   new-lane.sh auth-refactor            # lane/auth-refactor in .claude/worktrees/auth-refactor
      #   new-lane.sh hotfix main              # lane/hotfix off main, in-repo
      #   new-lane.sh --sibling big-migration  # lane/big-migration in ../<repo>-big-migration
      set -euo pipefail
      
      show_help() { sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//' >&2; }
      
      SIBLING=0
      ARGS=()
      for a in "$@"; do
        case "$a" in
          --sibling) SIBLING=1 ;;
          -h|--help) show_help; exit 0 ;;
          --) : ;;
          -*) echo "new-lane: unknown option '$a' (valid: --sibling, -h/--help)" >&2; exit 2 ;;
          *) ARGS+=("$a") ;;
        esac
      done
      set -- ${ARGS[@]+"${ARGS[@]}"}
      
      [ -n "${1:-}" ] || { show_help; exit 2; }
      
      SLUG=$(printf '%s' "$1" | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-')
      [ -n "$SLUG" ] || { echo "new-lane: slug empty after sanitising" >&2; exit 2; }
      
      ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "new-lane: not inside a git repo" >&2; exit 2; }
      
      # Anchor at the MAIN worktree root so a lane never nests inside a linked worktree
      # (running from .claude/worktrees/<x> would otherwise create worktrees-in-worktrees).
      COMMON=$(git -C "$ROOT" rev-parse --git-common-dir 2>/dev/null || echo "$ROOT/.git")
      case "$COMMON" in
        /*|[A-Za-z]:[\\/]*) ;;             # already absolute
        *) COMMON="$ROOT/$COMMON" ;;       # relative → resolve under ROOT
      esac
      MAIN=$(cd "$(dirname "$COMMON")" 2>/dev/null && pwd) || MAIN="$ROOT"
      
      REPO=$(basename "$MAIN")
      BRANCH="lane/$SLUG"
      BASE="${2:-$(git -C "$MAIN" rev-parse --abbrev-ref HEAD 2>/dev/null)}"
      
      # Resolve the lane path (no side effects yet).
      if [ "$SIBLING" -eq 1 ]; then
        WT="$(dirname "$MAIN")/${REPO}-${SLUG}"
      else
        WT="$MAIN/.claude/worktrees/$SLUG"
      fi
      
      # Validate EVERYTHING before mutating anything — a bad base or a conflict must
      # never leave partial state (e.g. a stray .gitignore append, see below).
      git -C "$MAIN" rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 \
        || { echo "new-lane: base '$BASE' is not a valid branch/commit" >&2; exit 2; }
      git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH" && { echo "new-lane: branch $BRANCH already exists" >&2; exit 1; }
      [ -e "$WT" ] && { echo "new-lane: path $WT already exists" >&2; exit 1; }
      
      # In-repo placement is only safe when the lane dir is gitignored — otherwise
      # `git add -A` from the main checkout stages worktree gitlinks (the 2026-04-19
      # incident). Ensure the ignore now that all checks have passed, so the in-repo
      # default is safe in ANY repo — not just one someone already tidied.
      if [ "$SIBLING" -ne 1 ] && ! git -C "$MAIN" check-ignore -q ".claude/worktrees/.probe" 2>/dev/null; then
        printf '\n# git worktrees / lanes — never tracked (new-lane.sh)\n.claude/worktrees/\n' >> "$MAIN/.gitignore"
        echo "new-lane: '.claude/worktrees/' was not gitignored — added it to .gitignore (commit this)" >&2
      fi
      
      git -C "$MAIN" worktree add "$WT" -b "$BRANCH" "$BASE" >&2
      
      # Carry over gitignored env/secret files the new worktree won't have (Cloudflare/Node/etc.).
      for f in .dev.vars .env .env.local .env.development .secrets; do
        if [ -e "$MAIN/$f" ] && [ ! -e "$WT/$f" ]; then
          cp -r "$MAIN/$f" "$WT/$f" 2>/dev/null && echo "new-lane: carried over $f" >&2
        fi
      done
      
      echo "new-lane: lane ready — branch $BRANCH (base $BASE)" >&2
      echo "new-lane: open a Claude session with cwd '$WT' — one writer per tree; land via fleet-ops" >&2
      printf '%s\n' "$WT"
      
    • status.sh 6.2 KB
      #!/bin/bash
      # git-status - one-shot read-only repo overview
      #
      # Usage:
      #   bash status.sh             # survey current directory
      #   bash status.sh <repo-path> # survey explicit path
      #
      # Exit codes:
      #   0  CLEAN (nothing ahead/behind, tree empty, no stashes)
      #   1  NON-CLEAN (at least one signal non-zero)
      #   2  Not a git repo
      
      set -u
      
      REPO="${1:-$PWD}"
      
      # Guard: must be inside a git repo
      if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "not-a-repo: $REPO"
        exit 2
      fi
      
      REPO_ROOT=$(git -C "$REPO" rev-parse --show-toplevel)
      cd "$REPO_ROOT" || { echo "cannot-cd: $REPO_ROOT"; exit 2; }
      
      # Best-effort fetch — record failure but don't abort
      FETCH_OK=true
      git fetch --quiet 2>/dev/null || FETCH_OK=false
      
      # Age of last successful fetch (mtime of FETCH_HEAD)
      fetch_age=-1
      if [ -f .git/FETCH_HEAD ]; then
        if fetch_mtime=$(stat -c '%Y' .git/FETCH_HEAD 2>/dev/null); then
          :
        elif fetch_mtime=$(stat -f '%m' .git/FETCH_HEAD 2>/dev/null); then
          :
        else
          fetch_mtime=""
        fi
        if [ -n "$fetch_mtime" ]; then
          fetch_age=$(( $(date +%s) - fetch_mtime ))
        fi
      fi
      
      # Branch / HEAD
      BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "(detached)")
      HEAD_INFO=$(git log -1 --format='%h %s (%ar)' 2>/dev/null || echo "(no commits)")
      
      # Sync with upstream (if configured)
      AHEAD=0
      BEHIND=0
      if [ "$BRANCH" != "(detached)" ] && git rev-parse '@{u}' >/dev/null 2>&1; then
        AHEAD=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
        BEHIND=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
        SYNC_LINE="$AHEAD ahead / $BEHIND behind"
      else
        SYNC_LINE="no upstream"
      fi
      
      # Working tree
      STAGED=$(git diff --cached --name-only | wc -l | tr -d ' ')
      UNSTAGED=$(git diff --name-only | wc -l | tr -d ' ')
      UNTRACKED=$(git ls-files --others --exclude-standard | wc -l | tr -d ' ')
      STASHES=$(git stash list | wc -l | tr -d ' ')
      
      # Shortstat if there's uncommitted change
      SHORTSTAT=""
      if [ "$STAGED" -gt 0 ] || [ "$UNSTAGED" -gt 0 ]; then
        SHORTSTAT=$(git diff HEAD --shortstat 2>/dev/null \
          | sed 's/^ *//' \
          | sed -E 's/([0-9]+) files? changed, //' \
          | sed -E 's/([0-9]+) insertions?\(\+\)/+\1/' \
          | sed -E 's/([0-9]+) deletions?\(-\)/-\1/' \
          | tr -d '()')
      fi
      
      # Worktrees — registered vs filesystem
      WT_REGISTERED=$(git worktree list 2>/dev/null | wc -l | tr -d ' ')
      WT_FS=0
      if [ -d .claude/worktrees ]; then
        WT_FS=$(find .claude/worktrees -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
      fi
      
      # Branches
      BR_LOCAL=$(git branch 2>/dev/null | wc -l | tr -d ' ')
      BR_REMOTE=$(git branch -r 2>/dev/null | wc -l | tr -d ' ')
      
      # Optional PR linkage (graceful if gh absent or no PR)
      PR_LINE=""
      if command -v gh >/dev/null 2>&1 && [ "$BRANCH" != "(detached)" ]; then
        PR_JSON=$(gh pr view --json number,url,state 2>/dev/null || true)
        if [ -n "$PR_JSON" ] && command -v jq >/dev/null 2>&1; then
          PR_LINE=$(printf '%s' "$PR_JSON" \
            | jq -r 'if .number then "PR #\(.number): \(.url) [\(.state)]" else empty end' 2>/dev/null)
        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$" \
          | wc -l | tr -d ' ')
        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"
      echo "HEAD:    $HEAD_INFO"
      echo "sync:    $SYNC_LINE"
      
      TREE_LINE="$STAGED staged / $UNSTAGED unstaged / $UNTRACKED untracked / $STASHES stashes"
      if [ -n "$SHORTSTAT" ]; then
        TREE_LINE="$TREE_LINE ($SHORTSTAT)"
      fi
      echo "tree:    $TREE_LINE"
      
      # Only show worktrees line if there are multiple registered OR .claude/worktrees exists
      if [ "$WT_REGISTERED" -gt 1 ] || [ "$WT_FS" -gt 0 ]; then
        echo "trees:   $WT_REGISTERED registered / $WT_FS in .claude/worktrees"
      fi
      
      echo "branch:  $BR_LOCAL local / $BR_REMOTE remote"
      
      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
          if   [ "$fetch_age" -gt 86400 ]; then age_display="$((fetch_age / 86400))d ago"
          elif [ "$fetch_age" -gt 3600  ]; then age_display="$((fetch_age / 3600))h ago"
          elif [ "$fetch_age" -gt 60    ]; then age_display="$((fetch_age / 60))m ago"
          else age_display="${fetch_age}s ago"
          fi
        else
          age_display="unknown"
        fi
        echo "fetch:   FAILED (last successful: $age_display)"
      fi
      
      # --- Verdict ----------------------------------------------------------------
      echo ""
      if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ] && \
         [ "$STAGED" -eq 0 ] && [ "$UNSTAGED" -eq 0 ] && \
         [ "$UNTRACKED" -eq 0 ] && [ "$STASHES" -eq 0 ]; then
        echo "verdict: CLEAN"
        exit 0
      fi
      
      FLAGS=""
      [ "$AHEAD"     -gt 0 ] && FLAGS="$FLAGS ahead"
      [ "$BEHIND"    -gt 0 ] && FLAGS="$FLAGS behind"
      [ "$STAGED"    -gt 0 ] && FLAGS="$FLAGS staged"
      [ "$UNSTAGED"  -gt 0 ] && FLAGS="$FLAGS unstaged"
      [ "$UNTRACKED" -gt 0 ] && FLAGS="$FLAGS untracked"
      [ "$STASHES"   -gt 0 ] && FLAGS="$FLAGS stashes"
      echo "verdict: NON-CLEAN —${FLAGS}"
      exit 1
      
    • worktree-survey.sh 6.1 KB
      #!/bin/bash
      # worktree-survey.sh - Read-only worktree survey + triage
      #
      # Enumerates registered worktrees, cross-references with .claude/worktrees/
      # filesystem entries, classifies each, and emits a table + summary.
      #
      # NEVER mutates. Respects rules/worktree-boundaries.md.
      #
      # Usage:
      #   bash worktree-survey.sh              # survey current repo
      #   bash worktree-survey.sh <repo-path>  # survey explicit repo
      #
      # Exit codes:
      #   0  All worktrees healthy (no ghosts, orphans, or prunable)
      #   1  Attention needed (ghosts, orphans, or prunable candidates found)
      #   2  Not a git repo
      
      set -u
      
      REPO="${1:-$PWD}"
      
      if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "not-a-repo: $REPO"
        exit 2
      fi
      
      REPO_ROOT=$(git -C "$REPO" rev-parse --show-toplevel)
      cd "$REPO_ROOT" || exit 2
      
      # Detect trunk branch
      TRUNK="main"
      if ! git rev-parse --verify main >/dev/null 2>&1; then
        if git rev-parse --verify master >/dev/null 2>&1; then
          TRUNK="master"
        fi
      fi
      
      # Parse `git worktree list --porcelain` into TSV: path \t branch \t head
      TMP_REG=$(mktemp)
      git worktree list --porcelain 2>/dev/null | awk '
        /^worktree / { p=substr($0,10); b=""; h=""; next }
        /^HEAD /     { h=substr($0,6); next }
        /^branch /   { b=substr($0, 19); next }  # skip "branch refs/heads/" (18 chars)
        /^detached/  { b="(detached)"; next }
        /^$/         { if (p != "") { print p"\t"b"\t"h } p=""; b=""; h=""; next }
        END          { if (p != "") print p"\t"b"\t"h }
      ' > "$TMP_REG"
      
      WT_COUNT=$(wc -l < "$TMP_REG" | tr -d ' ')
      
      # Enumerate filesystem entries in .claude/worktrees/ (canonical absolute paths)
      TMP_FS=$(mktemp)
      if [ -d .claude/worktrees ]; then
        while IFS= read -r d; do
          [ -z "$d" ] && continue
          (cd "$d" 2>/dev/null && pwd -P)
        done < <(find .claude/worktrees -maxdepth 1 -mindepth 1 -type d 2>/dev/null) > "$TMP_FS"
      fi
      
      FS_COUNT=$(wc -l < "$TMP_FS" | tr -d ' ')
      
      # Counters
      GHOSTS=0
      PRUNABLE=0
      WIP=0
      UNPUSHED=0
      ORPHANS=0
      
      # Header
      printf "%-40s %-20s %-22s %-12s %s\n" "PATH" "BRANCH" "STATE" "AGE" "VERDICT"
      echo "──────────────────────────────────────────────────────────────────────────────────────────────"
      
      # --- Process each registered worktree ---
      while IFS=$'\t' read -r path branch head; do
        [ -z "$path" ] && continue
      
        # Canonical absolute (for orphan comparison)
        canon_path=$( (cd "$path" 2>/dev/null && pwd -P) || echo "$path" )
      
        # Display path
        if [ "$path" = "$REPO_ROOT" ]; then
          disp="<trunk>"
        else
          disp="${path#$REPO_ROOT/}"
          [ ${#disp} -gt 38 ] && disp="...${disp: -35}"
        fi
      
        # Ghost: registered but filesystem gone
        if [ ! -d "$path" ]; then
          printf "%-40s %-20s %-22s %-12s %s\n" "<$disp>" "$branch" "FILESYSTEM GONE" "?" "git worktree prune"
          GHOSTS=$((GHOSTS+1))
          continue
        fi
      
        # Tree state
        staged=$(git -C "$path" diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
        unstaged=$(git -C "$path" diff --name-only 2>/dev/null | wc -l | tr -d ' ')
        untracked=$(git -C "$path" ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
      
        # Upstream sync
        ahead=0
        behind=0
        if [ "$branch" != "(detached)" ] && git -C "$path" rev-parse '@{u}' >/dev/null 2>&1; then
          ahead=$(git -C "$path" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
          behind=$(git -C "$path" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
        fi
      
        # Age
        age="?"
        if [ -n "$head" ]; then
          age=$(git log -1 --format='%ar' "$head" 2>/dev/null | sed 's/ ago//')
        fi
      
        # Merged into trunk?
        merged=false
        if [ -n "$head" ] && [ "$branch" != "$TRUNK" ] && \
           git rev-parse --verify "$TRUNK" >/dev/null 2>&1 && \
           git merge-base --is-ancestor "$head" "$TRUNK" 2>/dev/null; then
          merged=true
        fi
      
        # Build state string
        state=""
        dirty=false
        [ "$staged"    -gt 0 ] && state="${state} ${staged}s"    && dirty=true
        [ "$unstaged"  -gt 0 ] && state="${state} ${unstaged}u"  && dirty=true
        [ "$untracked" -gt 0 ] && state="${state} ${untracked}?" && dirty=true
        [ "$ahead"     -gt 0 ] && state="${state} +${ahead}"
        [ "$behind"    -gt 0 ] && state="${state} -${behind}"
        state="${state# }"
        [ -z "$state" ] && state="clean"
        [ "$merged" = true ] && state="$state (merged)"
      
        # Verdict
        if [ "$branch" = "$TRUNK" ]; then
          verdict="(trunk)"
        elif [ "$dirty" = true ]; then
          verdict="has WIP"
          WIP=$((WIP+1))
        elif [ "$ahead" -gt 0 ]; then
          verdict="unpushed"
          UNPUSHED=$((UNPUSHED+1))
        elif [ "$merged" = true ]; then
          verdict="PRUNABLE"
          PRUNABLE=$((PRUNABLE+1))
        else
          verdict="in-flight"
        fi
      
        printf "%-40s %-20s %-22s %-12s %s\n" "$disp" "$branch" "$state" "$age" "$verdict"
      done < "$TMP_REG"
      
      # --- Orphans: filesystem entries in .claude/worktrees/ with no registration ---
      while IFS= read -r fs_path; do
        [ -z "$fs_path" ] && continue
        registered=false
        while IFS=$'\t' read -r reg_path _ _; do
          reg_canon=$( (cd "$reg_path" 2>/dev/null && pwd -P) || echo "$reg_path" )
          if [ "$reg_canon" = "$fs_path" ]; then
            registered=true
            break
          fi
        done < "$TMP_REG"
        if [ "$registered" = false ]; then
          disp="${fs_path#$REPO_ROOT/}"
          printf "%-40s %-20s %-22s %-12s %s\n" "$disp" "?" "UNREGISTERED" "?" "manual review (DO NOT touch)"
          ORPHANS=$((ORPHANS+1))
        fi
      done < "$TMP_FS"
      
      rm -f "$TMP_REG" "$TMP_FS"
      
      # --- Summary ---
      echo ""
      echo "Summary: $WT_COUNT registered / $FS_COUNT in .claude/worktrees / $ORPHANS orphan"
      echo "  PRUNABLE (merged, clean, linked):   $PRUNABLE"
      echo "  WIP (uncommitted changes):          $WIP"
      echo "  Unpushed (ahead of upstream):       $UNPUSHED"
      echo "  Ghost (registered, FS missing):     $GHOSTS"
      echo "  Orphan (FS exists, unregistered):   $ORPHANS    ← read-only, never rm without review"
      
      # Legend note (shown only if abbreviations appear in output)
      if [ "$WIP" -gt 0 ] || [ "$UNPUSHED" -gt 0 ]; then
        echo ""
        echo "  STATE legend: Ns=staged, Nu=unstaged, N?=untracked, +N=ahead, -N=behind"
      fi
      
      # Exit 1 if anything needs attention
      if [ "$GHOSTS" -gt 0 ] || [ "$ORPHANS" -gt 0 ] || [ "$PRUNABLE" -gt 0 ]; then
        exit 1
      fi
      exit 0
      
  • tests
    • run.sh 8.8 KB
      #!/usr/bin/env bash
      # Self-test for git-ops worktree provisioning. Offline + deterministic (git only,
      # no network). Covers new-lane.sh (in-repo default, gitignore precondition,
      # --sibling, main-anchoring, validate-before-mutate, arg hygiene) and the
      # worktree-guard.sh `git clean` double-force detection.
      # Resolves paths relative to itself so it runs in the repo and once installed.
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures (SKIP+exit 0 if git is unavailable)
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      NL="$SKILL/scripts/new-lane.sh"
      # worktree-guard.sh lives in the repo's hooks/ — resolve for both repo + installed layouts.
      GUARD=""
      for c in "$SKILL/../../hooks/worktree-guard.sh" "$HOME/.claude/hooks/worktree-guard.sh"; do
        [ -f "$c" ] && { GUARD="$c"; break; }
      done
      
      command -v git >/dev/null 2>&1 || { echo "SKIP: git not available"; exit 0; }
      
      SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
      P=0; F=0
      ok(){ P=$((P+1)); printf '  PASS  %s\n' "$1"; }
      no(){ F=$((F+1)); printf '  FAIL  %s\n' "$1"; }
      mkrepo(){ rm -rf "$1"; git init -q -b main "$1"; git -C "$1" config user.email t@t
        git -C "$1" config user.name t; git -C "$1" config core.autocrlf false
        echo base > "$1/f"; git -C "$1" add -A; git -C "$1" commit -qm init; }
      
      echo "=== git-ops new-lane / worktree-guard self-test ==="
      
      echo "-- new-lane: in-repo default + gitignore precondition --"
      mkrepo "$SB/A"; cd "$SB/A"
      OUT="$(bash "$NL" auth-refactor 2>/dev/null)"; rc=$?
      [ $rc -eq 0 ] && ok "exit 0" || no "exit $rc"
      case "$OUT" in */.claude/worktrees/auth-refactor) ok "stdout = in-repo path only";; *) no "stdout=[$OUT]";; esac
      [ -d "$SB/A/.claude/worktrees/auth-refactor" ] && ok "lane dir created in-repo" || no "lane dir missing"
      git check-ignore -q .claude/worktrees/.x && ok ".claude/worktrees/ now gitignored" || no "gitignore not ensured"
      git show-ref --verify --quiet refs/heads/lane/auth-refactor && ok "branch lane/auth-refactor" || no "branch missing"
      
      echo "-- new-lane: no duplicate gitignore on second lane --"
      before=$(grep -c worktrees .gitignore); bash "$NL" hotfix main >/dev/null 2>&1
      [ "$before" = "$(grep -c worktrees .gitignore)" ] && ok "gitignore not duplicated" || no "gitignore duplicated"
      
      echo "-- new-lane: blanket .claude/ ignore is respected (no append) --"
      mkrepo "$SB/B"; cd "$SB/B"; printf '.claude/\n' > .gitignore; git add .gitignore; git commit -qm ig
      sz=$(wc -c <.gitignore); bash "$NL" x >/dev/null 2>&1
      [ "$sz" = "$(wc -c <.gitignore)" ] && ok "blanket .claude/ -> gitignore untouched" || no "appended despite blanket ignore"
      
      echo "-- new-lane: --sibling places outside the repo --"
      mkrepo "$SB/S"; cd "$SB/S"
      OS="$(bash "$NL" --sibling big 2>/dev/null)"
      [ "$OS" = "$SB/S-big" ] && ok "sibling path = $OS" || no "sibling=[$OS]"
      [ -d "$SB/S-big" ] && ok "sibling dir outside repo" || no "sibling dir missing"
      
      echo "-- new-lane: anchors at MAIN when invoked from inside a lane (no nesting) --"
      mkrepo "$SB/N"; cd "$SB/N"; L1="$(bash "$NL" first 2>/dev/null)"
      ( cd "$L1" && bash "$NL" second >/dev/null 2>&1 )
      [ -d "$SB/N/.claude/worktrees/second" ] && ok "lane anchored to MAIN" || no "did not anchor to MAIN"
      [ ! -d "$L1/.claude/worktrees/second" ] && ok "no nested worktree inside the lane" || no "nested worktree leaked"
      
      echo "-- new-lane: validate-before-mutate + arg hygiene --"
      mkrepo "$SB/V"; cd "$SB/V"
      bash "$NL" lane1 no-such-branch >/dev/null 2>/dev/null; ee=$?
      [ $ee -eq 2 ] && ok "invalid base -> exit 2" || no "invalid base exit $ee"
      [ ! -f .gitignore ] && ok "invalid base left no .gitignore" || no "partial gitignore on bad base"
      bash "$NL" --frobnicate slug >/dev/null 2>/dev/null; ee=$?
      [ $ee -eq 2 ] && ok "unknown flag -> exit 2" || no "unknown flag exit $ee"
      [ -z "$(git branch --list 'lane/*')" ] && ok "unknown flag created no branch" || no "stray branch from bad flag"
      mkrepo "$SB/V2"; cd "$SB/V2"; git branch lane/dup
      bash "$NL" dup >/dev/null 2>/dev/null; ee=$?
      [ $ee -eq 1 ] && ok "existing branch -> exit 1" || no "dup exit $ee"
      [ ! -f .gitignore ] && ok "conflict left no .gitignore" || no "partial gitignore on conflict"
      bash "$NL" >/dev/null 2>/dev/null; [ $? -eq 2 ] && ok "no slug -> exit 2" || no "empty not exit 2"
      
      echo "-- new-lane: slug sanitised; syntax clean --"
      mkrepo "$SB/Z"; cd "$SB/Z"
      bash "$NL" "Feat Foo/Bar" >/dev/null 2>&1
      git show-ref --verify --quiet refs/heads/lane/feat-foobar && ok "slug 'Feat Foo/Bar' -> lane/feat-foobar" || no "slug sanitise wrong: $(git branch --list 'lane/*')"
      bash -n "$NL" && ok "new-lane.sh syntax OK" || no "new-lane.sh syntax error"
      
      if [ -n "$GUARD" ]; then
        echo "-- worktree-guard: git clean force-level detection --"
        bash -n "$GUARD" && ok "worktree-guard.sh syntax OK" || no "guard syntax error"
        WG="$SB/WG"; mkrepo "$WG"; mkdir -p "$WG/.claude/worktrees"
        gclean(){ local o; o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$WG" | bash "$GUARD" 2>&1)
          if [ "$2" = warn ]; then case "$o" in *double-force*) ok "guard WARN: $1";; *) no "guard expected WARN: $1 [${o:-silent}]";; esac
          else [ -z "$o" ] && ok "guard silent: $1" || no "guard expected silent: $1"; fi; }
        gclean "git clean -fdx"            silent
        gclean "git clean -ffdx"          warn
        gclean "git clean --force --force" warn
        gclean "git clean -f -f"          warn
        gclean "git clean -fd -fx"        warn
        gclean "git clean --force -f"     warn
        gclean "git clean -n"             silent
        gclean "git clean -fd ./somepath" silent
        # exemption: a session whose cwd is inside a worktree is not warned
        ex=$(printf '{"tool_input":{"command":"git clean -ffdx"},"cwd":"%s/.claude/worktrees/foo"}' "$WG" | bash "$GUARD" 2>&1)
        [ -z "$ex" ] && ok "guard exempt inside own worktree" || no "guard warned inside own worktree"
        # existing rules unregressed
        for c in "git add -A" "rm -rf .claude/worktrees/foo" "git worktree remove .claude/worktrees/foo"; do
          o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$c" "$WG" | bash "$GUARD" 2>&1)
          [ -n "$o" ] && ok "guard still warns: $c" || no "guard regressed: $c"
        done
      else
        echo "  SKIP  worktree-guard.sh not found (hooks/ unavailable in this layout)"
      fi
      
      echo "-- land-all: classification of mixed branches --"
      LA="$SKILL/scripts/land-all.sh"
      bash -n "$LA" && ok "land-all.sh syntax OK" || no "land-all.sh syntax error"
      R="$SB/LA"; mkrepo "$R"; cd "$R"
      # LANDABLE: ahead, clean, no worktree, recent commit
      git checkout -q -b feat/land-me; echo x > x; git add -A; git commit -qm land; git checkout -q main
      # MERGED: merged into trunk
      git checkout -q -b feat/merged; echo m > m; git add -A; git commit -qm m; git checkout -q main
      git merge -q --no-ff -m "merge: feat/merged" feat/merged
      # NOTHING-AHEAD: points at trunk → ancestor → folds into MERGED
      git branch feat/at-trunk main
      # STALE: ahead but last commit older than --recent-days (fixed past ISO date;
      # relative dates like "30 days ago" aren't accepted by GIT_COMMITTER_DATE on all gits)
      git checkout -q -b feat/stale; echo s > s; git add -A
      GIT_AUTHOR_DATE="2020-01-01T00:00:00" GIT_COMMITTER_DATE="2020-01-01T00:00:00" git commit -qm stale
      git checkout -q main
      # WIP: worktree, ahead, dirty tracked file
      git worktree add -q "$SB/wt-wip" -b feat/wip main >/dev/null 2>&1
      echo more > "$SB/wt-wip/x2"; git -C "$SB/wt-wip" add -A; git -C "$SB/wt-wip" commit -qm ahead
      echo dirty >> "$SB/wt-wip/f"    # f is tracked (from mkrepo) → unstaged change
      
      # --active-window 0 disables live-writer detection so fresh worktrees don't all read ACTIVE.
      OUT="$(bash "$LA" "$R" --porcelain --active-window 0 --recent-days 7)"
      sof(){ printf '%s\n' "$OUT" | awk -F'\t' -v b="$1" '$2==b{print $1}'; }
      [ "$(sof feat/land-me)" = LANDABLE ] && ok "feat/land-me → LANDABLE"     || no "land-me got [$(sof feat/land-me)]"
      [ "$(sof feat/merged)"  = MERGED   ] && ok "feat/merged → MERGED"        || no "merged got [$(sof feat/merged)]"
      [ "$(sof feat/at-trunk)" = MERGED  ] && ok "nothing-ahead → MERGED"      || no "at-trunk got [$(sof feat/at-trunk)]"
      [ "$(sof feat/stale)"   = STALE    ] && ok "feat/stale → STALE"          || no "stale got [$(sof feat/stale)]"
      [ "$(sof feat/wip)"     = WIP      ] && ok "feat/wip → WIP"              || no "wip got [$(sof feat/wip)]"
      badnf=$(printf '%s\n' "$OUT" | awk -F'\t' 'NF!=7{c++} END{print c+0}')
      [ "$badnf" = 0 ] && ok "porcelain 7 fields/line" || no "porcelain NF!=7 on $badnf line(s)"
      
      echo "-- land-all: exit 1 when LANDABLE exists; live-writer → ACTIVE --"
      bash "$LA" "$R" --active-window 0 >/dev/null 2>&1; [ $? -eq 1 ] && ok "exit 1 (landable available)" || no "expected exit 1"
      # A wide active window makes the freshly-written feat/wip worktree read as a live writer.
      OUT2="$(bash "$LA" "$R" --porcelain --active-window 3600 --recent-days 7)"
      a2=$(printf '%s\n' "$OUT2" | awk -F'\t' '$2=="feat/wip"{print $1}')
      [ "$a2" = ACTIVE ] && ok "fresh worktree → ACTIVE (live-writer guard)" || no "expected ACTIVE got [$a2]"
      
      echo "=== $P passed, $F failed ==="
      [ $F -eq 0 ]
      
  • SKILL.md 23.2 KB
    ---
    name: git-ops
    description: "Git + worktree orchestrator: status survey, per-worktree triage, commits, PRs, branches, releases, rebases — reads inline, writes to a background agent. Triggers on: git status, anything to commit, anything to push, commit, push, create PR, rebase, release, tag, changelog, worktree, land worktrees."
    license: MIT
    allowed-tools: "Read Bash Glob Grep Agent TaskCreate TaskUpdate"
    metadata:
      author: claude-mods
      related-skills: review, ci-cd-ops, push-gate
    ---
    
    # Git Ops
    
    Intelligent git operations orchestrator. Routes read-only queries inline for speed, dispatches write operations to a background Sonnet agent (`git-agent`) to free the main session.
    
    ## Architecture
    
    ```
    User intent (commit, PR, rebase, status, etc.)
        |
        +---> Tier 1: Read-only (status, log, diff, blame)
        |       |
        |       +---> Execute INLINE via Bash (fast, no subagent)
        |
        +---> Tier 2: Safe writes (commit, push, tag, PR, stash)
        |       |
        |       +---> Gather context from conversation
        |       +---> Dispatch to git-agent (background, Sonnet)
        |       |       +---> Fallback: general-purpose with inlined protocol
        |       +---> Agent executes and reports back
        |
        +---> Tier 3: Destructive (rebase, reset, force-push, branch -D)
                |
                +---> Dispatch to git-agent (background, Sonnet)
                |       +---> Fallback: general-purpose with inlined protocol
                +---> Agent produces PREFLIGHT REPORT (does NOT execute)
                +---> Orchestrator relays preflight to user
                +---> On confirmation: re-dispatch with execute authority
    ```
    
    ## Safety Tiers
    
    ### Tier 1: Read-Only - Run Inline
    
    No subagent needed. Execute directly via Bash for instant results.
    
    | Operation | Command |
    |-----------|---------|
    | **Status (rich)** | `bash $HOME/.claude/skills/git-ops/scripts/status.sh` — one-shot HEAD + sync + tree + worktrees + branches + PR |
    | **Worktree survey** | `bash $HOME/.claude/skills/git-ops/scripts/worktree-survey.sh` — per-worktree state, drift detection, prunable/WIP/ghost/orphan triage |
    | **Land-all plan** | `bash $HOME/.claude/skills/git-ops/scripts/land-all.sh [--porcelain]` — classifies every branch as LANDABLE/STALE/WIP/ACTIVE/MERGED for a batch land (see "Land all" below) |
    | Status (bare) | `git status --short` |
    | Log | `git log --oneline -20` |
    | Diff (unstaged) | `git diff --stat` |
    | Diff (staged) | `git diff --cached --stat` |
    | Diff (full) | `git diff [file]` or `git diff --cached [file]` |
    | Branch list | `git branch -v` |
    | Remote branches | `git branch -rv` |
    | Stash list | `git stash list` |
    | Blame | `git blame [file]` |
    | Show commit | `git show [hash] --stat` |
    | Reflog | `git reflog --oneline -20` |
    | Tags | `git tag --list --sort=-v:refname` |
    | Worktree list | `git worktree list` |
    | PR list | `gh pr list` |
    | PR status | `gh pr view [N]` |
    | Issue list | `gh issue list` |
    | CI checks | `gh pr checks [N]` |
    | Run status | `gh run list --limit 5` |
    
    For T1 operations, format results cleanly and present directly. Use `delta` for diffs when available.
    
    **When to reach for the bundled scripts:**
    - User asks "status", "where are we", "anything to commit", "anything to push" → `status.sh`
    - 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).
    
    **Context gathering before dispatch:**
    
    | Operation | Context to Gather |
    |-----------|-------------------|
    | **Commit** | What the user has been working on (from conversation), staged files, recent commit style |
    | **Push** | Current branch, tracking info, commits ahead of remote |
    | **PR create** | All commits on branch vs main, conversation context for description |
    | **Tag/release** | Commits since last tag, version pattern in use |
    | **Stash** | Current changes, user's stash message if provided |
    | **Cherry-pick** | Target commit details, current branch |
    | **Branch create** | Base branch, naming convention from recent branches |
    | **gh issue create** | User description, labels if mentioned |
    
    **Dispatch template:**
    
    ```
    You are the git-agent handling a Tier 2 (safe write) operation.
    
    ## Domain Knowledge
    For release or PR operations, read CI context first:
    - Read: skills/ci-cd-ops/SKILL.md (release workflows, PR conventions)
    
    ## Context
    - Current branch: {branch}
    - Repository: {repo info}
    - User intent: {what the user asked for}
    - Conversation context: {relevant summary of what was being worked on}
    
    ## Operation
    {specific operation details}
    
    ## Project Conventions
    {commit style, branch naming, PR template if detected}
    
    Execute the operation following your T2 protocol (verify state, execute, confirm, report).
    ```
    
    ### Tier 3: Destructive - Preflight Required
    
    Dispatch to `git-agent` with explicit instruction to produce a preflight report ONLY.
    
    **Dispatch template (preflight):**
    
    ```
    You are the git-agent handling a Tier 3 (destructive) operation.
    
    ## Context
    - Current branch: {branch}
    - Repository: {repo info}
    - User intent: {what the user asked for}
    
    ## Operation
    {specific operation details}
    
    IMPORTANT: Do NOT execute this operation. Produce a T3 Preflight Report only.
    Show exactly what will happen, what the risks are, and how to recover.
    ```
    
    **After user confirms:** Re-dispatch with execute authority:
    
    ```
    User has confirmed the Tier 3 operation after reviewing the preflight.
    
    ## Approved Operation
    {exact operation from preflight}
    
    ## Confirmation
    Proceed with execution. Follow T3 execution protocol:
    1. Create a safety bookmark: note the current HEAD hash
    2. Execute the operation
    3. Verify the result
    4. Report with the safety bookmark for recovery
    ```
    
    ## Dispatch Mechanics
    
    ### Background Agent (Default for T2/T3)
    
    ```python
    # Dispatch to git-agent, runs in background, Sonnet model
    Agent(
        subagent_type="git-agent",
        model="sonnet",
        run_in_background=True,  # Frees main session
        prompt="..."             # From dispatch templates above
    )
    ```
    
    The main session continues working while the agent handles git operations. Results arrive asynchronously.
    
    ### Foreground Agent (When Result Needed Immediately)
    
    For operations where the user is waiting on the result (e.g., "commit this then let's move on"):
    
    ```python
    Agent(
        subagent_type="git-agent",
        model="sonnet",
        run_in_background=False,  # Wait for result
        prompt="..."
    )
    ```
    
    ### Worktree Isolation (Only When Requested)
    
    When the user explicitly asks for worktree isolation (e.g., "do this in a separate worktree", "prepare a branch without touching my working tree"):
    
    ```python
    Agent(
        subagent_type="git-agent",
        model="sonnet",
        isolation="worktree",     # Isolated repo copy
        run_in_background=True,
        prompt="..."
    )
    ```
    
    ## Fallback: When git-agent Is Unavailable
    
    If `git-agent` is not registered as a subagent type (e.g., plugin not installed, agent files missing), fall back to `general-purpose` with the git-agent identity inlined in the prompt.
    
    **Detection:** If dispatching to `git-agent` fails or the subagent type is not listed in available agents, switch to fallback mode automatically.
    
    **Fallback dispatch template:**
    
    ```python
    Agent(
        subagent_type="general-purpose",  # Fallback
        model="sonnet",
        run_in_background=True,
        prompt="""You are acting as a git operations agent. You are precise, safety-conscious,
    and follow the three-tier safety system:
    - T1 (read-only): execute freely
    - T2 (safe writes): execute on instruction, verify before and after
    - T3 (destructive): preflight report only unless explicitly told to execute
    
    {original dispatch prompt here}
    """
    )
    ```
    
    **Key differences from primary dispatch:**
    - Uses `general-purpose` instead of `git-agent` subagent type
    - Inlines the safety tier protocol directly in the prompt (the agent won't have git-agent's system prompt)
    - Everything else stays the same - context gathering, templates, foreground/background choice
    
    **When to use each:**
    
    | Condition | Dispatch Method |
    |-----------|----------------|
    | `git-agent` available | Primary: `subagent_type="git-agent"` |
    | `git-agent` unavailable | Fallback: `subagent_type="general-purpose"` with inlined protocol |
    | No agent dispatch possible | Last resort: execute T2 operations inline (main context) |
    
    The last-resort inline path should only be used for simple T2 operations (single commit, simple push). Complex workflows (PR creation, release, changelog) should always use an agent.
    
    ## Extended Operations
    
    ### Release Workflow
    
    `git-ops` owns the **local** half of releases only — analysing commits, generating CHANGELOG content, creating the local tag. The **remote** half (push, `gh release create`, repo metadata) belongs to the `github-ops` skill.
    
    When user asks to "create a release", "bump version", or "tag a release":
    
    1. **Inline (T1):** Check current version and commits since last tag
       ```bash
       git describe --tags --abbrev=0 2>/dev/null
       git log --oneline $(git describe --tags --abbrev=0 2>/dev/null)..HEAD
       ```
    
    2. **Determine version bump:**
       - `feat:` commits -> minor bump
       - `fix:` commits -> patch bump
       - `BREAKING CHANGE:` or `!:` -> requires explicit user approval (never auto-major)
       - Or use version specified by user
    
    3. **Dispatch to git-agent (T2):** Generate CHANGELOG content + create local tag.
    
    4. **Hand off to `github-ops`** for the remote half: push commits, push tag, create the GitHub release with notes, update repo metadata if warranted. Do not call `gh release create` from git-ops — that crosses the local/remote boundary. See `skills/github-ops/SKILL.md` mode `update`.
    
    ### Changelog Generation
    
    When user asks to "generate changelog" or "update CHANGELOG.md":
    
    1. **Inline (T1):** Gather commit history for the range
    2. **Dispatch to git-agent (T2):** Categorise commits, format as Keep a Changelog, write file
    
    ### PR Workflow (Full Cycle)
    
    When user says "create a PR" or "open a PR":
    
    1. **Inline (T1):** Check branch state, diff against main
    2. **Gather context:** What was the user working on? What does the conversation tell us about the goal?
    3. **Dispatch to git-agent (T2):** Create PR with contextual title and body
    4. **Report:** PR number, URL, summary
    
    ### Branch Cleanup
    
    When user asks to "clean up branches" or "delete merged branches":
    
    1. **Inline (T1):** List merged branches
       ```bash
       git branch --merged main | grep -v "main\|master\|\*"
       ```
    2. **Show list to user** - this is a T3 preflight (deletion)
    3. **On confirmation:** Dispatch to git-agent to delete them
    
    ### Semantic Versioning Analysis
    
    When user asks "what should the next version be":
    
    1. **Inline (T1):** Analyse commits since last tag
    2. Categorise by Conventional Commits
    3. Report recommended bump with reasoning
    
    ### Conflict Resolution Support
    
    When user encounters merge conflicts:
    
    1. **Inline (T1):** `git status` to show conflicted files
    2. **Inline (T1):** Read conflict markers in each file
    3. **Present options:** ours, theirs, manual resolution
    4. **After resolution:** Dispatch to git-agent (T2) for staging and continue
    
    ## Worktree Operations
    
    Worktrees are first-class in this skill. The classification is:
    
    | Op | Tier | How |
    |----|------|-----|
    | **Survey** | T1 | `bash scripts/worktree-survey.sh` — read-only, reports per-worktree state + drift |
    | **New lane** | T2 (inline) | `bash scripts/new-lane.sh [--sibling] <slug> [base]` — fast scripted provisioning: branch `lane/<slug>` **in-repo** at `.claude/worktrees/<slug>` (gitignored; `--sibling` for an outside `<repo>-<slug>` instead) + carries over gitignored env files. The one-command collision remedy — see "Lane provisioning" below |
    | **Create (bespoke)** | T2 | `git worktree add <path> -b <branch>` via agent — for non-standard layouts the script doesn't cover |
    | **Land** | T2 | Rebase worktree branch onto trunk + test + fast-forward. Multi-step procedure — see "Worktree Land Procedure" below |
    | **Prune (clean)** | T2 | `git worktree prune` for ghost entries (registered but FS-missing). Always safe, no data loss possible |
    | **Remove** | **T3** | `git worktree remove <path>` — destroys filesystem state. Requires preflight + explicit confirm per worktree |
    
    ### Lane provisioning (the collision remedy)
    
    `scripts/new-lane.sh <slug> [base-branch]` is the fast, model-invocable way to isolate parallel
    work — the remedy the peer-writer guards (`session-start-unicode-scan.sh` at boot,
    `pre-write-peer-guard.sh` mid-session) point you to. It:
    
    - creates branch `lane/<slug>` **in-repo** at `<main>/.claude/worktrees/<slug>` — the native
      Claude Code worktree location: tidy (no sibling dirs scattered across the parent) and gitignored
      so `git add -A` can't stage its gitlinks — off `[base-branch]` (default: current branch);
    - **ensures the gitignore precondition**: if `.claude/worktrees/` isn't gitignored it adds the entry
      first (the in-repo location is only safe when ignored), so the default is safe in *any* repo;
    - **`--sibling`** places it outside the repo at `<repo>/../<repo>-<slug>` instead — use when you need
      structural isolation from repo-scoped destructive ops (`git clean -ff`, `rm -rf <repo>`) or in a
      repo that can't gitignore the dir;
    - anchors at the **main** worktree root, so invoking it from inside a lane won't nest worktrees;
    - **carries over gitignored env files** (`.dev.vars`, `.env*`, `.secrets`) the fresh worktree
      would otherwise lack, so the lane runs immediately;
    - prints the worktree path on stdout (everything else on stderr), so it composes:
      `cd "$(bash scripts/new-lane.sh hotfix main)"`;
    - refuses if the branch or path already exists — never clobbers.
    
    Lane work durability: **committed** lane work lives in the shared object store and survives even
    deletion of the worktree dir (recover via `git worktree add <path> lane/<slug>`); only *uncommitted*
    work is at risk from `git clean -ff` / `rm -rf`. Land early/often — see `rules/worktree-boundaries.md`.
    
    Run it **inline** (deterministic, non-destructive); land the lane back via the Worktree Land
    Procedure below or `fleet-ops`. Reach for it whenever two sessions would otherwise share one checkout.
    
    ### Survey-first discipline
    
    Never recommend prune/remove without first running `scripts/worktree-survey.sh`
    and presenting the output to the user. The survey categorises each worktree as:
    
    - `(trunk)` — the main repo itself, never prune
    - `PRUNABLE` — merged into trunk, no uncommitted work, no unpushed commits → safe to remove
    - `has WIP` — uncommitted changes → commit or stash first, never auto-remove
    - `unpushed` — commits ahead of upstream → push or cherry-pick before remove
    - `in-flight` — not merged, not dirty → probably still in active use
    - `GHOST` — registered but filesystem gone → `git worktree prune` fixes
    - `UNREGISTERED` / orphan — filesystem dir with no git entry → **DO NOT touch without explicit review**
    
    ### Worktree Land Procedure (T2)
    
    For landing a branch from a worktree onto the trunk (rebase + test + ff):
    
    1. Verify preconditions: worktree clean, branch ahead of trunk, not already merged
    2. Fetch trunk, rebase worktree branch onto it
    3. Run project test command (detect from `package.json` / `pyproject.toml` / `justfile`)
    4. On test pass: fast-forward trunk to the rebased tip
    5. Do NOT push — that's a separate explicit step (and should go through `push-gate`)
    
    Dispatch this to `git-agent` as a T2 operation with the worktree path + trunk name.
    
    ### Land all — batch-land every pending lane (T1 plan → fleet-ops execution)
    
    The front-door for "I've got 4-5 chips/sessions/worktrees, land the ones that are
    done." git-ops **discovers and classifies**; `fleet-ops` **executes** the sequential,
    test-gated landing. No duplicated landing logic — the two compose.
    
    **Triggers:** "land everything", "land all my worktrees", "land the pending chips",
    "clean up and land what's done", "where are we and land it".
    
    **Procedure:**
    
    1. **Survey (T1, read-only).** Run `scripts/land-all.sh --porcelain` (add `--recent-days N`
       if the user's lanes span longer than a week). Each candidate branch is classified:
    
       | Status | Meaning | Default action |
       |--------|---------|----------------|
       | `LANDABLE` | clean, ahead, not merged, recent, no live writer | **land** |
       | `STALE` | clean + ahead but last commit > `--recent-days` old | park (offer to prune/archive, or land explicitly) |
       | `WIP` | uncommitted tracked changes | park — commit in-lane first |
       | `ACTIVE` | a session is writing it **right now** (recent file activity) | **never land** — park |
       | `MERGED` | already an ancestor of trunk (incl. nothing ahead) | prune candidate |
    
    2. **Confirm the plan (`AskUserQuestion`, HARD RULE).** Present the three groups — *land these
       LANDABLE / park these WIP+ACTIVE+STALE / prune these MERGED* — and get explicit go.
       Never skip this: landing is outward-facing on the trunk. Surface `far behind (N)` notes so the
       user knows which lands may conflict.
    
    3. **Execute via fleet-ops.** For the confirmed landable set:
       ```bash
       bash $HOME/.claude/skills/fleet-ops/scripts/fleet.sh track <landable-branches...>
       bash $HOME/.claude/skills/fleet-ops/scripts/fleet.sh land --all --running
       ```
       fleet-ops lands **oldest-first**, runs the test gate, **auto-rebases** the remaining lanes after
       each land, and marks any lane that hits a real conflict `CONFLICT` — it does **not** guess a
       resolution. This is a T2 write; dispatch through `git-agent` or run inline if the user is waiting.
    
    4. **Escalate conflicts, don't auto-resolve.** A `CONFLICT` lane stops being landed and is reported.
       Offer: resolve in the lane, skip it, or revert (`fleet revert <branch>`). Sequential + auto-rebase
       *minimises* conflicts (each lane rebases onto a trunk that already has the prior lands); genuine
       semantic conflicts are always the user's call.
    
    5. **Offer cleanup (survey-first, T3).** After landing, the `MERGED` branches and any
       now-landed worktrees are prune candidates. Follow **Survey-first discipline** + the T3 Remove
       preflight — never auto-`git worktree remove`; confirm per worktree. Respect
       `rules/worktree-boundaries.md` throughout: `ACTIVE`/orphan/unregistered trees are never touched.
    
    **Safety invariants:** `ACTIVE` lanes (live peer writer) are never landed — this is the
    worktree-boundaries live-writer guard applied to landing. `WIP` needs an explicit commit first.
    `STALE` needs explicit opt-in (`--recent-days` or naming the branch). Only `LANDABLE` lands by default.
    
    ### Boundaries (HARD RULE)
    
    See `rules/worktree-boundaries.md`. Summary:
    
    - **Never** `rm -rf .claude/worktrees/` — the orphan count in survey is informational, never a cleanup cue
    - **Never** `git add -A` when `.claude/worktrees/` has untracked entries (sweeps gitlinks into commits)
    - **Never** decide another session's worktree is "orphaned" — ask first
    - Cross-project work stays cross-project; a worktree in repo X is never our concern when we're operating on repo Y
    
    ## Decision Logic
    
    When a git-related request arrives, follow this flow:
    
    ```
    1. Classify the operation tier (T1/T2/T3)
    
    2. If T1:
       - Execute inline via Bash
       - Format and present results
       - DONE
    
    3. If T2:
       - Gather context (conversation, git state, conventions)
       - Decide foreground vs background:
         * User waiting on result? -> foreground
         * User continuing other work? -> background
       - Dispatch to git-agent with context
       - Relay result when received
    
    4. If T3:
       - Gather context
       - Dispatch to git-agent for PREFLIGHT ONLY
       - Present preflight report to user
       - Wait for explicit confirmation
       - Re-dispatch with execute authority
       - Relay result
    ```
    
    ## Quick Reference
    
    | Task | Tier | Inline/Agent |
    |------|------|-------------|
    | Check status (rich) | T1 | Inline (`scripts/status.sh`) |
    | Worktree survey | T1 | Inline (`scripts/worktree-survey.sh`) |
    | View diff | T1 | Inline |
    | View log | T1 | Inline |
    | List PRs | T1 | Inline |
    | Commit | T2 | Agent |
    | Push | T2 | Agent |
    | Create PR | T2 | Agent |
    | Create tag | T2 | Agent |
    | Create release | T2 | Agent |
    | Stash push/pop | T2 | Agent |
    | Cherry-pick | T2 | Agent |
    | Create branch | T2 | Agent |
    | Create worktree | T2 | Agent |
    | Land worktree | T2 | Agent (rebase + test + ff) |
    | Prune ghost worktrees | T2 | Agent (`git worktree prune`) |
    | Rebase | T3 | Agent (preflight) |
    | Force push | T3 | Agent (preflight) |
    | Reset --hard | T3 | Agent (preflight) |
    | Delete branch | T3 | Agent (preflight) |
    | Discard changes | T3 | Agent (preflight) |
    | Merge to main | T3 | Agent (preflight) |
    | Remove worktree | T3 | Agent (preflight per worktree) |
    
    ## Tools
    
    | Tool | Purpose |
    |------|---------|
    | `git` | All git operations |
    | `gh` | GitHub CLI - PRs, issues, releases, actions |
    | `delta` | Syntax-highlighted diffs (if available) |
    | `lazygit` | Interactive TUI (suggest to user, not for agent) |
    
    ## Additional Resources
    
    For detailed patterns, load:
    - `./references/rebase-patterns.md` - Interactive rebase workflows and safety
    - `./references/stash-patterns.md` - Stash operations and workflows
    - `./references/advanced-git.md` - Bisect, cherry-pick, worktrees, reflog, conflicts
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related