e0557334bc
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
93 lines
4.3 KiB
Bash
Executable File
93 lines
4.3 KiB
Bash
Executable File
#!/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 ]
|