#!/usr/bin/env bash # factory-idle-guard.sh 的迴歸測試(inkstone/ISEP#30)。 # # 來由:2026-08-23 雲端驗收連續三次被這道閘誤攔,三次都不是宣告意圖—— # ① 「那句是等你,不是我的下一步」 ← 在否認自己有下一步 # ② 「這不是『下一步我要做 X』」 ← 引用閘自己的訊息來說明沒在做 # ③ 貼出閘自己的正則原始碼來舉報這個 bug ← code block 裡的引用也被當成宣告 # 而閘的訊息教人走「選項③:改寫那句話,說明它在等什麼」,程式碼裡卻沒有那條分支。 # # 本檔的兩個方向都要有證據(誤攔與漏攔都要測): # A 群「該放行」——上面三種誤攔案例 + 既有放行路徑不准變鬆的回歸 # B 群「該擋」 ——真的宣告了未來動作、且這回合沒按下任何 trigger # C 群「訊息承諾的出路真的走得通」——訊息裡列的每一條,各一個實例 # # 用法:hooks/tests/factory-idle-guard.test.sh [hooks/factory-idle-guard.sh 的路徑] # 🔴 全程在一個乾淨的 TMP 底下造假 transcript,跑完自己清;不碰任何真 repo、不打網路。 # (CLAUDE_PROJECT_DIR 指向沒有 gitea remote 的 TMP ⇒ 副閘拿不到 token,直接 exit 0, # 所以本檔測的一律是主閘。) set -u HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/factory-idle-guard.sh}" TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0 N=0 # mk ... # block spec:`T:<文字>` = assistant 的文字段;`U:<工具名>` = assistant 的 tool_use mk() { python3 - "$@" <<'PY' import json, sys path, specs = sys.argv[1], sys.argv[2:] rows = [{"type": "user", "message": {"content": [{"type": "text", "text": "【工單】去辦"}]}}] blocks = [] for s in specs: k, v = s.split(":", 1) blocks.append({"type": "text", "text": v} if k == "T" else {"type": "tool_use", "name": v, "id": "t1", "input": {}}) rows.append({"type": "assistant", "message": {"content": blocks}}) with open(path, "w") as f: for r in rows: f.write(json.dumps(r, ensure_ascii=False) + "\n") PY } # t <期望 exit> <說明> ... t() { want="$1"; desc="$2"; shift 2 N=$((N+1)) tr="$TMP/tr-$N.jsonl" mk "$tr" "$@" out=$(printf '{"transcript_path":"%s"}' "$tr" \ | CLAUDE_PROJECT_DIR="$TMP" 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" | sed -n '1,6p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi } echo "── A 群:該放行(2026-08-23 雲端三次誤攔的原形狀)─────────────────" t 0 "① 否認自己有下一步(『那句是等你,不是我的下一步』)" \ 'T:我看完了。那句是等你,不是我的下一步。' t 0 "① 長版:連同被否認的原句一起引用" \ 'T:你貼的那行「回覆後我就繼續」是等你,不是我的下一步。' t 0 "② 引用閘自己的訊息來說明沒在做" \ 'T:這不是『下一步我要做 X』,是在說明那道閘認錯了對象。' t 0 "③ code block 裡貼閘自己的正則原始碼(舉報 bug)" \ 'T:誤攔的真兇是這段: ``` r"(下一步(我|就是|是)?[::]?\s*(?!不是宣告)|接下來我|我(現在|接著|等下|等一下)(就)?(去|來|做|派|審|跑)" ``` 它連裸的「下一步」三個字都吃。' t 0 "③ 行內 code 貼同一段正則" \ 'T:問題出在 `接下來我` 這個 alternative 沒有要求後面接動詞。' t 0 "引用 leo 的話(blockquote)含宣告字樣" \ 'T:> 下一步我去把那支閘修好,你不要插手。 以上是他八月十六日的原話,我引來說明這道閘的規格。' t 0 "User Story 票名(身為…我要…我才…)不是宣告" \ 'T:我把票名寫成:身為總管,我要把散落的分支收掉整理好,我才不會每天重數一次。' # ↓ 這兩向不是想出來的,是拿本機一條 2068 行的真 transcript 跑出來的: # 舊閘在 26 個真實回合終止點上擋了 2 次,兩次咬的都是「我在交代 leo 該做什麼」。 t 0 "④ 主詞是 leo 的下一步(『你下一步去把 X 併進 main』)" \ 'T:## 你下一步(兩招,先便宜的) 你下一步去把 feat/isep-dynamic-install 併進 main,然後開一個全新雲端 session。' t 0 "④ 同上,交代對方要跑的動作" \ 'T:前三題不適用。下一步還是那一個動作:你把那條分支併進 main,再跑一次看信標印什麼。' t 0 "④ 『你的下一步』——主詞在對方身上(單字 lookbehind 蓋不到的變體)" \ 'T:我把兩條路都寫清楚了。你的下一步去把那條分支併進 main,再開一個新 session 看信標。' t 2 "④ 的反面:主詞換回我自己 ⇒ 照樣擋" \ 'T:我把兩條路都寫清楚了。我的下一步去把那條分支併掉。' echo echo "── A 群:既有放行路徑,不准變鬆(迴歸)─────────────────────────" t 0 "純對話回合,沒有任何宣告" \ 'T:這道閘的問題在於它把引用當成宣告,量錯了對象。' t 0 "宣告了,但這個回合真的呼叫了 Agent(=按下 trigger)" \ 'T:下一步我去把 sdd-guard 的誤攔補上。' 'U:Agent' t 0 "宣告了,但這個回合真的呼叫了 Task" \ 'T:接下來我會去補測試。' 'U:Task' t 0 "宣告句帶票號指涉(已經在別人手上)" \ 'T:下一步我去把那支閘補好——已經派給 inkstone/ISEP#30,證據在 comment 3970。' t 0 "宣告之後真的動手了(文字後面還有工具呼叫)" \ 'T:先讀原始碼,接下來我會去補測試。' 'U:Read' echo echo "── B 群:該擋(真的宣告了未來動作,且這回合沒按下任何 trigger)────" t 2 "『下一步我去把 X 補上』,無派工無票號" \ 'T:我看完了。下一步我去把那支閘的誤攔補上。' t 2 "『接下來我會去補測試』" \ 'T:目前狀況清楚了。接下來我會去補測試。' t 2 "把請示寫成條件句(2026-08-17 leo 實撞那族)" \ 'T:回「規劃」我就派人盤這份計畫。' t 2 "『我等你確認』——空手停在等人身上" \ 'T:我等你確認。' t 2 "『我現在就去跑一遍』" \ 'T:我現在就去跑一遍那批測試。' t 2 "『確認後我才動』" \ 'T:核准後我才去改那支閘。' t 0 "票號寫在行內 code 裡也算數(等長替換才量得到)" \ 'T:下一步我去把那支閘補好——已派給 `inkstone/ISEP#30`。' t 2 "票號離宣告句太遠(文章別處提到而已)⇒ 不算它在別人手上" \ 'T:早上那批已經在 inkstone/ISEP#30 收掉了。 (中略:這裡是一大段跟那張票無關的驗收敘述,用來把距離拉開。'"$(printf 'x%.0s' $(seq 1 320))"') 下一步我去把那支閘的誤攔補上。' echo echo "── C 群:訊息承諾的每一條出路,都要真的走得通 ─────────────────" t 0 "出路①:這個回合直接呼叫 Agent 派出去" \ 'T:下一步我去把那支閘補好。' 'U:Agent' t 0 "出路②:把票號寫進那句話" \ 'T:下一步我去把那支閘補好(已派給 inkstone/ISEP#30)。' t 0 "出路③:改寫那句話,用 ⏸ 等: 說明它在等什麼" \ 'T:⏸ 等:leo 親手跑 scripts/github-arm.sh 解保險(D20 人閘,不是我能自己按的)。 在那之前我等你確認,這件事我不自己動。' t 0 "出路③:宣告 + 標記同時存在也放行(標記說明了它在等什麼)" \ 'T:那批 workflow 要重打 bundle,下一步我去重打。⏸ 等:arcrun-rag 那條 PR 先併,前置沒解除。' t 0 "出路④:這個回合收尾在動作上(最後一個工具呼叫之後沒有文字)" \ 'T:下一步我去把那支閘補好。' 'U:Edit' echo echo "── 出路③ 的反面:沒寫標記就照樣擋(不准靠刪掉那句話矇混)───────" t 2 "同一句話拿掉 ⏸ 等: 標記 ⇒ 擋" \ 'T:那批 workflow 要重打 bundle。我等你確認,這件事我不自己動。' echo echo "── 壞輸入不准亂擋(fail-open)────────────────────────────────" n_bad=0 for payload in '{}' '{"transcript_path":"/nonexistent/xx.jsonl"}' 'not-json'; do N=$((N+1)) printf '%s' "$payload" | CLAUDE_PROJECT_DIR="$TMP" sh "$HOOK" >/dev/null 2>&1; rc=$? if [ "$rc" -eq 0 ]; then printf ' ✅ 壞輸入 %-34s ⇒ exit=0\n' "$payload"; PASS=$((PASS+1)) else printf ' ❌ 壞輸入 %-34s ⇒ exit=%s(期望 0)\n' "$payload" "$rc"; FAIL=$((FAIL+1)) fi n_bad=$((n_bad+1)) done echo echo "────── 通過 $PASS / 失敗 $FAIL (共 $N 向)" [ "$FAIL" -eq 0 ]