#!/usr/bin/env bash # 管什麼: 把 kbdb_search 掉出來的原始值當證據,貼進票或派工單裡,而那筆資料從沒走過產品檢索路徑時,擋一次。 # 為什麼: 2026-08-27 inkstone/Arcrun#167 comment 4865——總管用 kbdb_search 的輸出判定「兩個缺陷都還在」,退回一張已經做完、有測試、東西在 main 的票,還重派一條線。半小時後自己更正(comment 4877):那條路本來就不會有那個欄位。 # 誤觸時怎麼關: 指令或派工單裡加 `search-is-evidence: <一句理由>` 重送(留痕);同一 session 只擋一次。 # # search-is-not-proof-guard.sh — 你判斷的那條路,和你走的那條路,是同一條嗎? # (PreToolUse: Bash / Agent / Task) # # ── 這一格為什麼原本沒有閘(inkstone/ISEP#30 comment 4879 的原話)────── # 「所有現有的閘防的是**危險動作**:推 main、推 prod、寫金鑰、打真庫、開重複票。 # 而『用 kbdb_search 去證明某條路壞了』——不危險、不寫檔、不推 main。 # **它只是錯的方法。** ⇒ 閘的座標系裡沒有這一維。」 # # ── 判準:出處,不是措辭 ──────────────────────────────────────────── # 🔴 驗收條件第 4 條寫死:**不要用關鍵字比對我的措辭**。 # leo 2026-08-17 已經證偽過文字層——那天 **8 次誤攔、0 次正確攔截**, # 而且方向穩定:紅線寫得越細,命中關鍵字的機率越高 ⇒ **在懲罰謹慎**。 # # 所以這支一個字都不看。它只問一件機械問題: # # 你正要送出去的這份東西裡,有沒有一個**只在 kbdb_search 的回應裡出現過** # 的值(entry id / `kb://` 來源位址)?而那筆資料,這個 session 從來沒有 # 用產品檢索路徑(graph / wiki 內容)拿過? # # ⇒ 兩個條件都成立,才代表「我在拿輔助工具的輸出,當產品檢索路徑的證據」。 # 這是**資料的出處**,不是我的用詞。值跟動作一樣有限且可枚舉。 # # ── D81:哪條才是產品檢索路徑(leo 2026-08-13 親自裁定)────────────── # `system-dev/docs/2-architecture/decisions/D81-vector-only-for-entity-normalize.md` # 「你會用 **graph 找到該找的東西**,用 **LLM wiki 去拉到對的內容**, # 所以全量向量化沒意義⋯⋯**向量的目的是用來協助把 entities normalize**。」 # ⇒ 主路徑=graph + wiki 內容;向量/關鍵字搜尋=**輔助**。 # # 🔴 **這支不會讓 `kbdb_search` 變難用。** 它是合法的基本盤工具, # subagent-wiki-guard.sh 還明文要求「查任何東西之前先用它查 wiki」。 # 查一筆資料在不在、找 record_id、翻 wiki 找判例——**全部照放**, # 因為那些用途不會把 entry id 貼進票裡當證據。 # # ── 真跡(本閘的規格,Arcrun#167 comment 4865 原文照貼)─────────────── # $ kbdb_search(q="小果 AFTEE 詐貸") → count 5 # { "id": "e_b77f23a1-b467-4aa4-883c-58a051ab8e66", … } # "metadata_json": "{\"source\":\"kb://小果被AFTEE詐貸.pdf#8\", … }" # ⇒ 那則退回裡**同時**有 entry id 和 `kb://` 位址,兩類指紋都命中, # 而整個 session 沒有任何一次 graph/wiki 內容的呼叫。 # ⇒ 半小時後 comment 4877:「上一則退回是錯的;真正的缺口是那支工具 MCP 沒有」。 # **那半小時裡,一條線被停掉、一張做完的票被退回、一份工單被重寫。** # # ── 誠實邊界(不補,補了就變成文字層)──────────────────────────────── # 1. **改寫過的證據抓不到**——「我看到出處還是 `../` 開頭」這種轉述沒有指紋。 # 但房規本來就要求貼原始輸出(規則四之一、派工鐵律之三), # 而貼了原始輸出就會被抓到。**不貼的那條路由別的閘管,不歸這支。** # 2. **只認 KBDB 這一族**。別的「錯的驗證路徑」(拿 portal 畫面當 MCP 的證據—— # 08-27 同一天的另一半)這支看不到,因為那不經過工具呼叫。 # 3. 想不出乾淨判準的地方就留白,不硬做。 # `.claude/branch-holds.md` 檔頭那句:**永遠在響的警報,等於訓練人忽略這個警報。** set -uo pipefail INPUT=$(cat) SID=$(printf '%s' "$INPUT" | python3 -c " import sys, json try: print(json.load(sys.stdin).get('session_id','') or 'nosid') except Exception: print('nosid') " 2>/dev/null || echo nosid) SEARCH="/tmp/.kbdb-evidence-search-$SID" # 這個 session 從來沒用搜尋拿過任何東西 ⇒ 這支沒有立場說話,最便宜的出口放在最前面 [ -s "$SEARCH" ] || exit 0 WARNED="/tmp/.search-is-not-proof-warned-$SID" [ -f "$WARNED" ] && exit 0 # 同一 session 只擋一次,不鬼打牆 # 🔴 env 要 export,不能掛在 `printf` 前面——那樣只餵到 printf,python3 收不到 export SEARCH_FILE="$SEARCH" export WALKED_FILE="/tmp/.kbdb-evidence-walked-$SID" HIT=$(printf '%s' "$INPUT" | python3 -c ' import json, os, re, sys try: d = json.load(sys.stdin) except Exception: raise SystemExit # 讀不到 payload 就不擋(那不是使用者的錯) tool = d.get("tool_name") or "" ti = d.get("tool_input") or {} if not isinstance(ti, dict): raise SystemExit # ── ① 這是不是一個「往外送」的動作?────────────────────────────── # 往外送 = 這份文字會變成別人的前提:貼進票、關票、記裁決、或發包給另一條線。 # 只在自己腦袋裡看搜尋結果,永遠不會走到這裡。 body, kind = "", "" if tool in ("Agent", "Task"): body = ti.get("prompt") or "" kind = "派工單" elif tool == "Bash": cmd = ti.get("command") or "" if not cmd: raise SystemExit outward = False if re.search(r"\bticket\s+(say|close|decide|new|subtask|handback)\b", cmd): outward = True if re.search(r"issues/\d+(/comments)?\b", cmd) and re.search(r"\b(POST|PATCH)\b", cmd): outward = True if not outward: raise SystemExit body, kind = cmd, "票上的留言" # 內文常常放在 -F <檔> 裡,指令本身看不到 ⇒ 跟 comment-carries-task-guard 同一招 m = re.search(r"[\s](?:-F|--file)[\s]+(\S+)", cmd) if m and os.path.isfile(m.group(1)): try: body += "\n" + open(m.group(1), encoding="utf-8", errors="replace").read() except Exception: pass else: raise SystemExit if not body: raise SystemExit # 逃生口(留痕:它會留在指令歷史/派工單上) if "search-is-evidence:" in body: raise SystemExit def load(p): try: with open(p, encoding="utf-8") as f: return {ln.strip() for ln in f if ln.strip()} except Exception: return set() searched = load(os.environ["SEARCH_FILE"]) walked = load(os.environ["WALKED_FILE"]) # ── ② 只在搜尋裡出現過、真路徑沒走過的那些值 ───────────────────── # 走過真路徑的(graph / wiki 內容 / query)從候選裡扣掉—— # **搜完再去走一次真路徑,本來就是對的做法,不該被罰。** only_search = searched - walked if not only_search: raise SystemExit # ── ③ 這些值有沒有真的被貼進要送出去的那份東西裡 ────────────────── hits = sorted(v for v in only_search if v in body) if not hits: raise SystemExit print(kind + "\t" + "\t".join(hits[:3])) ' 2>/dev/null) || exit 0 [ -n "${HIT:-}" ] || exit 0 KIND=$(printf '%s' "$HIT" | cut -f1) VALS=$(printf '%s' "$HIT" | cut -f2- | tr '\t' '\n' | sed 's/^/ /') date +%s > "$WARNED" cat >&2 <\` 重送(留痕)。 同一 session 只擋這一次。 EOF exit 2