#!/usr/bin/env bash
# pre-push-check.sh  -  primary gate before every push.
#
# Purpose:
#   The repo HAS workflows (test.yml, ci-lite.yml), but they have not executed
#   since 2026-07-25: the jobs complete in ~2 seconds with zero steps recorded,
#   which is a runner/quota block rather than a test failure. Until that is
#   resolved, this hook is not a second opinion - it is the only thing standing
#   between a broken commit and npm.
#
#   It therefore runs the CANONICAL chain, `npm test`, rather than a hand-picked
#   subset. The subset it used to run was missing eight of the eleven steps
#   (lint-skills, lint-mcp-refs, three of the four evals, validate-prefs,
#   scorecard) and drove the smoke suites through the bare
#   `for f in smoke-*.sh` loop that run-smokes.mjs was written to replace,
#   because that loop cannot tell a passing suite from one that exited 0 having
#   asserted nothing. A safety net with holes in it is worse than a known gap:
#   it is trusted.
#
# Two modes, because a gate that runs INSIDE the push is a gate that breaks
# pushes. Git opens the connection to the remote before this hook fires, so a
# six-minute run idles that connection until the server drops it: the first two
# attempts to push the v15.12.0 rename died on a broken pipe with every gate
# green. The work has to happen outside the network window.
#
#   pre-push-check.sh --run    run every gate and stamp the tree as green
#   pre-push-check.sh          verify the stamp only; refuse the push if absent
#
# The hook uses the second form, so a push is either instant or refused - never
# slow. Refusing is the point: no stamp means nothing has verified this tree,
# and the whole reason this file exists is that nothing else will.
#
# Install as a git pre-push hook:
#   ln -sf ../../pipeline/scripts/pre-push-check.sh .git/hooks/pre-push
#   chmod +x .git/hooks/pre-push
#
# Or run manually before pushing:
#   bash pipeline/scripts/pre-push-check.sh
#
# Exit codes:
#   0  -  all gates passed, safe to push
#   1  -  a gate failed (test, lint, schema, or personal-data leak)
#   2  -  environment problem (node missing, npm missing, wrong cwd)

set -uo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT" || { echo "FAIL: can't cd to repo root" >&2; exit 2; }

if ! command -v node >/dev/null 2>&1; then
  echo "FAIL: node not found" >&2
  exit 2
fi
if ! command -v npm >/dev/null 2>&1; then
  echo "FAIL: npm not found" >&2
  exit 2
fi

# A full run takes minutes, and git has already opened the connection to the
# remote by the time this hook runs - a long gate idles that connection until
# the server drops it, which is a broken pipe on a push whose gates all passed.
# So the verdict is cached against the exact tree it was produced from: HEAD
# plus a hash of the working tree, which changes the moment anything does. A
# re-push of an unchanged tree is instant; a single edited byte re-runs
# everything. The stamp is never a way to skip the gate, only a way to avoid
# paying for the same answer twice.
STAMP_DIR="${TMPDIR:-/tmp}/multi-agent-prepush"
mkdir -p "$STAMP_DIR" 2>/dev/null || true
# Key the stamp on CONTENT, not on the commit. Hashing HEAD meant that
# committing the very files the gate had just verified invalidated the stamp:
# same bytes, different SHA, six more minutes. `git write-tree` against a
# throwaway index gives the tree object for the working state, which is
# identical before and after the commit that records it, and changes the
# instant any tracked byte does. The real index is never touched.
TREE_ID=""
_tmpidx=$(mktemp 2>/dev/null) || _tmpidx=""
if [ -n "$_tmpidx" ]; then
  cp "$(git rev-parse --git-dir)/index" "$_tmpidx" 2>/dev/null || true
  TREE_ID=$(GIT_INDEX_FILE="$_tmpidx" git add -A 2>/dev/null \
    && GIT_INDEX_FILE="$_tmpidx" git write-tree 2>/dev/null)
  rm -f "$_tmpidx"
fi
STAMP="$STAMP_DIR/$TREE_ID"

RUN_MODE=0
for arg in "$@"; do
  case "$arg" in
    --run | --force) RUN_MODE=1 ;;
  esac
done

if [ -n "${TREE_ID:-}" ] && [ -f "$STAMP" ] && [ "$RUN_MODE" -eq 0 ]; then
  echo "→ Local CI gate: green for this exact tree ($(cat "$STAMP"))"
  exit 0
fi

# Verify-only (the hook path): refuse rather than run. A push must not wait.
if [ "$RUN_MODE" -eq 0 ]; then
  echo "✗ This tree has not passed the local gate." >&2
  echo "" >&2
  echo "  Run it, then push:" >&2
  echo "    npm run gate" >&2
  echo "" >&2
  echo "  It takes about six minutes and is not run here on purpose: git has" >&2
  echo "  already opened the connection to the remote, and a long hook idles it" >&2
  echo "  until the push dies with every gate green." >&2
  echo "  Bypass (emergencies only): git push --no-verify" >&2
  exit 1
fi

echo "→ Running local CI gate (primary  -  GitHub Actions is not executing)"
echo ""

FAILED=0

run_step() {
  local name="$1"
  shift
  echo "→ $name"
  if "$@"; then
    echo "  ✓ $name passed"
  else
    echo "  ✗ $name FAILED" >&2
    FAILED=$((FAILED+1))
  fi
  echo ""
}

# The full chain, defined once in package.json so this hook cannot drift behind
# it: unit tests, run-smokes.mjs, both linters, all four evals, both schema
# validators and the scorecard.
run_step "npm test (full gate chain)" npm test --silent
run_step "lint"                npx --no-install eslint . --max-warnings 0
run_step "personal-data leak"  bash pipeline/scripts/smoke-personal-data.sh

# v3.7+ output-quality + token-budget gates (best-effort; skip if scripts absent).
if [ -f "$HOME/.claude/scripts/output-quality-check.sh" ]; then
  run_step "output quality"      bash "$HOME/.claude/scripts/output-quality-check.sh"
fi
if [ -f "$HOME/.claude/scripts/token-budget-report.mjs" ]; then
  # Daily roll-up; non-zero exit means a budget breach was detected.
  run_step "token budget"        node "$HOME/.claude/scripts/token-budget-report.mjs" --period daily
fi

if [ "$FAILED" -eq 0 ] && [ -n "${TREE_ID:-}" ]; then
  date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STAMP" 2>/dev/null || true
fi

if [ "$FAILED" -gt 0 ]; then
  echo "" >&2
  echo "✗ $FAILED gate(s) failed. Fix before pushing." >&2
  echo "  To bypass (emergencies only): git push --no-verify" >&2
  exit 1
fi

echo "✓ All gates passed  -  safe to push"
exit 0
