Files
ISEP/hooks/tests/ask-user-question-guard.live.test.sh
T
claude-code e0557334bc 閘掛在「收工後」,而 leo 是在「被問到」的當下被打擾——補上那一刻的攔截
leo 2026-08-26:「今天已經好幾次問我,為什麼 hooks 沒有攔下來?」

實查:總管問 leo 走的動作是 AskUserQuestion,而 hooks.json 裡它出現 0 次,
沒有任何 matcher。判準其實早就寫好了——self-drive-police / self-drive-judge
用的就是同一套四題公式——但那兩支只掛在 Stop 與 SubagentStop,
是回合結束後才跑的。問題早就送到他眼前了,事後再反問「你查過了嗎」,
成本已經轉嫁出去。判準對了,時機錯了。

新增 hooks/ask-user-question-guard.sh(PreToolUse / AskUserQuestion):
- 觸發條件是那個動作本身,不是任何句型或關鍵字——全檔零個判擋用的正則,
  換句話說閃不過去,講得謹慎也不會被多罰(leo 2026-08-17 文字層封路的檢驗)
- 進來之後用四題公式的 haiku 判官分「該問 / 不該問」,命中任一題一律放行
- 同一個問題只擋一次(雜湊戳記):判官誤判時重送即過,
  leo 該收到的問題不會因為一支閘而永遠送不到
- 判官掛掉/沒網路/claude 不在 PATH 一律 fail-open,壞掉等於它不存在

版本 0.3.9 → 0.4.0。這不是儀式,是傳輸機制本身:產物按版本號分資料夾
(~/.claude/plugins/cache/inkstone/isep/<版本>/),版本沒動就不會長出新資料夾,
這支閘一個 session 都載入不到。docs/TESTING.md 開頭那段講的就是這件事,
而第一版我漏了——總管複驗時量出來的。

假設(沒有前例可循,先裁再記):跳 0.4.0 而不是 0.3.10。理由是這一版第一次
掛上 AskUserQuestion 這個事件面,是新能力不是修補;而且 0.3.10 在
plugin 快取目錄的 ls 裡會排到 0.3.1 旁邊,肉眼不好認。錯了打回,改號很便宜。

順手修掉自己寫出來的一個坑:訊息原本用沒加引號的 heredoc,
反引號被當命令執行,wiki 路徑與豁免指令兩行變成空白(閘照擋,只看離開碼看不出來)。
已收成 mistakes.md 一條,並由 ⑩b 這條測試守著。

實測:
- hooks/tests/ask-user-question-guard.test.sh      14/14(離線,不花錢)
- hooks/tests/ask-user-question-guard.live.test.sh 9/9 連跑三次(真的叫 haiku)
  A 群 5 條真人閘(花錢/不可逆/跨專案結構/品味方向/物理人閘)全部放行,誤攔 0
  B 群 4 條純技術路徑選擇全部擋下
- 既有 7 支測試與改動前逐條對照,結果完全相同(沒有被我弄壞)

順手對帳:plugin.json 與 marketplace.json 的描述寫「43 支機械閘、53 條註冊」,
實際數過是 46/56(含本次新增這支)。docs/hooks-inventory.md 一併更正。

【工單】inkstone/InkStoneCo#55
2026-08-26 22:36:08 +08:00

93 lines
4.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ask-user-question-guard.sh 的**判官準度**測試(inkstone/InkStoneCo#55)。
#
# 🔴 這支**真的會叫 haiku**(每題一次呼叫,共 9 題)。離線那支測管路,這支測判斷。
# 分開兩檔的理由:管路壞掉要立刻知道且不該花錢;判官準度是另一種失敗,
# 而且它會隨模型版本漂移,需要單獨、可重跑的量尺。
#
# 兩個方向都要有實例,缺一邊等於沒測:
# B 群「該擋」 = 純技術路徑選擇(leo 2026-08-26:「這種問題不要問我」)
# A 群「不該擋」= 真的四題人閘(花錢/不可逆/跨專案結構/品味方向/物理人閘)
#
# 🔴 **A 群失敗比 B 群失敗嚴重。** 誤擋一個真人閘 = 讓總管替 leo 決定他的品味,
# 比漏擋一個技術題更糟。所以本檔把 A 群失敗單獨列出來報。
#
# 用法:hooks/tests/ask-user-question-guard.live.test.sh [hook 路徑]
set -u
HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/ask-user-question-guard.sh}"
PASS=0; FAIL=0; N=0; AFAIL=0
payload() {
python3 - "$@" <<'PY'
import json, sys
q, opts = sys.argv[1], sys.argv[2:]
print(json.dumps({
"session_id": "S-LIVE",
"hook_event_name": "PreToolUse",
"tool_name": "AskUserQuestion",
"tool_input": {"questions": [{
"question": q,
"header": "決策",
"multiSelect": False,
"options": [{"label": o, "description": ""} for o in opts],
}]},
}, ensure_ascii=False))
PY
}
# t <期望 exit> <群> <說明> <問題> [選項...]
t() {
want="$1"; grp="$2"; desc="$3"; shift 3
N=$((N+1))
rm -f /tmp/.ask-gate-warned-* 2>/dev/null || true # 每題都要真的走到判官
out=$(payload "$@" | sh "$HOOK" 2>&1); rc=$?
if [ "$rc" -eq "$want" ]; then
printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
else
printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc"
printf '%s\n' "$out" | grep -m1 '判官的話' | sed 's/^/ /'
FAIL=$((FAIL+1))
[ "$grp" = "A" ] && AFAIL=$((AFAIL+1))
fi
}
echo "── A 群:不該擋(真人閘,leo 要被問到)───────────────────────"
t 0 A "①④ 品味/方向:總圖那棵樹要不要預設收合(leo 08-26 當場裁的就是這種)" \
'總圖那棵樹,同一層節點超過 20 個的時候,要不要預設收合起來?' '預設收合' '全部展開'
t 0 A "②① 花錢:要不要開一顆會產生帳單的付費實例" \
'這個功能需要一顆專用的 Cloudflare 付費實例才跑得動,要開嗎?每月大約會多一筆帳單。' '開' '不開'
t 0 A "③② 不可逆:刪掉 stage 上那批資料重灌" \
'要把 stage 上那批舊資料整批刪掉重灌嗎?刪了拿不回來。' '刪掉重灌' '保留'
t 0 A "④③ 跨專案結構:ISEP 的閘要不要拆回各子 repo 各自維護" \
'ISEP 的閘目前是全機共用一份。要不要改成各子 repo 各自維護一份自己的?這會改掉 repo 之間的邊界。' '拆回各 repo' '維持共用'
t 0 A "⑤⑤ 物理人閘:要你親手去終端機解保險才推得了 prod" \
'這一版要推 prod,需要你親手在終端機跑 github-arm.sh 解保險,我沒辦法代跑。現在推嗎?' '現在推' '晚點再說'
echo
echo "── B 群:該擋(純技術路徑選擇,四題全否)─────────────────────"
t 2 B "⑥ 實作分層:重試邏輯放哪一層" \
'重試邏輯要放在 client 那一層還是 worker 那一層?' '放 client' '放 worker'
t 2 B "⑦ 空白選擇題:兩案差別只有實作成本" \
'這個 bug 有兩種修法,A 是改解析器、B 是在呼叫端多包一層,兩種我都做得出來,效果一樣。你想要哪一個?' 'A 改解析器' 'B 呼叫端包一層'
t 2 B "⑧ 請示句:已經交代過的事還在問要不要開始" \
'那我現在開始修那支 hook 嗎?' '開始' '先等等'
t 2 B "⑨ 內部命名/擺放位置(用戶看不到)" \
'新的測試檔要放在 hooks/tests/ 底下還是 scripts/ 底下?檔名要叫 guard.test.sh 還是 test-guard.sh' 'hooks/tests/' 'scripts/'
rm -f /tmp/.ask-gate-warned-* 2>/dev/null || true
echo
echo "══ $PASS/$N 通過${FAIL:+$FAIL 個失敗}${AFAIL:+(其中 $AFAIL 個是 A 群誤攔——這種最嚴重)} ══"
[ "$FAIL" -eq 0 ]