diff --git a/hooks/lib/mtime.sh b/hooks/lib/mtime.sh new file mode 100644 index 0000000..530e283 --- /dev/null +++ b/hooks/lib/mtime.sh @@ -0,0 +1,41 @@ +# hooks/lib/mtime.sh — 「這個檔幾點被動的」,跨 macOS/Linux 都問得出來。 +# 不是獨立掛的閘(沒進 hooks.json),給那幾支用 /tmp 戳記的閘 `source` 用。 +# +# ══ 為什麼要有這支(inkstone/ISEP#90 ④,2026-08-28 實查)═══════════════ +# +# 三支閘(prod-write-guard、main-and-prod-push-guard、stage-before-prod-guard) +# 的戳記檢查都寫成這一行: +# +# MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) +# +# 在 macOS(BSD stat)上它是對的:`-f %m` 就是 mtime。 +# 在 **Linux(GNU coreutils)上 `-f` 是「顯示檔案系統資訊」**,而且它 +# **一邊回非零、一邊往 stdout 吐一整段檔案系統的區塊**: +# +# $ stat -f %m /tmp/.probe +# stat: cannot read file system information for '%m': No such file or directory ← stderr +# File: "/tmp/.probe" ← stdout +# ID: 0 Namelen: 255 Type: ext2/ext3 +# … +# +# ⇒ `2>/dev/null` 把錯誤訊息吃掉、`||` 接著跑 `stat -c %Y` 把正確的秒數**接在那堆垃圾後面** +# ⇒ `MT` 變成「一段多行文字+一個數字」 +# ⇒ 閘下一行的 `case "$NOW$MT" in *[!0-9]*) return 1` 一定命中 +# ⇒ **在 Linux 上,那三支閘的戳記永遠不會被接受。** +# +# 🔴 這件事的後果不是「少一個便利功能」: +# 那三支閘都是「擋下來,但**總管看過就可以放行**」的設計。 +# 放行的那道門在雲端(Linux)打不開 ⇒ **它們在雲端等於純擋**, +# 總管照著閘自己印的指示做,做幾次都打不開,而閘不會告訴他門是壞的。 +# inkstone/InkStoneCo#99 記的「連續四次蓋不出戳記」就是這個形狀。 +# +# 🔴 順序反過來就好,但不能只靠順序:改成先 `-c %Y`(GNU)再 `-f %m`(BSD), +# 而且**每一步都驗它是不是純數字**——因為這個 bug 的成因正是 +# 「命令失敗了卻還是印了東西出來」,只看離開碼會再被騙一次。 +file_mtime() { + _fm=$(stat -c %Y "$1" 2>/dev/null || true) + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(python3 -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac + printf '%s' "$_fm" +} diff --git a/hooks/main-and-prod-push-guard.sh b/hooks/main-and-prod-push-guard.sh index 87f9d3e..6c55d52 100755 --- a/hooks/main-and-prod-push-guard.sh +++ b/hooks/main-and-prod-push-guard.sh @@ -74,6 +74,18 @@ esac # ⇒ 沒有可靠的身分辨識時,改成**正向確認**:推 main 前要有一枚新鮮的戳記。 # subagent 當然造得出那枚戳記——但它得刻意繞過一段明講「不要這樣做」的訊息。 # **閘擋的是無心,不是惡意。** 而現在這個版本連無心都擋不了(它誰都擋)。 +# 🔴 戳記的 mtime 一律走 lib/mtime.sh:舊寫法(`stat -f %m || stat -c %Y`)在 Linux 上 +# **永遠算不出數字**(GNU 的 `-f` 是「檔案系統資訊」,它一邊回非零一邊吐一整段文字, +# 於是後面接上的秒數被那段垃圾污染)⇒ 這支閘給總管的逃生口在雲端整個焊死, +# 而閘不會告訴他門是壞的(inkstone/ISEP#90 ④,全文見 hooks/lib/mtime.sh)。 +# lib 不在時就地補一份同順序的定義——這道門不能因為少一個檔案就再關上一次。 +. "$(dirname "$0")/lib/mtime.sh" 2>/dev/null || file_mtime() { + _fm=$(stat -c %Y "$1" 2>/dev/null || true) + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac + printf '%s' "$_fm" +} + STAMP="/tmp/.main-push-ok" # 🔴 2026-08-11 這道閘被自己的戳記穿透了,修法寫在這裡: @@ -87,7 +99,7 @@ STAMP="/tmp/.main-push-ok" stamp_ok() { [ -f "$STAMP" ] || return 1 NOW=$(date +%s 2>/dev/null || echo 0) - MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) + MT=$(file_mtime "$STAMP") case "$NOW$MT" in *[!0-9]*) return 1 ;; esac [ "$NOW" -gt 0 ] && [ "$MT" -gt 0 ] || return 1 [ $((NOW - MT)) -lt 900 ] || return 1 diff --git a/hooks/prod-write-guard.sh b/hooks/prod-write-guard.sh index 437d424..4c96fef 100755 --- a/hooks/prod-write-guard.sh +++ b/hooks/prod-write-guard.sh @@ -48,11 +48,23 @@ except Exception: print("") # **15 分鐘內一條並行的 subagent 把 commit 推上了另一個 repo 的 main**。 # ⇒ **多條 subagent 並行時,「時間窗」本身就是漏洞。** 所以改成單次用完即丟。 # ⇒ 而且只在**真的要擋的那一刻**才檢查——放在檔頭會被任何一條無關指令把戳記燒掉。 +# 🔴 戳記的 mtime 一律走 lib/mtime.sh:舊寫法(`stat -f %m || stat -c %Y`)在 Linux 上 +# **永遠算不出數字**(GNU 的 `-f` 是「檔案系統資訊」,它一邊回非零一邊吐一整段文字, +# 於是後面接上的秒數被那段垃圾污染)⇒ 這支閘給總管的逃生口在雲端整個焊死, +# 而閘不會告訴他門是壞的(inkstone/ISEP#90 ④,全文見 hooks/lib/mtime.sh)。 +# lib 不在時就地補一份同順序的定義——這道門不能因為少一個檔案就再關上一次。 +. "$(dirname "$0")/lib/mtime.sh" 2>/dev/null || file_mtime() { + _fm=$(stat -c %Y "$1" 2>/dev/null || true) + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac + printf '%s' "$_fm" +} + STAMP="/tmp/.prod-write-ok" stamp_ok() { [ -f "$STAMP" ] || return 1 NOW=$(date +%s 2>/dev/null || echo 0) - MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) + MT=$(file_mtime "$STAMP") case "$NOW$MT" in *[!0-9]*) return 1 ;; esac [ "$NOW" -gt 0 ] && [ "$MT" -gt 0 ] || return 1 [ $((NOW - MT)) -lt 900 ] || return 1 diff --git a/hooks/stage-before-prod-guard.sh b/hooks/stage-before-prod-guard.sh index 8eecbab..1a007db 100755 --- a/hooks/stage-before-prod-guard.sh +++ b/hooks/stage-before-prod-guard.sh @@ -159,6 +159,18 @@ fi # ⇒ 改成兩者都認:內容有數字就用內容,沒有就用 mtime(`touch` 因此真的有效)。 # # 另外認 leo 親手蓋的章(scripts/stage-ok.sh 產生),那是比我自評更強的訊號。 +# 🔴 戳記的 mtime 一律走 lib/mtime.sh:舊寫法(`stat -f %m || stat -c %Y`)在 Linux 上 +# **永遠算不出數字**(GNU 的 `-f` 是「檔案系統資訊」,它一邊回非零一邊吐一整段文字, +# 於是後面接上的秒數被那段垃圾污染)⇒ 這支閘給總管的逃生口在雲端整個焊死, +# 而閘不會告訴他門是壞的(inkstone/ISEP#90 ④,全文見 hooks/lib/mtime.sh)。 +# lib 不在時就地補一份同順序的定義——這道門不能因為少一個檔案就再關上一次。 +. "$(dirname "$0")/lib/mtime.sh" 2>/dev/null || file_mtime() { + _fm=$(stat -c %Y "$1" 2>/dev/null || true) + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac + printf '%s' "$_fm" +} + for STAMP in /tmp/.stage-ok-by-leo /tmp/.stage-verified; do [ -f "$STAMP" ] || continue now=$(date +%s) @@ -172,7 +184,7 @@ for STAMP in /tmp/.stage-ok-by-leo /tmp/.stage-verified; do # 抽數字會得到天文數字 ⇒ `now - t` 是**負數** ⇒ `< 21600` 成立 ⇒ **永遠放行**。 # ⇒ 只有「不晚於現在」的時間戳才算數;不合理就退回 mtime,別當成通過。 if [ "$t" -eq 0 ] || [ "$t" -gt "$now" ]; then - t=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) + t=$(file_mtime "$STAMP") fi [ "$t" -gt 0 ] && [ "$((now - t))" -lt 21600 ] && exit 0 # 6 小時內驗過 stage → 放行 done diff --git a/hooks/tests/gate-ok.test.sh b/hooks/tests/gate-ok.test.sh new file mode 100755 index 0000000..0c5de96 --- /dev/null +++ b/hooks/tests/gate-ok.test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# scripts/gate-ok +「戳記真的打得開門」的迴歸測試(inkstone/ISEP#90 ④) +# +# ══ 這支守的是什麼 ═════════════════════════════════════════════════════ +# 三支閘(prod-write/main-and-prod-push/stage-before-prod)都是 +# 「擋下來,但**總管看過就可以放行**」的設計。既有的測試只驗了「擋得住」, +# **一條都沒有驗過「放得開」**——於是 2026-08-28 才發現: +# `stat -f %m` 在 GNU coreutils 上是「檔案系統資訊」,它一邊回非零一邊吐一整段文字, +# 把接在後面的秒數污染掉 ⇒ **在 Linux(=每一個雲端 session)上,那三支閘的戳記 +# 永遠不會被接受** ⇒ 它們在雲端等於純擋,而閘不會告訴你門是壞的。 +# +# 所以這支的第一優先是「門打得開」,第二優先是「門沒有因此變寬」。 +# +# 🔴 這支會真的寫 /tmp 的戳記檔(那是閘寫死的路徑,沒有覆寫的開關)。 +# 每一條測完就把它刪掉;跑之前若有真的在等的戳記,會被這支洗掉—— +# 請不要在「已經蓋好戳記正要推東西」的當下跑它。 +set -u +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # hooks/ +REPO="$(cd "$ROOT/.." && pwd)" +GATEOK="$REPO/scripts/gate-ok" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"; rm -f /tmp/.prod-write-ok /tmp/.main-push-ok /tmp/.stage-verified /tmp/.solo-ok-TESTSID' EXIT +export GIT_CONFIG_GLOBAL="$TMP/gitconfig"; : > "$GIT_CONFIG_GLOBAL" +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t + +PASS=0; FAIL=0 +ok(){ if [ "$2" = "$3" ]; then printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); + else printf ' ❌ %s —— 期望 %s,實得 %s\n' "$1" "$3" "$2"; FAIL=$((FAIL+1)); fi; } + +mkrepo(){ d="$TMP/$1"; git init -q -b main "$d"; ( cd "$d" && echo a>a && git add a && git commit -qm x ) >/dev/null 2>&1; printf '%s\n' "$d"; } +fire_prod(){ printf '{"tool_name":"Bash","tool_input":{"command":"acr push x.yaml"}}' \ + | bash "$ROOT/prod-write-guard.sh" >/dev/null 2>&1; echo $?; } +fire_push(){ printf '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' \ + | ( cd "$1" && bash "$ROOT/main-and-prod-push-guard.sh" >/dev/null 2>&1; echo $? ); } + +rm -f /tmp/.prod-write-ok /tmp/.main-push-ok /tmp/.stage-verified + +echo "── 最重要:門打得開(Linux 上 stat -f %m 那個 bug 的迴歸)──────" +ok "① 沒戳記 → prod-write 擋" "$(fire_prod)" 2 +bash "$GATEOK" prod-write >/dev/null +ok "② gate-ok prod-write 之後 → 放行" "$(fire_prod)" 0 +ok "③ 單次用完即丟:同一枚不能放行第二次" "$(fire_prod)" 2 + +R1=$(mkrepo r1); R2=$(mkrepo r2) +ok "④ 沒戳記 → 推 main 擋" "$(fire_push "$R1")" 2 +bash "$GATEOK" main-push "$R1" >/dev/null +ok "⑤ gate-ok main-push 之後 → 放行" "$(fire_push "$R1")" 0 + +echo "── 門沒有因此變寬 ────────────────────────────────────────────" +bash "$GATEOK" main-push "$R1" >/dev/null +ok "⑥ 替 r1 開的門,r2 走不過(綁 repo 沒鬆)" "$(fire_push "$R2")" 2 +rm -f /tmp/.main-push-ok +# 過期的戳記不算數:把 mtime 調到 16 分鐘前 +bash "$GATEOK" prod-write >/dev/null +touch -d '16 minutes ago' /tmp/.prod-write-ok 2>/dev/null || touch -A -001600 /tmp/.prod-write-ok 2>/dev/null +ok "⑦ 16 分鐘前的戳記 → 過期,照樣擋" "$(fire_prod)" 2 +rm -f /tmp/.prod-write-ok +# 空內容的 main-push 戳記=萬用鑰匙,08-12 那次穿透的形狀,不准復活 +: > /tmp/.main-push-ok +ok "⑧ 空內容的 main-push 戳記 → 不算數" "$(fire_push "$R1")" 2 +rm -f /tmp/.main-push-ok + +echo "── gate-ok 自己:不認得的就要拒絕,別蓋一枚打不開的戳記 ──────" +bash "$GATEOK" nonsense >/dev/null 2>&1; ok "⑨ 不認得的閘名 → 離開碼 2" "$?" 2 +bash "$GATEOK" solo >/dev/null 2>&1; ok "⑩ solo 沒帶 session id → 離開碼 2" "$?" 2 +bash "$GATEOK" main-push "$TMP" >/dev/null 2>&1; ok "⑪ 不是 git repo → 離開碼 2" "$?" 2 +ok "⑫ 而且**沒有**留下一枚註定打不開的空戳記" "$([ -f /tmp/.main-push-ok ] && echo yes || echo no)" no +bash "$GATEOK" solo TESTSID >/dev/null 2>&1 +ok "⑬ solo → 蓋出 /tmp/.solo-ok-" "$([ -f /tmp/.solo-ok-TESTSID ] && echo yes || echo no)" yes +bash "$GATEOK" stage-verified >/dev/null 2>&1 +ok "⑭ stage-verified → 蓋出 /tmp/.stage-verified" "$([ -f /tmp/.stage-verified ] && echo yes || echo no)" yes + +echo "── file_mtime 本身:兩個平台的寫法都要答得出純數字 ────────────" +. "$ROOT/lib/mtime.sh" +touch "$TMP/probe" +M=$(file_mtime "$TMP/probe") +case "$M" in ''|*[!0-9]*) R=bad ;; *) R=ok ;; esac +ok "⑮ file_mtime 回純數字(舊寫法在 Linux 上會回一整段文字)" "$R" ok +ok "⑯ 檔案不存在 → 回 0,不是空字串" "$(file_mtime "$TMP/nope")" 0 + +echo +printf '通過 %s 條,失敗 %s 條\n' "$PASS" "$FAIL" +[ "$FAIL" = 0 ] || exit 1 diff --git a/hooks/unpushed-police.sh b/hooks/unpushed-police.sh index a5b1e2b..e3d7763 100755 --- a/hooks/unpushed-police.sh +++ b/hooks/unpushed-police.sh @@ -57,6 +57,18 @@ except Exception: print(False) # **2 一律不報。** 誤攔比漏擋更該修(leo 2026-08-17:文字層那些閘 8 次誤攔、 # 0 次正確攔截)——把「離線」當成「你沒推」,就是拿雜訊懲罰謹慎。 +# 🔴 戳記的 mtime 一律走 lib/mtime.sh:舊寫法(`stat -f %m || stat -c %Y`)在 Linux 上 +# **永遠算不出數字**(GNU 的 `-f` 是「檔案系統資訊」,它一邊回非零一邊吐一整段文字, +# 於是後面接上的秒數被那段垃圾污染)⇒ 這支閘給總管的逃生口在雲端整個焊死, +# 而閘不會告訴他門是壞的(inkstone/ISEP#90 ④,全文見 hooks/lib/mtime.sh)。 +# lib 不在時就地補一份同順序的定義——這道門不能因為少一個檔案就再關上一次。 +. "$(dirname "$0")/lib/mtime.sh" 2>/dev/null || file_mtime() { + _fm=$(stat -c %Y "$1" 2>/dev/null || true) + case "${_fm:-}" in ''|*[!0-9]*) _fm=$(stat -f %m "$1" 2>/dev/null || true) ;; esac + case "${_fm:-}" in ''|*[!0-9]*) _fm=0 ;; esac + printf '%s' "$_fm" +} + GIT_ASK="env GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/echo GIT_SSH_COMMAND=ssh\ -oBatchMode=yes" _TO="" command -v timeout >/dev/null 2>&1 && _TO="timeout 15" @@ -72,7 +84,7 @@ ls_remote_heads() { _lr_cache="${UNPUSHED_CACHE_DIR:-/tmp}/.unpushed-lsremote-$_lr_key" if [ -f "$_lr_cache" ]; then _lr_now=$(date +%s 2>/dev/null || echo 0) - _lr_mt=$(stat -f %m "$_lr_cache" 2>/dev/null || stat -c %Y "$_lr_cache" 2>/dev/null || echo 0) + _lr_mt=$(file_mtime "$_lr_cache") case "$_lr_now$_lr_mt" in *[!0-9]*) _lr_mt=0 ;; esac if [ "$_lr_mt" -gt 0 ] && [ $((_lr_now - _lr_mt)) -lt 120 ]; then [ -s "$_lr_cache" ] || return 1 diff --git a/scripts/gate-ok b/scripts/gate-ok new file mode 100755 index 0000000..098ed1a --- /dev/null +++ b/scripts/gate-ok @@ -0,0 +1,113 @@ +#!/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) + 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) + 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" "這題確認是人閘,放行一次" ;; + esac ;; + + *) + printf '❌ 不認得的閘名:%s\n\n' "$GATE" >&2 + usage >&2 + exit 2 ;; +esac