#!/usr/bin/env bash
# gate-ok — 各道閘「總管看過了，放行下一個動作」的**單一入口**（inkstone/ISEP#90 ④）
#
# ══ 為什麼要有這支 ═════════════════════════════════════════════════════
#
# 這些閘每一支都留了一道給總管的門，而每一道門長得都不一樣：
#     touch /tmp/.prod-write-ok
#     git rev-parse --show-toplevel > /tmp/.main-push-ok
#     touch /tmp/.stage-verified
#     touch /tmp/.solo-ok-<session_id>
#     …
# 兩個後果：
#   ① **記不住**。門的形狀藏在被擋下的那則訊息裡，每支都不同，
#      而且有兩支還帶參數（綁 repo、綁 session）——照著抄錯一個字，門就打不開。
#   ② **放行不了**。逃生口是「一條臨時湊出來的 Bash 指令」，
#      而臨時湊出來的指令**沒有穩定的形狀可以事先放行**：
#      `.claude/settings.json` 的 allow 只能逐條寫死
#      （現場真的就寫著 `Bash(touch /tmp/.prod-write-ok)` 這種完全比對的一行），
#      多一個 `&&`、換一個檔名、加一個 session id，就落在那條規則之外。
#      2026-08-28（inkstone/InkStoneCo#99）總管在雲端連續四次沒能把
#      prod-write 的戳記蓋出去 ⇒ **閘留了一道給總管的門，而那道門打不開**
#      ⇒ 任何「擋下來、但總管可以放行」的閘，在那個環境裡等於「純擋」。
#
# ⇒ 這支把所有逃生口收斂成**一個名字、一種形狀**：
#       bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" <閘名> [參數]
#   一條前綴規則就涵蓋全部，而且以後新增閘不必再動一次設定。
#
# 🔴 這支**不會弱化任何一道閘**：它蓋的戳記跟閘原本認的是同一個檔、同一種語意
#   （單次用完即丟、綁 repo、綁 session、有效期都不變）。它換掉的只有「怎麼蓋」。
# 🔴 也**不是給 subagent 用的**：閘擋的是無心不是惡意（見 main-and-prod-push-guard
#   第 76 行）。subagent 照樣造得出戳記，但它得刻意繞過一段明講「不要這樣做」的訊息。
set -eu

usage() {
  cat <<'USAGE'
用法：gate-ok <閘名> [參數]

  prod-write                改到線上實例（prod-write-guard）——放行下一個動作，用完即消失
  main-push [repo 路徑]     推 gitea main（main-and-prod-push-guard）——綁 repo，預設是 cwd
  stage-verified            stage 真的驗過了（stage-before-prod-guard）——記得在回覆貼實測輸出
  solo <session_id>         這件事真的該自己做（subagent-first-guard）——回覆裡要寫理由
  dispatch <session_id>     派工單真的有例外（dispatch-format-guard）
  ask <session_id>          這題真的是人閘（ask-user-question-guard）
  roster <session_id>       派工真的是名單的例外（roster-guard）
  kbdb-down                 KBDB 真的連不上（history-first-guard）——回覆裡要說明

  --list                    列出目前存在的戳記

🔴 每一枚戳記都是**留痕**：它會出現在指令歷史上，而且多數是單次用完即丟。
   蓋之前先確定你真的看過那個動作會改到什麼；蓋完在回覆裡寫一句為什麼。
USAGE
}

stamp() {  # stamp <檔> <說明> [內容]
  if [ $# -ge 3 ]; then printf '%s\n' "$3" > "$1"; else : >> "$1"; touch "$1"; fi
  printf '✅ 已蓋戳記：%s\n   %s\n' "$1" "$2"
}

[ $# -ge 1 ] || { usage; exit 2; }

case "$1" in
  -h|--help|help) usage; exit 0 ;;
  --list)
    printf '目前存在的戳記：\n'
    ls -la /tmp/.prod-write-ok /tmp/.main-push-ok /tmp/.stage-verified \
           /tmp/.kbdb-down /tmp/.solo-ok-* /tmp/.dispatch-ok-* /tmp/.ask-ok-* 2>/dev/null \
      || printf '（一枚都沒有）\n'
    exit 0 ;;
esac

GATE="$1"; shift || true

case "$GATE" in
  prod-write)
    stamp /tmp/.prod-write-ok "只放行**下一個**會改到線上那台的動作，用完即消失（15 分鐘失效）" ;;

  main-push)
    # 綁 repo：戳記內容要是那個 repo 的 toplevel，閘會拿它跟「push 的目標 repo」比對。
    # 🔴 解不出 repo 時**寧可失敗也不蓋空的**——閘明文寫著空內容不算數
    #    （08-12 那次穿透就是空檔案變成萬用鑰匙），蓋一個註定打不開的戳記
    #    只會讓人以為門開了。
    D="${1:-$(pwd)}"
    TOP="$(git -C "$D" rev-parse --show-toplevel 2>/dev/null || true)"
    if [ -z "$TOP" ]; then
      printf '❌ %s 不在任何 git repo 裡，解不出要放行哪個 repo。\n' "$D" >&2
      printf '   用法：gate-ok main-push <那個 repo 的路徑>\n' >&2
      exit 2
    fi
    stamp /tmp/.main-push-ok "只放行推 $TOP 的 main，單次、15 分鐘失效" "$TOP" ;;

  stage-verified)
    stamp /tmp/.stage-verified "stage 驗過了（6 小時）——**記得在回覆裡貼 stage 的實測輸出**" ;;

  kbdb-down)
    stamp /tmp/.kbdb-down "KBDB 連不上而放行——**回覆裡要說明**，這是留痕不是豁免" ;;

  solo|dispatch|ask|roster)
    SID="${1:-${CLAUDE_SESSION_ID:-}}"
    if [ -z "$SID" ]; then
      printf '❌ %s 要綁 session id（閘用它認「是不是同一個 session」）。\n' "$GATE" >&2
      printf '   被擋下的那則訊息裡就有；或 gate-ok %s <session_id>\n' "$GATE" >&2
      exit 2
    fi
    case "$GATE" in
      solo)     stamp "/tmp/.solo-ok-$SID"     "這件事自己做——**回覆裡要寫理由**" ;;
      dispatch) stamp "/tmp/.dispatch-ok-$SID" "這張派工單的例外，放行一次" ;;
      ask)      stamp "/tmp/.ask-ok-$SID"      "這題確認是人閘，放行一次" ;;
      roster)   stamp "/tmp/.roster-ok-$SID"   "這次派工確認是名單的例外，放行一次" ;;
    esac ;;

  *)
    printf '❌ 不認得的閘名：%s\n\n' "$GATE" >&2
    usage >&2
    exit 2 ;;
esac
