#!/usr/bin/env bash
#
# Chump pre-commit hook: five jobs.
#
# 1. Lease-collision guard (agent_lease): refuse to commit a file that is
#    claimed by a DIFFERENT live session in `.chump-locks/`. Prevents silent
#    stomps when two agents race on the same path. Default ON; bypass with
#    `CHUMP_LEASE_CHECK=0` or `git commit --no-verify`. See
#    `docs/AGENT_COORDINATION.md` for the full coordination model.
#
# 2. Stomp-warning (INFRA-WORKTREE-STAGING): non-blocking warning when
#    staged files have a working-tree mtime older than CHUMP_STOMP_WARN_SECS
#    (default 600s). Catches the case where agent A `git add`s a file but
#    doesn't commit, agent B `git add`s something unrelated, and the stale
#    file silently rides along in B's commit. Default ON; disable with
#    `CHUMP_STOMP_WARN=0`.
#
# 3. Cargo-fmt auto-fix: keep the tree cargo-fmt-clean so CI's
#    `cargo fmt --all -- --check` never trips. With multiple agents
#    committing in parallel, drift was happening every few commits and
#    burning 20 minutes per dependabot rebase.
#
# 4. Cargo-check build guard (coordination audit 2026-04-17): refuse
#    to commit code that doesn't compile. Kills the "fix(ci):" follow-up
#    churn — 12 such commits in 48h (8% of all commits) were the cost
#    of letting broken-compile commits land. ~5-15s incremental on the
#    developer's machine; saves ~5 minutes of CI queue time per mistake.
#    Default ON; bypass with `CHUMP_CHECK_BUILD=0` for genuine WIP
#    commits or `git commit --no-verify`. Only runs when staged .rs
#    files are present.
#
# 5. gaps.yaml write discipline (coordination audit 2026-04-17): per
#    CLAUDE.md the ONLY per-session status fields (status: in_progress,
#    claimed_by, claimed_at) belong in .chump-locks/ lease files, NOT
#    in docs/gaps.yaml. docs/gaps.yaml is an append-only ledger: new
#    gaps + flipping open→done with closed_by/closed_date. This guard
#    rejects commits that add `status: in_progress` or `claimed_by:`
#    lines to gaps.yaml so the 6-commits-a-day yaml-thrash loop stops.
#    Also includes (INFRA-GAPS-DEDUP 2026-04-19): a duplicate-ID guard
#    that rejects any commit where two gaps in the staged file share the
#    same id. This was the root cause of 7 collision pairs found in the
#    initial audit. Default ON; bypass with `CHUMP_GAPS_LOCK=0` or
#    --no-verify.
#
# Install once: `./scripts/install-hooks.sh`
# Skip for one commit: `git commit --no-verify` (use sparingly — also
# skips the lease check, which defeats the coordination system).

set -e

REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)

# ------------------------------------------------------------------
# 1. Lease-collision guard
# ------------------------------------------------------------------

if [ "${CHUMP_LEASE_CHECK:-1}" != "0" ]; then
    LOCKS_DIR="$REPO_ROOT/.chump-locks"
    if [ -d "$LOCKS_DIR" ]; then
        # Same session-ID precedence as src/agent_lease.rs::current_session_id:
        # CHUMP_SESSION_ID env → $HOME/.chump/session_id → (unknown; don't block).
        MY_SESSION_ID="${CHUMP_SESSION_ID:-}"
        if [ -z "$MY_SESSION_ID" ] && [ -f "$HOME/.chump/session_id" ]; then
            MY_SESSION_ID=$(head -n1 "$HOME/.chump/session_id" 2>/dev/null | tr -d '[:space:]')
        fi

        STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR || true)

        # Only run if we have both a session ID AND staged files. When a
        # contributor commits without a session ID (pure-human flow) we let
        # them through — lease conflicts only matter between bots with IDs.
        if [ -n "$STAGED_FILES" ] && [ -n "$MY_SESSION_ID" ]; then
            now_epoch=$(date -u +%s)
            conflict=""

            for lease_file in "$LOCKS_DIR"/*.json; do
                [ -f "$lease_file" ] || continue

                holder=$(sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$lease_file" | head -n1)
                [ -n "$holder" ] || continue
                [ "$holder" = "$MY_SESSION_ID" ] && continue

                # Expiry check — RFC3339 -> epoch. macOS uses -j -f, Linux uses -d.
                # Unparseable → treat as live (safer: block rather than stomp).
                expires=$(sed -n 's/.*"expires_at"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$lease_file" | head -n1)
                exp_epoch=""
                if [ -n "$expires" ]; then
                    if [ "$(uname -s)" = "Darwin" ]; then
                        exp_epoch=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$expires" +%s 2>/dev/null || true)
                    else
                        exp_epoch=$(date -u -d "$expires" +%s 2>/dev/null || true)
                    fi
                fi
                if [ -n "$exp_epoch" ] && [ "$exp_epoch" -le "$now_epoch" ]; then
                    continue  # expired
                fi

                # Extract paths[] from the JSON. Handles BOTH formats we see:
                #   - pretty-printed (one path per line), written by Rust atomic_write
                #   - compact single-line array, written by some external agents
                # Logic: include the line that STARTS the paths array, include
                # subsequent lines until we see `]` (which closes the array).
                # Don't `next` past the starting line or the compact `[...]`
                # form loses everything.
                # Filter out the literal "paths" token which slips through
                # the quoted-string extractor.
                paths=$(awk '
                    /"paths"[[:space:]]*:/ { flag=1 }
                    flag            { print }
                    flag && /\]/    { flag=0 }
                ' "$lease_file" \
                    | grep -oE '"[^"]+"' \
                    | sed 's/^"//;s/"$//' \
                    | grep -vx 'paths' || true)

                for pattern in $paths; do
                    [ -n "$pattern" ] || continue

                    # Determine match mode from the pattern.
                    #   `**`           → matches everything
                    #   `foo/bar/**`   → prefix `foo/bar/`
                    #   `foo/bar/`     → prefix `foo/bar/`
                    #   `foo/bar.rs`   → exact match
                    prefix=""
                    exact=""
                    if [ "$pattern" = "**" ]; then
                        prefix=""
                        exact=""
                        # Matches everything — just mark conflict against the first staged file.
                        first_staged=$(printf '%s\n' "$STAGED_FILES" | head -n1)
                        conflict="$first_staged  (claimed by $holder as wildcard $pattern)"
                        break 2
                    elif [ "${pattern%/\*\*}" != "$pattern" ]; then
                        prefix="${pattern%/\*\*}/"
                    elif [ "${pattern%/}" != "$pattern" ]; then
                        prefix="$pattern"
                    else
                        exact="$pattern"
                    fi

                    while IFS= read -r staged; do
                        [ -n "$staged" ] || continue
                        if [ -n "$prefix" ]; then
                            case "$staged/" in
                                "$prefix"*)
                                    conflict="$staged  (claimed by $holder as prefix $pattern)"
                                    break 2
                                    ;;
                            esac
                        elif [ -n "$exact" ] && [ "$staged" = "$exact" ]; then
                            conflict="$staged  (claimed exactly by $holder)"
                            break 2
                        fi
                    done <<EOF_STAGED
$STAGED_FILES
EOF_STAGED
                done
            done

            if [ -n "$conflict" ]; then
                echo "[pre-commit] LEASE CONFLICT — another agent claims files you're committing:" >&2
                echo "[pre-commit]   $conflict" >&2
                echo "[pre-commit]" >&2
                echo "[pre-commit] Options:" >&2
                echo "[pre-commit]   - wait for the other session's lease to expire (chump --leases)" >&2
                echo "[pre-commit]   - coordinate directly; have them release their lease" >&2
                echo "[pre-commit]   - bypass: CHUMP_LEASE_CHECK=0 git commit ..." >&2
                echo "[pre-commit]   - skip ALL hooks: git commit --no-verify" >&2
                exit 1
            fi
        fi
    fi
fi

# ------------------------------------------------------------------
# 2. Stomp-warning — non-blocking flag for stale staged files
# ------------------------------------------------------------------
#
# Background: the main Chump worktree is shared by multiple agents. When
# agent A `git add`s foo.rs at 14:00 and agent B runs `git add bar.rs &&
# git commit` at 14:30, B's commit silently includes A's staged foo.rs.
# Observed twice in one session (cf79287 + a5b5053 in gaps.yaml).
#
# Heuristic: if ANY staged file has a working-tree mtime older than
# CHUMP_STOMP_WARN_SECS seconds, warn on stderr. Non-blocking — this is
# intentionally advisory. False positives (legitimate commits that touch
# files edited hours ago) are noise, not blockers. True positives save
# us from silent stomps.

if [ "${CHUMP_STOMP_WARN:-1}" != "0" ]; then
    STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR || true)
    if [ -n "$STAGED_FILES" ]; then
        now_epoch=$(date -u +%s)
        threshold="${CHUMP_STOMP_WARN_SECS:-600}"
        stale_lines=""
        while IFS= read -r f; do
            [ -z "$f" ] && continue
            # Only check files that still exist in the working tree.
            # Deletions and renames-to-nonexistent are out of scope.
            [ -f "$f" ] || continue
            # stat flags differ macOS vs Linux. Try macOS first; fall back to
            # Linux on failure.
            mtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0)
            [ "$mtime" -gt 0 ] || continue
            age=$((now_epoch - mtime))
            if [ "$age" -gt "$threshold" ]; then
                stale_lines="${stale_lines}  - $f (mtime ${age}s ago)"$'\n'
            fi
        done <<EOF_STAGED
$STAGED_FILES
EOF_STAGED

        if [ -n "$stale_lines" ]; then
            echo "[pre-commit] STOMP WARNING — staged files with mtime > ${threshold}s:" >&2
            printf '%s' "$stale_lines" >&2
            echo "[pre-commit] If these were staged by another agent, unstage them now:" >&2
            echo "[pre-commit]   git reset HEAD <file>" >&2
            echo "[pre-commit] Prefer scripts/chump-commit.sh <file1> [...] -m '...' which" >&2
            echo "[pre-commit] resets unrelated staging automatically." >&2
            # Opt-in hard block: CHUMP_STRICT_STAGING=1 promotes the warning to
            # an error. Off by default because human commits often bundle
            # legit multi-file changes with stale mtimes (e.g. touched a week
            # ago). Bot sessions should set this in their env.
            if [ "${CHUMP_STRICT_STAGING:-0}" = "1" ]; then
                echo "[pre-commit] CHUMP_STRICT_STAGING=1 — refusing commit." >&2
                exit 1
            fi
            echo "[pre-commit] If legitimate, proceed. To silence: CHUMP_STOMP_WARN=0 git commit ..." >&2
            echo "[pre-commit] Threshold tuning: CHUMP_STOMP_WARN_SECS=<seconds>" >&2
        fi
    fi
fi

# ------------------------------------------------------------------
# 5. gaps.yaml write discipline (runs BEFORE fmt early-exit so it
#    catches yaml-only commits too)
# ------------------------------------------------------------------
#
# Per CLAUDE.md: status/claim fields live in .chump-locks/ lease files,
# NOT in docs/gaps.yaml. This hook rejects adds of `status: in_progress`,
# `claimed_by:`, or `claimed_at:` lines on staged gaps.yaml to stop the
# hot-write loop (6 docs(gaps) commits in 48h before this audit).

if [ "${CHUMP_GAPS_LOCK:-1}" != "0" ]; then
    if git diff --cached --name-only --diff-filter=ACM | grep -qx 'docs/gaps.yaml'; then
        # Added lines only (ignore context + deletions).
        added=$(git diff --cached --unified=0 -- docs/gaps.yaml \
            | grep -E '^\+[^+]' | sed 's/^\+//')
        offending=""
        if printf '%s\n' "$added" | grep -qE '^[[:space:]]*status:[[:space:]]*in_progress[[:space:]]*$'; then
            offending="${offending}  - adds 'status: in_progress' (put claim in .chump-locks/ instead)"$'\n'
        fi
        if printf '%s\n' "$added" | grep -qE '^[[:space:]]*claimed_by:[[:space:]]*'; then
            offending="${offending}  - adds 'claimed_by:' (put claim in .chump-locks/ instead)"$'\n'
        fi
        if printf '%s\n' "$added" | grep -qE '^[[:space:]]*claimed_at:[[:space:]]*'; then
            offending="${offending}  - adds 'claimed_at:' (put claim in .chump-locks/ instead)"$'\n'
        fi
        if [ -n "$offending" ]; then
            echo "[pre-commit] gaps.yaml DISCIPLINE — per CLAUDE.md, claim state lives in .chump-locks/" >&2
            printf '%s' "$offending" >&2
            echo "[pre-commit] Claim the gap via: scripts/gap-claim.sh <GAP-ID>" >&2
            echo "[pre-commit] Only flip gaps.yaml on ship (status: done + closed_by + closed_date)." >&2
            echo "[pre-commit] Bypass: CHUMP_GAPS_LOCK=0 git commit ..." >&2
            exit 1
        fi

        # NEW (INFRA-GAPS-DEDUP 2026-04-19): duplicate-ID guard.
        # Reject any commit that introduces a NEW gap entry whose id: value
        # already exists in gaps.yaml (either in HEAD or in the staged version).
        # This is the root cause of the 7 collision pairs fixed in INFRA-GAPS-DEDUP.
        # A new id is detected by scanning the added (+) lines for `- id: XXX`
        # and cross-checking against the full staged file (or HEAD).
        # Bypass: CHUMP_GAPS_LOCK=0
        if command -v python3 >/dev/null 2>&1; then
            dup_ids=$(python3 - <<'PYEOF_DUP'
import subprocess, sys, re
# Get the staged gaps.yaml content
try:
    staged = subprocess.check_output(["git", "show", ":docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    sys.exit(0)
# Collect all ids in the staged file
all_ids = re.findall(r'^\s*-\s*id:\s*(\S+)', staged, re.MULTILINE)
# Find duplicates
seen = {}
dups = []
for gid in all_ids:
    if gid in seen:
        if gid not in dups:
            dups.append(gid)
    else:
        seen[gid] = 1
if dups:
    print("\n".join(dups))
PYEOF_DUP
)
            if [ -n "$dup_ids" ]; then
                echo "[pre-commit] DUPLICATE GAP ID — gaps.yaml now contains duplicate id(s):" >&2
                echo "$dup_ids" | sed 's/^/  - /' >&2
                echo "" >&2
                echo "[pre-commit] Each gap must have a unique id. Use the next available ID in its series." >&2
                echo "[pre-commit] To find the next available id, run:" >&2
                echo "[pre-commit]   python3 -c \"import yaml; d=yaml.safe_load(open('docs/gaps.yaml')); print(sorted(g['id'] for g in d['gaps'] if g['id'].startswith('COG-')))\"" >&2
                echo "[pre-commit] Bypass (e.g. for the dedup fix itself): CHUMP_GAPS_LOCK=0 git commit ..." >&2
                exit 1
            fi
        fi

        # NEW (2026-04-18 incident): gap-ID redefinition guard.
        # If gaps.yaml diff CHANGES (not just appends) an existing gap's
        # `title:` or `description:`, that's a hijack — another agent
        # silently took an ID someone else was using. Caught the
        # PR #60 (EVAL-011 fixture-expansion → judge-bias) collision.
        # Only check when origin/main is reachable.
        if git rev-parse --quiet --verify origin/main >/dev/null 2>&1; then
            base="origin/main"
        else
            base="HEAD~1"
        fi
        # Find every `id: XXX-NNN` on origin/main; for each, check that
        # OUR diff vs origin/main doesn't change the title/description on
        # the same gap block. We use a python helper because shell yaml
        # parsing is fragile.
        if command -v python3 >/dev/null 2>&1; then
            redefinitions=$(python3 - "$base" <<'PYEOF'
import subprocess, sys, re
base = sys.argv[1]
def gap_titles(text):
    """Returns {gap_id: (title, description_first_line)}."""
    out = {}
    cur_id = None
    desc_seen = False
    for line in text.splitlines():
        m = re.match(r'^\s*-\s*id:\s*(\S+)', line)
        if m:
            cur_id = m.group(1)
            out[cur_id] = ["", ""]
            desc_seen = False
            continue
        if cur_id and re.match(r'^\s*title:', line):
            out[cur_id][0] = line.strip()
        if cur_id and re.match(r'^\s*description:', line):
            desc_seen = True
            continue
        if cur_id and desc_seen and out[cur_id][1] == "" and line.strip():
            out[cur_id][1] = line.strip()[:80]
            desc_seen = False
    return out
try:
    base_text = subprocess.check_output(["git","show", f"{base}:docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    sys.exit(0)
try:
    new_text = subprocess.check_output(["git","show", ":docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    sys.exit(0)
old = gap_titles(base_text)
new = gap_titles(new_text)
collisions = []
for gid, (old_title, old_desc) in old.items():
    if gid in new:
        new_title, new_desc = new[gid]
        if old_title and new_title and old_title != new_title:
            collisions.append(f"{gid}: title changed\n    OLD: {old_title}\n    NEW: {new_title}")
        elif old_desc and new_desc and old_desc != new_desc:
            collisions.append(f"{gid}: description changed (first line)\n    OLD: {old_desc}\n    NEW: {new_desc}")
print("\n".join(collisions))
PYEOF
)
            if [ -n "$redefinitions" ]; then
                echo "[pre-commit] gap-ID HIJACK — gaps.yaml diff redefines existing gap(s):" >&2
                echo "$redefinitions" | sed 's/^/  /' >&2
                echo "" >&2
                echo "[pre-commit] If you meant to track NEW work, use a NEW gap ID." >&2
                echo "[pre-commit] If you meant to UPDATE the existing gap (e.g. close it" >&2
                echo "[pre-commit]   with status:done + closed_date), keep title/description" >&2
                echo "[pre-commit]   intact and add a resolution_notes block instead." >&2
                echo "[pre-commit] Bypass: CHUMP_GAPS_LOCK=0 git commit ..." >&2
                exit 1
            fi
        fi

        # NEW (INFRA-014, 2026-04-21): recycled-ID guard.
        # A gap ID that was closed (status: done) in origin/main should not be
        # silently reopened by a diff that flips it back to status: open. New
        # work under the same problem needs a new ID. This closes the gap
        # between the duplicate-ID guard (catches two entries with same id in
        # one file) and the hijack guard (catches title/description rewrites):
        # reopening a done gap under the same id, with title/description left
        # intact, slipped past both. Bypass: CHUMP_GAPS_LOCK=0
        if command -v python3 >/dev/null 2>&1 && git rev-parse --quiet --verify origin/main >/dev/null 2>&1; then
            reopened=$(python3 - <<'PYEOF_RECYCLE'
import subprocess, sys, re
def gap_statuses(text):
    out = {}
    cur_id = None
    for line in text.splitlines():
        m = re.match(r'^\s*-\s*id:\s*(\S+)', line)
        if m:
            cur_id = m.group(1)
            out.setdefault(cur_id, None)
            continue
        if cur_id:
            s = re.match(r'^\s*status:\s*(\S+)', line)
            if s:
                out[cur_id] = s.group(1)
    return out
try:
    base_text = subprocess.check_output(["git","show","origin/main:docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
    new_text = subprocess.check_output(["git","show",":docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    sys.exit(0)
old = gap_statuses(base_text)
new = gap_statuses(new_text)
reopened = []
for gid, old_status in old.items():
    if old_status == "done" and gid in new and new[gid] and new[gid] != "done":
        reopened.append(f"{gid}: done -> {new[gid]}")
print("\n".join(reopened))
PYEOF_RECYCLE
)
            if [ -n "$reopened" ]; then
                echo "[pre-commit] gap-ID RECYCLE — commit reopens closed gap(s):" >&2
                echo "$reopened" | sed 's/^/  /' >&2
                echo "" >&2
                echo "[pre-commit] A gap closed on origin/main should not be reopened under the" >&2
                echo "[pre-commit]   same id. File a new gap with a fresh id that references the" >&2
                echo "[pre-commit]   original in its description if follow-up work is needed." >&2
                echo "[pre-commit] Bypass: CHUMP_GAPS_LOCK=0 git commit ..." >&2
                exit 1
            fi
        fi
    fi
fi

# ------------------------------------------------------------------
# 6b. Preregistration guard (RESEARCH-019): reject EVAL-*/RESEARCH-*
#     status:done commits that lack a docs/eval/preregistered/<GAP-ID>.md
#     in the staged tree. Enforces the protocol that every gap with live
#     data collection must lock its hypothesis before the first trial runs.
#     Bypass: CHUMP_PREREG_CHECK=0
# ------------------------------------------------------------------

if [ "${CHUMP_PREREG_CHECK:-1}" != "0" ]; then
    if git diff --cached --name-only --diff-filter=ACM | grep -qx 'docs/gaps.yaml'; then
        # Find gap IDs being flipped to status: done in this commit.
        # Strategy: scan +lines in the gaps.yaml diff for 'status: done' that
        # are immediately preceded (within the same hunk) by a gap ID line.
        # We use a simple python helper because the context-aware read is
        # easier than awk for multi-line analysis.
        if command -v python3 >/dev/null 2>&1; then
            missing_prereg=$(python3 - <<'PYEOF_PREREG'
import subprocess, sys, re

# Get the old and new gaps.yaml content
try:
    old_text = subprocess.check_output(["git", "show", "origin/main:docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    try:
        old_text = subprocess.check_output(["git", "show", "HEAD:docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
    except Exception:
        sys.exit(0)
try:
    new_text = subprocess.check_output(["git", "show", ":docs/gaps.yaml"], text=True, stderr=subprocess.DEVNULL)
except Exception:
    sys.exit(0)

def parse_statuses(text):
    """Returns {gap_id: status}."""
    out = {}
    cur_id = None
    for line in text.splitlines():
        m = re.match(r'^\s*-\s*id:\s*(\S+)', line)
        if m:
            cur_id = m.group(1)
        elif cur_id and re.match(r'^\s*status:\s*(\S+)', line):
            s = re.match(r'^\s*status:\s*(\S+)', line).group(1)
            out[cur_id] = s

    return out

old = parse_statuses(old_text)
new = parse_statuses(new_text)

# Find gaps newly flipped to done that match EVAL-* or RESEARCH-*
missing = []
for gid, new_status in new.items():
    if new_status != 'done':
        continue
    old_status = old.get(gid, 'open')
    if old_status == 'done':
        continue  # already done, no new flip
    if not re.match(r'^(EVAL|RESEARCH)-', gid):
        continue  # only enforce for EVAL-* and RESEARCH-*

    # Check if preregistration file exists in staged tree or HEAD
    prereg_path = f"docs/eval/preregistered/{gid}.md"
    in_staged = False
    in_head = False
    try:
        subprocess.check_output(["git", "show", f":{prereg_path}"], stderr=subprocess.DEVNULL)
        in_staged = True
    except Exception:
        pass
    if not in_staged:
        try:
            subprocess.check_output(["git", "show", f"HEAD:{prereg_path}"], stderr=subprocess.DEVNULL)
            in_head = True
        except Exception:
            pass
    if not in_staged and not in_head:
        # Also check origin/main
        in_main = False
        try:
            subprocess.check_output(["git", "show", f"origin/main:{prereg_path}"], stderr=subprocess.DEVNULL)
            in_main = True
        except Exception:
            pass
        if not in_main:
            missing.append(gid)

print("\n".join(missing))
PYEOF_PREREG
)
            if [ -n "$missing_prereg" ]; then
                echo "[pre-commit] PREREGISTRATION REQUIRED (RESEARCH-019) — closing eval/research gap(s) without a preregistration:" >&2
                echo "$missing_prereg" | while IFS= read -r gid; do
                    echo "  - $gid → missing docs/eval/preregistered/${gid}.md" >&2
                done
                echo "" >&2
                echo "[pre-commit] For each EVAL-* or RESEARCH-* gap that collects live trial data," >&2
                echo "[pre-commit] a preregistration must be committed BEFORE the first sweep runs:" >&2
                echo "[pre-commit]   cp docs/eval/preregistered/TEMPLATE.md docs/eval/preregistered/<GAP-ID>.md" >&2
                echo "[pre-commit]   # fill all fields, then: git add + git commit" >&2
                echo "[pre-commit]   # THEN run your sweep" >&2
                echo "[pre-commit]" >&2
                echo "[pre-commit] Bypass (retrospective or doc-only gap): CHUMP_PREREG_CHECK=0 git commit ..." >&2
                exit 1
            fi
        fi
    fi
fi

# ------------------------------------------------------------------
# 6. Submodule sanity (NEW 2026-04-19): catch dangling gitlinks BEFORE
#    they land. Commit 08da134 ("Updated config files") added the
#    sql-migrate gitlink (mode 160000) without an entry in .gitmodules,
#    which broke actions/checkout submodule init on EVERY subsequent PR
#    until PR #103 removed it. Cost the team ~20 stalled CI runs.
#
#    Rejects commits that add a new gitlink (mode 160000) where the
#    path is NOT also listed in .gitmodules (either staged or HEAD).
#    Bypass: CHUMP_SUBMODULE_CHECK=0
# ------------------------------------------------------------------

if [ "${CHUMP_SUBMODULE_CHECK:-1}" != "0" ]; then
    new_gitlinks=$(git diff --cached --raw | awk '$2 == "160000" && $5 == "A" { print $6 }')
    if [ -n "$new_gitlinks" ]; then
        offending=""
        for path in $new_gitlinks; do
            in_staged=""
            if git diff --cached --name-only | grep -qx '.gitmodules'; then
                in_staged=$(git show ":.gitmodules" 2>/dev/null \
                    | awk -v p="$path" '/^\[submodule/{ in_section=0 } $1=="path" && $3==p { in_section=1 } END{ print in_section }')
            fi
            in_head=$(git show "HEAD:.gitmodules" 2>/dev/null \
                | awk -v p="$path" '/^\[submodule/{ in_section=0 } $1=="path" && $3==p { in_section=1 } END{ print in_section }' || true)
            if [ "$in_staged" != "1" ] && [ "$in_head" != "1" ]; then
                offending="${offending}  - ${path} (gitlink added but no matching path entry in .gitmodules)\n"
            fi
        done
        if [ -n "$offending" ]; then
            echo "[pre-commit] DANGLING GITLINK — commit adds submodule gitlink(s) without .gitmodules entry:" >&2
            printf "$offending" >&2
            echo "[pre-commit]" >&2
            echo "[pre-commit] This BREAKS CI for every PR (actions/checkout fails on submodule init):" >&2
            echo "[pre-commit]   fatal: No url found for submodule path '<path>' in .gitmodules" >&2
            echo "[pre-commit]" >&2
            echo "[pre-commit] If you meant to add a submodule, use:" >&2
            echo "[pre-commit]   git submodule add <url> <path>" >&2
            echo "[pre-commit] If this gitlink is leftover from an undone 'submodule add', remove it:" >&2
            echo "[pre-commit]   git rm --cached <path>" >&2
            echo "[pre-commit]" >&2
            echo "[pre-commit] Bypass (very rare): CHUMP_SUBMODULE_CHECK=0 git commit ..." >&2
            exit 1
        fi
    fi
fi

# ------------------------------------------------------------------
# 3. Cargo-fmt auto-fix
# ------------------------------------------------------------------

staged_rust=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.rs$' || true)
if [ -z "$staged_rust" ]; then
    exit 0
fi

if ! command -v cargo > /dev/null 2>&1; then
    echo "[pre-commit] cargo not found; skipping fmt check (CI will catch drift)" >&2
    exit 0
fi

if ! cargo fmt --all 2> /tmp/chump-pre-commit-fmt.log; then
    echo "[pre-commit] cargo fmt failed (likely a syntax error). Output:" >&2
    cat /tmp/chump-pre-commit-fmt.log >&2
    echo "[pre-commit] Fix the syntax error or commit with --no-verify." >&2
    exit 1
fi

modified=$(git diff --name-only --diff-filter=M | grep -E '\.rs$' || true)
if [ -n "$modified" ]; then
    restage=$(comm -12 <(echo "$staged_rust" | sort -u) <(echo "$modified" | sort -u))
    if [ -n "$restage" ]; then
        echo "$restage" | xargs git add
        echo "[pre-commit] cargo fmt auto-formatted + re-staged:" >&2
        echo "$restage" | sed 's/^/  - /' >&2
    fi
fi

# ------------------------------------------------------------------
# 4. Cargo-check build guard — fails fast on compile errors
# ------------------------------------------------------------------
#
# CI runs the full test suite; that's slow (~2 min on a fresh runner,
# plus queue time). Running `cargo check --bin chump` locally first is
# ~5-15s incremental — much cheaper than a CI round-trip. The cost/
# benefit flips hard the moment TWO bots push broken-compile commits
# close together: the second agent's rebase onto main fails, they
# write a "fix(ci):" commit, the cycle compounds.
#
# This guard runs `cargo check --bin chump --tests` so it catches
# both library and test-binary compile errors. Default ON; bypass
# with CHUMP_CHECK_BUILD=0 (for WIP commits you know won't compile)
# or --no-verify.

if [ "${CHUMP_CHECK_BUILD:-1}" != "0" ]; then
    # Use `--bin chump --tests` to cover both runtime and test compiles.
    # `--release` would be more faithful to CI but too slow for a hook;
    # the debug profile catches >99% of type/borrow errors.
    check_log="/tmp/chump-pre-commit-check-$$.log"
    trap 'rm -f "$check_log"' EXIT
    if ! cargo check --bin chump --tests >"$check_log" 2>&1; then
        echo "[pre-commit] CARGO CHECK FAILED — compile error in staged code." >&2
        echo "[pre-commit] ---- last 30 lines of error output: ----" >&2
        tail -30 "$check_log" >&2
        echo "[pre-commit] -----------------------------------------" >&2
        echo "[pre-commit] Fix the errors, or:" >&2
        echo "[pre-commit]   - CHUMP_CHECK_BUILD=0 git commit ...  (explicit WIP)" >&2
        echo "[pre-commit]   - git commit --no-verify              (bypass all hooks)" >&2
        echo "[pre-commit] Full log: $check_log" >&2
        # Let the log persist on failure for the developer to read.
        trap - EXIT
        exit 1
    fi
fi

# ------------------------------------------------------------------
# 6. docs-delta check (INFRA-009, 2026-04-20)
# ------------------------------------------------------------------
# Red Letter #3: docs/ grew 66 → 119 → 139 files across three review
# cycles with zero deletions. Counter-pressure: if a commit adds a
# docs/*.md file, require EITHER a deletion of another docs/*.md OR a
# "Net-new-docs: +N" trailer in the commit message. Advisory (warning
# only) until 2026-04-28, blocking after. Bypass: CHUMP_DOCS_DELTA_CHECK=0.

if [ "${CHUMP_DOCS_DELTA_CHECK:-1}" != "0" ]; then
    ADDED=$(git diff --cached --name-only --diff-filter=A -- 'docs/*.md' | wc -l | tr -d ' ')
    DELETED=$(git diff --cached --name-only --diff-filter=D -- 'docs/*.md' | wc -l | tr -d ' ')
    if [ "$ADDED" -gt 0 ] && [ "$ADDED" -gt "$DELETED" ]; then
        MSG_FILE="${1:-$REPO_ROOT/.git/COMMIT_EDITMSG}"
        TRAILER_OK=0
        if [ -f "$MSG_FILE" ] && grep -qiE '^Net-new-docs:\s*\+?[0-9]+' "$MSG_FILE"; then
            TRAILER_OK=1
        fi
        if [ "$TRAILER_OK" -eq 0 ]; then
            NET=$((ADDED - DELETED))
            # Advisory until 2026-04-28 then blocking (date-gated)
            TODAY=$(date +%Y%m%d)
            MODE="warn"
            [ "$TODAY" -ge 20260428 ] && MODE="block"
            echo ""
            echo "⚠  docs-delta: commit adds $ADDED docs/*.md, deletes $DELETED (net +$NET)"
            echo "   Red Letter #3 counter-pressure: either delete/archive a comparable doc,"
            echo "   or add a commit-message trailer:    Net-new-docs: +$NET"
            echo "   Bypass: CHUMP_DOCS_DELTA_CHECK=0 git commit ..."
            if [ "$MODE" = "block" ]; then
                exit 1
            fi
        fi
    fi
fi

# ------------------------------------------------------------------
# 7. credential-pattern guard (INFRA-018, 2026-04-20)
# ------------------------------------------------------------------
# Scan staged diff for common API-key / token shapes. Blocks the commit
# if any match. Bypass: CHUMP_CREDENTIAL_CHECK=0.

if [ "${CHUMP_CREDENTIAL_CHECK:-1}" != "0" ]; then
    ADDED_LINES=$(git diff --cached --unified=0 -- . ':(exclude)scripts/git-hooks/pre-commit' 2>/dev/null \
        | grep -E '^\+' | grep -vE '^\+\+\+' || true)
    if [ -n "$ADDED_LINES" ]; then
        PATTERNS='(sk-ant-[A-Za-z0-9_-]{30,})|(sk-[A-Za-z0-9]{32,})|(tgp_v1_[A-Za-z0-9_-]{30,})|(AIzaSy[A-Za-z0-9_-]{30,})|(ghp_[A-Za-z0-9]{30,})|(github_pat_[A-Za-z0-9_]{30,})'
        HIT=$(printf '%s\n' "$ADDED_LINES" | grep -oE "$PATTERNS" | head -3 || true)
        if [ -n "$HIT" ]; then
            echo "" >&2
            echo "🚫 credential-pattern guard (INFRA-018): staged diff contains what looks like a secret" >&2
            printf '%s\n' "$HIT" | sed 's/^/   match: /' >&2
            echo "" >&2
            echo "   If real: unstage the file and rotate the key immediately." >&2
            echo "   If a false positive (fixture/example), bypass with:" >&2
            echo "     CHUMP_CREDENTIAL_CHECK=0 git commit ..." >&2
            exit 1
        fi
    fi
fi

exit 0
