#!/bin/bash
# .githooks/pre-commit — Shared State File Guard
#
# 共享状态文件禁止在非 main 分支创作；与 origin/main 完全一致的
# merge/rebase carry-in 不属于 feature branch 创作，可以提交。
# 规则来源：cat-cafe-skills/refs/shared-rules.md §14
#
# 共享状态文件清单（严格定义，不是整个 docs/features/）：
#   - docs/BACKLOG.md
#   - cat-config.json
#
# 注意：docs/features/F*.md 的 status/owner 字段变更也应在 main，
# 但整个 feature doc 的 spec/AC/设计内容可以在 worktree 改。
# 这里只拦截整文件级别的共享状态文件，字段级别靠 review 守护。
#
# 可被 --no-verify 绕过，所以需要 CI guard 兜底。

BRANCH=$(git branch --show-current 2>/dev/null)
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
STAGED_FILES="$(git diff --cached --name-only || true)"
VERDICT_BRANCH_DOMAIN=""
VERDICT_ID=""
VERDICT_ARTIFACT_ONLY=false
BIOME_PACKAGE_PATH="$REPO_ROOT/node_modules/@biomejs/biome/package.json"
BIOME_BINARY_PATH="$REPO_ROOT/node_modules/.bin/biome"
VERDICT_GENERATED_PATH_REGEX=""

run_biome_index_snapshot() {
  local snapshot_dir
  snapshot_dir="$(mktemp -d "${TMPDIR:-/tmp}/cat-cafe-precommit-index.XXXXXX")" || return 1
  (
    trap 'rm -rf "$snapshot_dir"' EXIT
    cd "$REPO_ROOT" &&
      git checkout-index --all --ignore-skip-worktree-bits --prefix="$snapshot_dir/" &&
      pnpm run check:biome-version -- --lockfile "$snapshot_dir/pnpm-lock.yaml" &&
      cd "$snapshot_dir" &&
      "$BIOME_BINARY_PATH" check . --diagnostic-level=error
  )
}

# B-3 (F234 2026-07-16): staged-scope variant — same index-snapshot semantics
# as run_biome_index_snapshot (#2860: scan INDEX content, not worktree, so
# unstaged WIP can't pollute the verdict), narrowed to the staged path list.
# Rationale: Biome is a per-file linter — file A's change cannot create a
# Biome error in file B, so full-repo scanning only lets UNRELATED repo debt
# block unrelated commits (same reason the verdict-artifact branch already
# got an exemption). Full-repo coverage stays owned by `pnpm check` / CI.
run_biome_index_snapshot_staged() {
  local snapshot_dir
  snapshot_dir="$(mktemp -d "${TMPDIR:-/tmp}/cat-cafe-precommit-index.XXXXXX")" || return 1
  (
    trap 'rm -rf "$snapshot_dir"' EXIT
    cd "$REPO_ROOT" &&
      git checkout-index --all --ignore-skip-worktree-bits --prefix="$snapshot_dir/" &&
      pnpm run check:biome-version -- --lockfile "$snapshot_dir/pnpm-lock.yaml" &&
      cd "$snapshot_dir" &&
      printf '%s\n' "$BIOME_LINTABLE_STAGED" \
        | xargs "$BIOME_BINARY_PATH" check --files-ignore-unknown=true --no-errors-on-unmatched --diagnostic-level=error
  )
}

# F192 Day-23: auto-published verdict artifact commits are git-backed evidence,
# not source changes. Running a full-repo Biome gate here lets unrelated repo
# debt block `cat_cafe_publish_verdict` even when the staged diff is strictly
# the current verdict's artifact set.
if [[ "$BRANCH" =~ ^verdict/auto/([^/]+)/(.+)$ ]]; then
  VERDICT_BRANCH_DOMAIN="${BASH_REMATCH[1]}"
  VERDICT_ID="${BASH_REMATCH[2]}"
  case "$VERDICT_BRANCH_DOMAIN" in
    eval-capability-wakeup) VERDICT_GENERATED_PATH_REGEX="|generated/capability-wakeup/${VERDICT_ID}/" ;;
    eval-memory) VERDICT_GENERATED_PATH_REGEX="|generated/memory/${VERDICT_ID}/" ;;
    eval-sop) VERDICT_GENERATED_PATH_REGEX="|generated/sop/${VERDICT_ID}/" ;;
  esac
fi
if [ -n "$VERDICT_ID" ] && [ -n "$STAGED_FILES" ]; then
  NON_VERDICT_ARTIFACT_PATHS="$(
    printf '%s\n' "$STAGED_FILES" \
      | grep -Ev "^(docs/harness-feedback/verdicts/${VERDICT_ID}\.md$|docs/harness-feedback/bundles/${VERDICT_ID}/${VERDICT_GENERATED_PATH_REGEX})" \
      || true
  )"
  if [ -z "$NON_VERDICT_ARTIFACT_PATHS" ]; then
    VERDICT_ARTIFACT_ONLY=true
  fi
fi

validate_staged_artifact_json() {
  local artifact_jsons
  artifact_jsons="$(printf '%s\n' "$STAGED_FILES" | grep -E '\.json$' || true)"
  while IFS= read -r relpath; do
    [ -z "$relpath" ] && continue
    if ! (
      cd "$REPO_ROOT" &&
      git show ":$relpath" \
        | node -e "JSON.parse(require('fs').readFileSync(0, 'utf8'))" >/dev/null
    ); then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Staged verdict artifact JSON is malformed: $relpath" >&2
      return 1
    fi
  done <<< "$artifact_jsons"
}

validate_staged_artifact_file_types() {
  local unexpected_artifacts
  unexpected_artifacts="$(
    while IFS= read -r relpath; do
      [ -z "$relpath" ] && continue
      if [ "$relpath" = "docs/harness-feedback/verdicts/${VERDICT_ID}.md" ] || [[ "$relpath" == *.json ]]; then
        continue
      fi
      printf '%s\n' "$relpath"
    done <<< "$STAGED_FILES"
  )"
  if [ -n "$unexpected_artifacts" ]; then
    echo "" >&2
    echo "🚫 BIOME GUARD failed! Verdict artifact-only commits may stage only the exact verdict .md plus .json evidence files." >&2
    printf '  - %s\n' "$unexpected_artifacts" >&2
    return 1
  fi
}

# ── Root Hygiene Guard (F214): 白名单制拦根目录新增文件（所有分支，含 main）──
# 规则来源：shared-rules.md §20。白名单制（不是黑名单）：根目录新增的文件，除非匹配
# 白名单，否则拒——这样能兜底拦下「未来未预料的垃圾类型」（spec Eval Contract Fixture 2）。
# debris / 有状态存储优先拒（即使扩展名看起来合法，如 cookies.json）。
# *.rdb* / *.sqlite* 与 clean-root-debris.sh / §20 同口径（覆盖 dump.rdb.backup-* 和
# dump.rdb-backup 等同族名）。放在 main bypass 之前，对每个分支都生效。
# ^[^/]+$ 只看根目录顶层新增文件，不碰子目录。
ROOT_ADDED=$(git diff --cached --name-only --diff-filter=A | grep -E '^[^/]+$' || true)
hygiene_debris=()
hygiene_unknown=()
while IFS= read -r f; do
  [ -z "$f" ] && continue
  # debris / 有状态存储优先拒（pattern 匹配，不依赖 .gitignore；continue 后不落白名单）
  case "$f" in
    *.log|*.tmp|forzadata-*.txt|cookies.json) hygiene_debris+=("$f (无状态残留)"); continue ;;
    *.rdb*|*.sqlite*) hygiene_debris+=("$f (有状态存储/圣域)"); continue ;;
  esac
  # secret/local 拒：被 .gitignore 标记却被 force-add (-f) 进来的（.env / .mcp.json /
  # *.local 等）。本 hook 是 force-added root 文件的唯一 backstop，绝不放行 .gitignore
  # 标记的 secret/local（云端 codex P1 round-2）。
  # --no-index: force-add (-f) 已把文件放进 index，普通 check-ignore 会因"已 tracked"
  # 而不报告；--no-index 检查 .gitignore 规则本身，正确识别 force-added 的 ignored 文件。
  if git check-ignore --no-index -q "$f" 2>/dev/null; then
    hygiene_debris+=("$f (.gitignore-marked secret/local — force-added)"); continue
  fi
  # 白名单 — 合法根文件。dotfile 用 enumerate known-safe（不用 .* catch-all，否则
  # 会放行任意 secret dotfile 如 .env）。
  case "$f" in
    *.md|*.mdx|*.json|*.jsonc|*.yaml|*.yml|*.cjs|*.mjs|*.ts|*.js|*.toml|*.lock|*.example|LICENSE*|CODEOWNERS|requirements.txt|Dockerfile*|Makefile|.gitignore|.gitattributes|.npmrc|.editorconfig|.nvmrc|.node-version|.prettierrc*|.eslintrc*|.dependency-cruiser.cjs|.dir-exceptions.json) ;;
    *) hygiene_unknown+=("$f") ;;
  esac
done <<< "$ROOT_ADDED"

if [ ${#hygiene_debris[@]} -gt 0 ] || [ ${#hygiene_unknown[@]} -gt 0 ]; then
  echo "" >&2
  echo "🧹 根目录卫生守护 (F214): 根目录新增文件未通过白名单！" >&2
  if [ ${#hygiene_debris[@]} -gt 0 ]; then
    echo "已知 debris / 有状态存储（绝不 commit 到根目录）：" >&2
    printf '  - %s\n' "${hygiene_debris[@]}" >&2
  fi
  if [ ${#hygiene_unknown[@]} -gt 0 ]; then
    echo "非白名单文件（未预料类型；如确为合法，请加进 hook 白名单）：" >&2
    printf '  - %s\n' "${hygiene_unknown[@]}" >&2
  fi
  echo "" >&2
  echo "处理：无状态产物移到 tmp/ 或删除；*.rdb/*.sqlite 是核心存储绝不 commit；合法新类型加白名单。" >&2
  echo "规则来源：shared-rules.md §20。绕过（确认合法时）：git commit --no-verify" >&2
  exit 1
fi

# ── Biome Guard: fail closed on stale local installs + full-index error scan ──
echo "" >&2
if [ "$VERDICT_ARTIFACT_ONLY" = "true" ]; then
  if ! validate_staged_artifact_file_types; then
    exit 1
  fi
  if [ ! -f "$BIOME_PACKAGE_PATH" ]; then
    echo "🧭 BIOME GUARD: no local Biome install in verdict artifact-only worktree; falling back to staged diff sanity check..." >&2
    if ! (
      cd "$REPO_ROOT" &&
      git diff --cached --check >&2
    ); then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Fix staged verdict artifact whitespace/conflict issues before publishing." >&2
      exit 1
    fi
    if ! validate_staged_artifact_json; then
      exit 1
    fi
  else
    echo "🧭 BIOME GUARD: verifying lockfile version + staged-only scan for verdict artifact-only auto branch..." >&2
    if ! (
      cd "$REPO_ROOT" &&
      pnpm run check:biome-version >&2 &&
      pnpm exec biome check --staged --files-ignore-unknown=true --no-errors-on-unmatched --diagnostic-level=error >&2
    ); then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Refresh the worktree install or fix the staged verdict artifacts before publishing." >&2
      exit 1
    fi
  fi
  if ! validate_staged_artifact_json; then
    exit 1
  fi
else
  # B-3 (F234 2026-07-16): scale verification to staged files / risk.
  # Tier a — no Biome-lintable file staged (docs/shell/yaml-only commits):
  #   skip Biome entirely (zero node_modules tax in fresh worktrees);
  #   whitespace/conflict sanity via git alone.
  # Tier b — lintable files staged + Biome installed:
  #   version check + index-snapshot scan narrowed to staged paths (#2860
  #   semantics preserved: content comes from INDEX, not worktree).
  # Tier c — lintable files staged + no local Biome: fail closed (a JS/TS
  #   change without its linter is exactly what this guard must block).
  # Extension set = everything Biome 2.4.1 can lint in this repo (css/html
  # verified live: "Checked 1 file"). Over-listing is harmless — the staged
  # scan runs with --files-ignore-unknown; under-listing is a real bypass
  # (Sol R1-P2: css canary escaped). Contract suite guards this list.
  # R2-P1 (Sol): biome config is GLOBAL-scope — changing it (e.g. a rule
  # warn→error) alters lint results for every file, so a per-file staged scan
  # of just the config proves nothing. Config staged ⇒ full-index risk tier.
  BIOME_CONFIG_STAGED="$(git diff --cached --name-only --diff-filter=d | grep -E '(^|/)biome\.jsonc?$' || true)"
  BIOME_LINTABLE_STAGED="$(git diff --cached --name-only --diff-filter=d | grep -E '\.(mjs|cjs|js|jsx|mts|cts|ts|tsx|json|jsonc|css|html|vue|svelte|astro|graphql|gql)$' || true)"
  # pnpm-lock.yaml staged = Biome version may drift via dependency change —
  # version check must run even when no lintable source file is staged.
  LOCKFILE_STAGED="$(git diff --cached --name-only | grep -E '^pnpm-lock\.yaml$' || true)"
  if [ -n "$BIOME_CONFIG_STAGED" ]; then
    if [ ! -f "$BIOME_PACKAGE_PATH" ]; then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! biome config staged but no local Biome install (config changes need a full-index scan)." >&2
      echo "   Run: env -u NODE_ENV pnpm install --frozen-lockfile" >&2
      exit 1
    fi
    echo "🧭 BIOME GUARD: biome config staged — global scope, running full Git index snapshot scan..." >&2
    if ! run_biome_index_snapshot >&2; then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Staged biome config change breaks existing files — fix them in the same commit or split the config change." >&2
      exit 1
    fi
  elif [ -z "$BIOME_LINTABLE_STAGED" ] && [ -n "$LOCKFILE_STAGED" ]; then
    echo "🧭 BIOME GUARD: pnpm-lock.yaml staged — verifying Biome version pin from INDEX content (no lintable source staged)..." >&2
    if ! (
      cd "$REPO_ROOT" &&
      STAGED_LOCKFILE="$(mktemp "${TMPDIR:-/tmp}/cat-cafe-staged-lockfile.XXXXXX")" &&
      trap 'rm -f "$STAGED_LOCKFILE"' EXIT &&
      git show :pnpm-lock.yaml > "$STAGED_LOCKFILE" &&
      pnpm run check:biome-version -- --lockfile "$STAGED_LOCKFILE" >&2 &&
      git diff --cached --check >&2
    ); then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Staged lockfile changes the Biome pin (or whitespace/conflict issues) — refresh install or fix before committing." >&2
      exit 1
    fi
  elif [ -z "$BIOME_LINTABLE_STAGED" ]; then
    echo "🧭 BIOME GUARD: no Biome-lintable files staged — skipping Biome, whitespace/conflict sanity only..." >&2
    if ! (
      cd "$REPO_ROOT" &&
      git diff --cached --check >&2
    ); then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Fix staged whitespace/conflict-marker issues before committing." >&2
      exit 1
    fi
  elif [ ! -f "$BIOME_PACKAGE_PATH" ]; then
    echo "" >&2
    echo "🚫 BIOME GUARD failed! Biome-lintable files staged but no local Biome install." >&2
    echo "   Run: env -u NODE_ENV pnpm install --frozen-lockfile" >&2
    exit 1
  else
    echo "🧭 BIOME GUARD: verifying lockfile version + staged-scope index snapshot scan..." >&2
    if ! run_biome_index_snapshot_staged >&2; then
      echo "" >&2
      echo "🚫 BIOME GUARD failed! Refresh the worktree install or fix the reported staged-file Biome errors." >&2
      exit 1
    fi
  fi
fi

# ── Brand Guard: detect brand-protected staged files ──
# F238 Phase C: dictionary-driven detection for brand-sensitive AND manual-port paths.
# Runs on ALL branches including main — brand contamination must be caught everywhere.
# The dictionary is the single source of truth — both classifications need guarding.
# Legacy patterns not yet in the dictionary are kept as fallback.
BRAND_PROTECTED_REGEX='^packages/web/src/app/layout\.tsx$|^packages/web/src/components/(SplitPaneView|ChatContainerHeader)\.tsx$|^packages/web/src/utils/api-client\.ts$|^packages/web/public/icons/'
if [ -f "$REPO_ROOT/scripts/brand-dictionary-helper.mjs" ]; then
  # Fail-closed: smoke-test ALL subcommand categories we depend on. If any
  # returns non-zero, empty, or garbage output, block the commit.
  # Category 1: --classify-path (classification pipeline)
  _SMOKE=$(node "$REPO_ROOT/scripts/brand-dictionary-helper.mjs" --classify-path "assets/system-prompts/x" 2>/dev/null) || _SMOKE=""
  _SMOKE_CLS=$(echo "$_SMOKE" | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));console.log(d.classification)}catch{console.log('BROKEN')}" 2>/dev/null) || _SMOKE_CLS="BROKEN"
  if [ "$_SMOKE_CLS" != "manual-port" ]; then
    echo "" >&2
    echo "🚫 BRAND GUARD: dictionary helper broken — fail-closed." >&2
    echo "   --classify-path smoke-test returned '$_SMOKE_CLS' (expected manual-port)." >&2
    exit 1
  fi
  # Category 2: --brand-sensitive-patterns / --manual-port-patterns (glob lists)
  # Cross-validation: convert each pattern to regex (same glob-to-regex below) and
  # verify at least one matches a known anchor path. This catches empty, garbage,
  # path-like garbage — any output that doesn't actually match real dictionary paths.
  # Anchor paths are hardcoded from the dictionary (defense-in-depth; helper unit tests
  # verify the dictionary-to-output pipeline).
  DICT_BS=$(node "$REPO_ROOT/scripts/brand-dictionary-helper.mjs" --brand-sensitive-patterns 2>/dev/null) || DICT_BS=""
  DICT_MP=$(node "$REPO_ROOT/scripts/brand-dictionary-helper.mjs" --manual-port-patterns 2>/dev/null) || DICT_MP=""
  # glob-to-regex helper (reused below for BRAND_PROTECTED_REGEX)
  _glob_to_re() { echo "$1" | sed 's/\./\\./g; s/\*\*/__GLOBSTAR__/g; s/\*/[^\/]*/g; s/__GLOBSTAR__/.*/g'; }
  # Cross-validate brand-sensitive patterns against THREE known anchors from different
  # glob families. A correct-subset that includes some but drops others fails.
  _BS_ANCHORS=("packages/web/public/manifest.json" "packages/web/public/icons/logo.png" "packages/web/public/concierge/skins/ragdoll-v1/pet.json")
  for _anchor in "${_BS_ANCHORS[@]}"; do
    _BS_HIT=""
    while IFS= read -r pat; do
      [ -z "$pat" ] && continue
      if echo "$_anchor" | grep -qE "^$(_glob_to_re "$pat")$"; then
        _BS_HIT="yes"; break
      fi
    done <<< "$DICT_BS"
    if [ -z "$_BS_HIT" ]; then
      echo "" >&2
      echo "🚫 BRAND GUARD: dictionary helper broken — fail-closed." >&2
      echo "   --brand-sensitive-patterns doesn't match anchor: $_anchor" >&2
      exit 1
    fi
  done
  # Cross-validate manual-port patterns against known anchor
  _MP_MATCH=""
  while IFS= read -r pat; do
    [ -z "$pat" ] && continue
    if echo "assets/system-prompts/test.md" | grep -qE "^$(_glob_to_re "$pat")$"; then
      _MP_MATCH="yes"; break
    fi
  done <<< "$DICT_MP"
  if [ -z "$_MP_MATCH" ]; then
    echo "" >&2
    echo "🚫 BRAND GUARD: dictionary helper broken — fail-closed." >&2
    echo "   --manual-port-patterns doesn't match known anchor (assets/system-prompts/test.md)." >&2
    exit 1
  fi
  DICT_PATTERNS=$(printf '%s\n%s' "$DICT_BS" "$DICT_MP" | sort -u)
  while IFS= read -r pat; do
    [ -z "$pat" ] && continue
    re=$(_glob_to_re "$pat")
    BRAND_PROTECTED_REGEX="${BRAND_PROTECTED_REGEX}|^${re}$"
  done <<< "$DICT_PATTERNS"
fi
BRAND_PROTECTED_STAGED=$(git diff --cached --name-only | grep -E "$BRAND_PROTECTED_REGEX" || true)
# Broad, cheap trigger only: the intake script remains the single authoritative
# aggregate classifier. False positives here merely run that classifier; they
# do not block. Keeping the trigger broad ensures a 1224-class staged change
# cannot avoid the automatic guard just because none of its files are branded.
STATEFUL_MIGRATION_CANDIDATE_STAGED=$(
  printf '%s\n' "$STAGED_FILES" \
    | grep -E '(^packages/api/src/.*/stores/|^packages/api/src/.*([Ss]tore|[Rr]edis|[Cc]ursor|freshness|read-state|unseen|briefing|closure|supplement).*\.ts$|^packages/shared/src/.*([Rr]edis|[Cc]ursor).*\.ts$|^packages/api/src/routes/.*\.ts$|^packages/api/scripts/.*([Cc]ursor|migrat|persist)|(^|/).*(lua|activation|feature-flag|migration|migrate|backfill|persist).*)' \
    || true
)

if [ -n "$BRAND_PROTECTED_STAGED" ] || [ -n "$STATEFUL_MIGRATION_CANDIDATE_STAGED" ]; then
  echo "" >&2
  if [ -n "$BRAND_PROTECTED_STAGED" ]; then
    echo "🛡 INBOUND GUARD: brand-protected files staged, running brand + 1224-class validation..." >&2
    echo "$BRAND_PROTECTED_STAGED" | sed 's/^/  - /' >&2
  else
    echo "🛡 INBOUND GUARD: checking staged files for 1224-class state migration risk..." >&2
    echo "$STATEFUL_MIGRATION_CANDIDATE_STAGED" | sed 's/^/  - /' >&2
  fi
  echo "" >&2
  if [ -f "scripts/intake-from-opensource.sh" ]; then
    if ! bash scripts/intake-from-opensource.sh --validate-inbound --from-index --state-migration-advisory >&2; then
      echo "" >&2
      echo "🚫 Inbound Guard failed! Fix brand contamination before committing." >&2
      echo "   See: bash scripts/intake-from-opensource.sh --validate-inbound" >&2
      exit 1
    fi
  else
    echo "🚫 Inbound Guard unavailable: scripts/intake-from-opensource.sh is missing." >&2
    echo "   Refusing to commit without the automatic brand + 1224-class migration guard." >&2
    exit 1
  fi
fi

# main 分支：Inbound Guard 已在上面对所有分支生效；仅跳过 Shared State Guard
# （共享状态文件 SHOULD be committed on main）
if [ "$BRANCH" = "main" ]; then
  exit 0
fi

# 获取暂存区中的共享状态文件，并只拦截相对 origin/main 有差异的文件。
SHARED_STATE_FILES=$(git diff --cached --name-only | grep -E '^(docs/BACKLOG\.md|cat-config\.json)$')
DIVERGENT_SHARED_STATE_FILES=""
SHARED_STATE_BASE_ERROR=""

if [ -n "$SHARED_STATE_FILES" ]; then
  if ! git rev-parse --verify --quiet origin/main >/dev/null; then
    DIVERGENT_SHARED_STATE_FILES="$SHARED_STATE_FILES"
    SHARED_STATE_BASE_ERROR="无法解析 origin/main，不能证明暂存内容只是上游 carry-in；按 fail-closed 拦截。"
  else
    while IFS= read -r shared_file; do
      [ -z "$shared_file" ] && continue
      if ! git diff --cached --quiet origin/main -- "$shared_file"; then
        if [ -n "$DIVERGENT_SHARED_STATE_FILES" ]; then
          DIVERGENT_SHARED_STATE_FILES=$(printf '%s\n%s' "$DIVERGENT_SHARED_STATE_FILES" "$shared_file")
        else
          DIVERGENT_SHARED_STATE_FILES="$shared_file"
        fi
      fi
    done <<< "$SHARED_STATE_FILES"
  fi
fi

if [ -n "$DIVERGENT_SHARED_STATE_FILES" ]; then
  echo "" >&2
  echo "🚫 SHARED-STATE GUARD: 共享状态文件不能在非 main 分支创作！" >&2
  if [ -n "$SHARED_STATE_BASE_ERROR" ]; then
    echo "$SHARED_STATE_BASE_ERROR" >&2
  fi
  echo "" >&2
  echo "被拦截的文件：" >&2
  echo "$DIVERGENT_SHARED_STATE_FILES" | sed 's/^/  - /' >&2
  echo "" >&2
  echo "正确做法：" >&2
  echo "$DIVERGENT_SHARED_STATE_FILES" | sed 's#^#  1. git restore --staged -- #' >&2
  echo "  2. 切到 main worktree（如已有：cd <main-worktree-path>）" >&2
  echo "     或：git stash && git checkout main（如 main 未被其他 worktree 占用）" >&2
  echo "  3. 在 main 上：git pull && 编辑 → git add → commit → push" >&2
  echo "  4. 切回 feature 分支继续工作" >&2
  echo "" >&2
  echo "规则来源：shared-rules.md §14「共享状态文件只在 main 改」" >&2
  echo "绕过方式：--no-verify（但 CI 会再拦一次）" >&2
  exit 1
fi

exit 0
