#!/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- # … # 兩個後果: # ① **記不住**。門的形狀藏在被擋下的那則訊息裡,每支都不同, # 而且有兩支還帶參數(綁 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 這件事真的該自己做(subagent-first-guard)——回覆裡要寫理由 dispatch 派工單真的有例外(dispatch-format-guard) ask 這題真的是人閘(ask-user-question-guard) roster 派工真的是名單的例外(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 \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