<GitSetup keywords="git, vcs, gitignore, branches, commits, hooks, history, tags, secrets, repo-bootstrap" type="infra-rules" ver="2.0">
  <Mission>
    Foundational git policy for every project: gitignore baseline, branch strategy, commit convention, history hygiene, tag/release semantics, hooks integration, secrets discipline, repo bootstrap.

    Goal: a repo that is reproducible, auditable, safe — no accidental secret leaks, no platform-specific binaries committed, no fabricated «green» status hiding broken history. The repo is the project's canonical record; every commit is part of the audit trail.

    Scope is git itself. Tool choice for hooks runner (lefthook / husky / simple-git-hooks) is the `git-hooks` category in the infrastructure scope discovery session. CI provider is the `ci` category. Both distinct from this rule.
  </Mission>

  <Belief_State>
    <Axiom id="AX_GITIGNORE_BASELINE">
      `.gitignore` covers three layers.

      **Mandatory** (every project):
      - `.claude/settings.local.json` (personal Claude Code overrides; team-shared `.claude/settings.json` IS tracked).
      - `.env`, `.env.local`, `.env.*.local` (secrets; only `.env.example` with placeholders is tracked).
      - OS artifacts: `.DS_Store`, `Thumbs.db`, `desktop.ini`.
      - Editor caches: `.idea/`, `*.swp`, `.vscode/` (or selectively).
      - Coverage: `coverage/`, `*.lcov`, `.nyc_output/`.
      - Logs: `*.log`, `npm-debug.log*`, `yarn-error.log`, etc.

      **Tool-derived** (per chosen stack from infrastructure scope discovery):
      - Node: `node_modules/`, `dist/`, `.next/`, `.nuxt/`, `.cache/`.
      - Python: `__pycache__/`, `*.pyc`, `.venv/`, `.pytest_cache/`, `.ruff_cache/`.
      - Go: `vendor/` (if not committed by policy), build artifacts.
      - Rust: `target/`.

      **Environment-specific:** developer-team decision (e.g., `scripts/local/`).

      Wrong here = secrets in repo, multi-GB bloat, platform-specific native binaries breaking CI.
    </Axiom>

    <Axiom id="AX_BRANCH_STRATEGY">
      Pick one strategy at project init, document in README, enforce via remote branch protection.

      | Strategy | When |
      |---|---|
      | `trunk-based` (recommended default) | small teams + strong CI; short-lived branches off `main`, merged within a day |
      | `github-flow` | long-lived feature branches, PR-driven, no `develop` |
      | `git-flow` | planned release cadences; `main` + `develop` + `release/*` + `hotfix/*` |

      Default branch name: `main` (not `master`) — both locally and on remote.

      Branch naming: `<type>/<short-description>`, where `type ∈ {feature, fix, chore, docs, refactor, test, perf}`. Lowercase, kebab-case.

      Mixing strategies within one team multiplies merge confusion.
    </Axiom>

    <Axiom id="AX_COMMIT_CONVENTION">
      Default: Conventional Commits format `<type>(<scope>): <subject>`. Alternatives acceptable with documented rationale.

      Conventional Commits:
      - Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `perf`, `build`, `ci`, `style`, `revert`.
      - Subject: imperative mood, ≤72 chars, no trailing period.
      - Body (optional): wraps at 100 chars, blank line between subject and body.
      - Footer: `BREAKING CHANGE: …`, issue refs, sign-off.

      Example: `feat(auth): add OAuth2 PKCE flow for native clients`.

      Acceptable alternatives (Decision Log entry required):
      - **plain** — no type prefix, just imperative subject.
      - **DCO sign-off** — `Signed-off-by: Name <email>` mandatory for licensing/legal; compatible with conventional commits.

      Conventional Commits enables auto-changelog, semantic-release, machine-readable history.
    </Axiom>

    <Axiom id="AX_HISTORY_POLICY">
      Linear history via rebase or squash-merge for PRs. Force-push forbidden on shared branches.

      - **Shared** (force-push FORBIDDEN): `main`, `develop`, `release/*`, any branch with active PRs from others.
      - **Personal** (force-push allowed): your own `feature/*`, `fix/*` until merged.

      Merge strategies:
      - `squash-merge` — feature branch becomes single commit on main. Simple history; loses commit granularity.
      - `rebase-and-merge` — feature commits rebased onto main, no merge commit. Preserves granularity; requires clean local history.
      - `merge commit` — discouraged unless branch shape must be preserved (e.g., release branches).

      Hooks bypass: `--no-verify` is FORBIDDEN unless operator explicitly authorizes; if used, the bypass MUST be documented in commit body.

      Shared-branch force-push silently rewrites history collaborators have already pulled. Recovery is manual and error-prone.
    </Axiom>

    <Axiom id="AX_DESTRUCTIVE_COMMANDS_REQUIRE_AUTH">
      Destructive git commands risk silent data loss. NOT blanket-forbidden — they're tools — but each invocation requires explicit operator authorization recorded in the Execution Log.

      **Requires explicit auth** (status `[!] BLOCKED` until operator confirms):
      - `git reset --hard <ref>` — discards working tree + index changes since `<ref>`.
      - `git restore .` (without specific path) — discards all uncommitted changes in working tree.
      - `git restore --staged --worktree <path>` with broad path — same risk.
      - `git clean -fd` / `-fdx` — deletes untracked files (and ignored-files with `-x`).
      - `git checkout -- <path>` (or `git checkout HEAD -- <path>`) with broad path — discards uncommitted changes.
      - `git push --force` / `--force-with-lease` on shared branches.
      - `git branch -D <branch>` (force-delete unmerged) — distinct from `-d` (safe delete).
      - `git rebase -i` — interactive history rewrite.
      - `git reflog expire` / `git gc --prune=now` — drops recoverable history.
      - `git filter-repo` / `git filter-branch` — bulk history rewrite.

      **Protocol per invocation:**
      1. State intent in Execution Log: «Need to <reason>; proposing <command>.»
      2. Set status `[!] BLOCKED` per `AX_BLOCKER_ESCALATION` (in `task-execution`).
      3. Wait for operator's explicit confirmation.
      4. Only then run.

      **Recovery first, destruction second.** Reflex «let me reset --hard and start over» is the failure mode this axiom prevents. Recovery options:
      - `git reflog` — find lost commit/state.
      - `git stash` — park uncommitted changes safely.
      - `git revert` — inverse commit (preserves history).
      - `git checkout <sha> -- <path>` — restore one file from a known-good commit.

      Forbidden as habit: «let me just reset --hard and try again»; `git clean -fdx` to «free disk» without listing what will be deleted; `--force-with-lease` on `main` because it «sounds safer than --force» (both destructive on shared branches).
    </Axiom>

    <Axiom id="AX_TAG_RELEASE">
      Semver tags: `vMAJOR.MINOR.PATCH`. Annotated tags only.

      Format:
      - Stable: `v1.2.3`.
      - Pre-release: `v1.2.3-alpha.1`, `v1.2.3-beta.2`, `v1.2.3-rc.1`.
      - Build metadata (optional, discouraged): `v1.2.3+20260507`.

      Always annotated:
      ```
      git tag -a v1.2.3 -m "Release 1.2.3 — <one-line summary>"
      ```
      Lightweight tags (`git tag v1.2.3` without `-a`) are FORBIDDEN — no metadata, look identical to branch refs.

      Push explicitly: `git push origin v1.2.3` (or `--tags` after release).
    </Axiom>

    <Axiom id="AX_HOOKS_INTEGRATION">
      Hooks fire on every commit/push. Required hooks: `pre-commit` (lint + typecheck on staged, <5s), `commit-msg` (conventional-commits validator), `pre-push` (full test suite). Optional: `prepare-commit-msg` (prepend issue ID), `post-merge` (reinstall if lock changed). Hook config committed to repo; one onboarding command registers them (e.g. `lefthook install`).
    </Axiom>

    <Axiom id="AX_LFS_SUBMODULES">
      LFS for large binaries. Submodules discouraged.

      **Git LFS** — required when tracking binary files >100MB (GitHub hard limit; other hosts vary). Configure `.gitattributes`: `*.psd filter=lfs diff=lfs merge=lfs -text`. LFS adds infrastructure cost (storage, bandwidth quota) — use only when binaries are necessary.

      **Submodules** — discouraged in favor of monorepo or package dependencies. If unavoidable: pin to specific commit (not branch); document in README why; expect fragile workflow (`git submodule update --init --recursive` after every checkout).
    </Axiom>

    <Axiom id="AX_SECRETS_DISCIPLINE">
      Secrets never committed. On leak: rotate first, rewrite history second.

      **Prevention:**
      - `.env`, `.env.local`, `.env.*.local` always gitignored (`AX_GITIGNORE_BASELINE`).
      - `.env.example` tracked with placeholder values to document required keys.
      - Pre-commit hook scans staged files for secret patterns (recommended: `gitleaks`, `detect-secrets`, `trufflehog`).
      - Code review checks for hardcoded credentials in any file extension.

      **On leak detection:**
      1. **Rotate the leaked credential immediately** (revoke + reissue). The credential is compromised the moment it hits a remote, regardless of subsequent rewrite.
      2. Document incident: what leaked, when, who rotated, what notifications went out.
      3. Optional: `git filter-repo` or BFG to scrub history, then force-push (coordinate with team).

      Git history is forever. Once a secret reaches the remote, assume it's harvested by scanners within minutes. Rotation is mandatory; history rewrite is hygiene, not security.
    </Axiom>
  </Belief_State>

  <Setup_Steps>
    <!-- Ordered actions for initial git repository configuration.
         Axioms in Belief_State define the WHY and invariants; these steps define the HOW and sequence. -->
    <Step id="STEP_1_INIT_REPO">
      Run `git init -b main` (or `git init` + `git branch -m main`). Default branch is `main`, never `master`.
    </Step>
    <Step id="STEP_2_CREATE_GITIGNORE">
      Author `.gitignore` **before staging any other file** — first push is the riskiest moment for leaks. Mandatory entries: `.claude/settings.local.json`, `.env`/`.env.local`/`.env.*.local`, `.DS_Store`, `Thumbs.db`, `.idea/`, `*.swp`, `coverage/`, `*.log`. Add tool-derived entries per chosen stack (see AX_GITIGNORE_BASELINE for full reference).
    </Step>
    <Step id="STEP_3_CHOOSE_BRANCH_STRATEGY">
      Choose one strategy — trunk-based (default for small teams with strong CI), github-flow (PR-driven), or git-flow (planned release cadences). Document in README. Immutable after first collaborator push.
    </Step>
    <Step id="STEP_4_FIRST_COMMIT">
      Stage and commit foundational files only: `LICENSE`, `README.md`, `.gitignore`. Message: `chore: initial commit`. Never include `.env`, generated artifacts, `node_modules/`, or credentials in this or any commit.
    </Step>
    <Step id="STEP_5_CONFIGURE_REMOTE_AND_PROTECTION">
      Add remote and push only after `.gitignore` is committed and verified. Before first feature work, configure branch protection on `main`: require PR before merge, require CI status checks, disallow force-push.
    </Step>
    <Step id="STEP_6_INSTALL_HOOKS">
      Install hooks runner per infrastructure scope decision (lefthook / husky / simple-git-hooks). Commit config file (e.g. `lefthook.yml`). Register with one onboarding command. Configure: `pre-commit` (lint + typecheck on staged, target <5s), `commit-msg` (conventional commits), `pre-push` (full test suite).
    </Step>
    <Step id="STEP_7_VERIFY">
      Confirm: `git status` clean; `.gitignore` covers all tool-generated paths; hooks fire on test commit; remote branch protection active.
    </Step>
  </Setup_Steps>

  <Anti_Patterns>
    <Anti_Pattern id="AP_NODE_MODULES_COMMITTED">
      <Bad>`git add node_modules/` or `node_modules/` not in `.gitignore`</Bad>
      <Why_Bad>10–500 MB bloat; macOS-compiled native binaries fail on Linux CI; PR diffs become unreadable. Lock file already encodes the dep tree deterministically.</Why_Bad>
      <Good>`node_modules/` in `.gitignore`. `npm ci` / `pnpm install --frozen-lockfile` restores from lock file.</Good>
    </Anti_Pattern>

    <Anti_Pattern id="AP_FORCE_PUSH_TO_MAIN">
      <Bad>`git push --force origin main`</Bad>
      <Why_Bad>Silently rewrites history collaborators already pulled. Recovery requires every team member to reset their local branch — error-prone, sometimes impossible if uncommitted work is on top.</Why_Bad>
      <Good>Force-push only on personal branches. Shared-branch issues fixed via revert commits.</Good>
    </Anti_Pattern>

    <Anti_Pattern id="AP_SECRET_IN_HISTORY">
      <Bad>Committed `.env` with real API key, then `git rm` in next commit.</Bad>
      <Why_Bad>The secret is in history. Anyone can `git log -p` and recover it. Public repos expose to scanners within seconds.</Why_Bad>
      <Good>Rotate the credential immediately. Then rewrite history with `git filter-repo` and force-push (coordinate with team). Add `.env` to `.gitignore` BEFORE first push.</Good>
    </Anti_Pattern>

    <Anti_Pattern id="AP_NO_VERIFY_AS_HABIT">
      <Bad>`git commit --no-verify` because pre-commit hook is «too slow».</Bad>
      <Why_Bad>Hooks exist to catch mistakes locally. Routinely bypassing shifts mistakes into CI (slower, more expensive) or main (worse). Slow hook = fix the hook, don't skip it.</Why_Bad>
      <Good>Diagnose hook slowness (lint scope too wide? full test suite in pre-commit instead of pre-push?). Optimize. `--no-verify` only with explicit operator authorization, documented in commit body.</Good>
    </Anti_Pattern>

    <Anti_Pattern id="AP_LIGHTWEIGHT_TAG">
      <Bad>`git tag v1.2.3` (no `-a`).</Bad>
      <Why_Bad>No tagger / date / message metadata. Looks identical to a branch ref in some tools. Release lineage unrecoverable.</Why_Bad>
      <Good>`git tag -a v1.2.3 -m "Release 1.2.3 — <summary>"`. Always annotated for release tags.</Good>
    </Anti_Pattern>
  </Anti_Patterns>

  <Verification_Hooks>
    <Hook id="HOOK_GITIGNORE_PRESENT">
      <Purpose>Verify `.gitignore` exists and is non-empty.</Purpose>
      <Command>test -s .gitignore && echo "OK"</Command>
      <Expected>Prints `OK`; exit 0.</Expected>
    </Hook>
    <Hook id="HOOK_NO_ENV_TRACKED">
      <Purpose>Verify no `.env*` (except `.env.example`) is tracked.</Purpose>
      <Command>git ls-files | grep -E '^\.env($|\.)' | grep -v '^\.env\.example$' | head -1 | grep -q . && echo "FAIL: .env tracked" || echo "OK"</Command>
      <Expected>Prints `OK`; exit 0.</Expected>
    </Hook>
    <Hook id="HOOK_NO_NODE_MODULES_TRACKED">
      <Purpose>Verify `node_modules/` is not tracked.</Purpose>
      <Command>git ls-files node_modules 2>/dev/null | head -1 | grep -q . && echo "FAIL: node_modules tracked" || echo "OK"</Command>
      <Expected>Prints `OK`; exit 0.</Expected>
    </Hook>
    <Hook id="HOOK_DEFAULT_BRANCH_MAIN">
      <Purpose>Verify default branch is `main`.</Purpose>
      <Command>git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | grep -q 'main$' && echo "OK" || echo "FAIL: default branch not main"</Command>
      <Expected>Prints `OK`; exit 0.</Expected>
    </Hook>
    <Hook id="HOOK_TAG_ANNOTATED">
      <Purpose>Verify all release tags are annotated (object type = tag, not commit).</Purpose>
      <Command>for t in $(git tag); do test "$(git cat-file -t $t)" = "tag" || echo "FAIL: $t is lightweight"; done; echo "OK"</Command>
      <Expected>Prints only `OK`; exit 0.</Expected>
    </Hook>
  </Verification_Hooks>

  <Reward_Criteria>
    ✅ `.gitignore` covers mandatory + tool-derived layers; first commit only after `.gitignore` in place.
    ✅ Default branch is `main`; remote branch protection prevents direct push, requires PR + green checks.
    ✅ Commits follow chosen convention; subjects ≤72 chars, imperative mood.
    ✅ Shared-branch history is linear; release tags annotated, semver format.
    ✅ Hooks runner installed via single onboarding command; `--no-verify` only with operator auth.
    ✅ No `.env*` (except `.env.example`) tracked; pre-commit secret scanner in place.
    ✅ Destructive commands preceded by explicit operator authorization in Execution Log; recovery preferred over destruction.

    ❌ Secrets in history; `node_modules/` (or equivalent) committed.
    ❌ Force-push to shared branches; lightweight release tags.
    ❌ `--no-verify` as habit; default branch named `master` in new project.
    ❌ Destructive command run as «let me just reset --hard» reflex without operator authorization.
    ❌ `.claude/settings.local.json` tracked.
  </Reward_Criteria>
</GitSetup>
