Files
ISEP/hooks/wiki-first-search.sh
Leo 47ed778cc4 改走「直接複製進薄殼 repo」,並修掉一支會偷跑指令的閘
leo 2026-08-21:「你應該把 Plugin 直接裝進 Github repo,從本地直接複製就好了」

為什麼這條對:雲端 session 是 fresh clone 薄殼 repo,而 setup script 讀不到
環境變數。走 marketplace 就得同時處理憑證、repo 可見性、環境快取三件事——
今天這三件各失敗過一次。複製進 repo 之後,clone 下來就有,沒有任何前置條件。

新增 scripts/vendor-to-shell.py:
  把 hooks/skills/commands/scripts/.claude-plugin 整份複製到 .claude/isep/,
  並把 54 條 hook 註冊改寫成薄殼裡的絕對路徑。
  保留 CLAUDE_PLUGIN_ROOT 這個變數名(44 支閘內部靠它定位自己的 lib/),
  只是把它指到複製過來的那份。

冒煙測試(54 條註冊全跑一遍,找路徑壞掉的):
  第一輪 4 條壞 → 3 條是 log 目錄不存在(已補建 .claude/hooks/)
                  1 條在真身也一樣壞 ⇒ 不是複製造成的
  第二輪 0 條壞

順手修掉那支既有 bug:wiki-first-search.sh
  python3 -c 用雙引號,註解裡的反引號被 shell 當指令替換
  ⇒ 這支閘每次觸發都在偷跑 bge-m3 與 head changelog.md。
  改成全形引號後實測靜默 exit 0。
  (crude grep 掃出 6 支疑似,但冒煙測試證明只有這一支真的中——
    再一次:證據勝過掃描。)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:51:00 +08:00

183 lines
12 KiB
Bash
Executable File
Raw Permalink 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.
#!/bin/bash
# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前
#
# 病根(2026-07-20 leo 點破,mistakes 第一鐵律):
# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。
# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論,
# 而是就像我直接在頁面 cmd+F,那些都會高亮。」
#
# 設計要點(為什麼是這個形狀):
# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。
# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。
# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。
# 3. 🔴 **硬擋,不是提醒**leo 2026-08-05 改):
# 「改成首先查 wiki,如果 wiki 沒有則放行其他搜尋,要擋」
# 原版全程 exit 0(只印字),實測擋不住——我照樣一路 curl/grep code
# 連 wiki 都沒查就下結論,還把 401 誤判給不相干的 commit。
# 現在:**本輪沒查過 wiki ⇒ exit 2 擋下**;查過(不論有無命中)⇒ 放行。
# 放行條件刻意寬鬆——目的是「逼你先看一眼 wiki」,不是攔住你查原文。
#
# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。
# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。
set -euo pipefail
INPUT=$(cat)
TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
# 取出這次查詢的關鍵字:Grep 用 patternGlob/Read 用路徑的檔名部分
QUERY=$(printf '%s' "$INPUT" | python3 -c "
import json,sys,os,re
try:
d=json.load(sys.stdin); ti=d.get('tool_input',{})
q = ti.get('pattern') or ''
if not q:
p = ti.get('file_path') or ti.get('path') or ''
q = os.path.splitext(os.path.basename(p))[0] if p else ''
if not q:
# Bash2026-07-21 補的破口——原版只掛 Grep|Glob|Read
# 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。
# leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。
# 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。
cmd = ti.get('command') or ''
# 2026-08-05 再補一個破口(leo 當場點破:「你應該做任何查詢先去查 KBDB,
# 但剛剛這一輪你都沒有查,但 hooks 沒攔阻?」):
# 原本只認「動外部系統」的動詞,但**查證類**指令(git log / grep / unzip /
# 讀 manifest)完全不觸發 ⇒ 我查「release 版本號怎麼算」整輪沒查過 KBDB/wiki。
# ⇒ 補上查證類動詞。判準:**會讓我形成結論的指令**都該先問記憶,
# 不是只有「會改壞東西的指令」。
# 2026-08-05 第三次補(leo:「我看你剛剛 bash 裡很多 grep,為什麼不是查 wiki?」):
# 再補「讀檔形成結論」的動詞——head/cat/sed/tail/awk/find/jq。
# 之前只認「動外部系統」與部分查證動詞,「head changelog.md」 這種
# **直接讀檔下判斷**的完全不觸發。
if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push|git|grep|unzip|manifest|version'
r'|head|cat|sed|tail|awk|find|jq)\b', cmd):
# 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞
cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd)
skip = {'https','http','client','accounts','workers','scripts',
'application','content','Authorization','Bearer','python3',
'curl','npx','bash','echo','grep','local','branch','origin'}
cand = [c for c in cand if c not in skip and not c.startswith('-')]
q = max(cand, key=len) if cand else ''
# grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞
# 🔴 2026-08-05 leo 點破:「最常做的就是 grep,為什麼沒在裡面」
# ——Grep 其實有註冊,壞在**這行取詞規則**:
# ① 連字號被當分隔 ⇒ 「bge-m3」 只取到「bge」(3 字)不足 4 字 ⇒ 整支不觸發
# ② **中文完全不匹配** ⇒ 查「版本號」「出貨」這類詞一律不觸發
# 而我日常查的關鍵字大量正是這兩類 ⇒ hook 形同虛設。
# ⇒ 容許「-」與「.」,並支援 CJK;中文 2 字即算一個詞。
words = re.findall(r'[A-Za-z_][A-Za-z0-9_.-]{2,}', q)
cjk = re.findall(r'[\u4e00-\u9fff]{2,}', q)
words = words + cjk
print(max(words, key=len) if words else '')
except Exception:
print('')
" 2>/dev/null || echo "")
[ -z "$QUERY" ] && exit 0
WIKI_DIR="system-dev/wiki"
[ -d "$WIKI_DIR" ] || exit 0
# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了)
TARGET=$(printf '%s' "$INPUT" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin); ti=d.get('tool_input',{})
print(ti.get('file_path') or ti.get('path') or '')
except Exception: print('')
" 2>/dev/null || echo "")
# ── wiki-first 閘門(leo 2026-08-05)────────────────────────────
# 記號檔:本輪(本 session)是否已經查過 wiki。放 /tmp 依 session 隔離,
# 新 session 自動重置——每輪工作都要重新先看一眼 wiki。
STAMP="/tmp/.wiki-first-$(id -u)-${CLAUDE_SESSION_ID:-nosession}"
# 這次動作**本身就是在查 wiki** → 蓋章放行(之後才允許查別的)
case "$TARGET" in
*system-dev/wiki*) : > "$STAMP"; exit 0 ;;
esac
# Bash 指令裡直接 grep/read wiki 的也算(例:grep -rn xxx system-dev/wiki/
if printf '%s' "$INPUT" | grep -q "system-dev/wiki"; then
: > "$STAMP"; exit 0
fi
# 已經查過 wiki → 放行,後面照舊只做提醒
if [ ! -f "$STAMP" ]; then
# 🔴 還沒查過 wiki 就想翻原文/打外部系統 → 擋(exit 2)
PRE_HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -8 || true)
{
echo "════════════════════════════════════════════════"
echo "⛔ 先查 wiki,才准查別的(leo 2026-08-05 立)"
echo "════════════════════════════════════════════════"
printf '你正要查「%s」,但這一輪還沒查過 wiki。\n\n' "$QUERY"
# 🔴 成本順序(leo 2026-08-05):「**成本最低的查就是 KBDB**,整了所有 repo 的 wiki
# 查詢應該效果最好最簡單,但你會去先查成本高的,甚至查不到後去查源碼,成本很高。」
# ⇒ 一律 **KBDB 先**(跨全部 repo、語意搜尋、一次呼叫),
# 再 grep 本 repo wiki(只認字面、只有這個 repo),最後才翻源碼。
echo "查詢成本由低到高——**照順序來,別跳過前面直接翻源碼**:"
echo ""
echo " 1️⃣ KBDB**最便宜也最強**:整合**所有 repo** 的 wiki,且有三種檢索)"
echo " kbdb_search(q=\"…\", mode=\"semantic\") ← 語意:問句、想不到精確字眼時"
echo " kbdb_search(q=\"…\", mode=\"keyword\") ← 關鍵字:確定有某個詞"
echo " kbdb_graph_neighbors(...) ← 關係:沿「A >> 關係 >> B」展開"
echo " kbdb_get_map() ← 不知道該查哪個庫時先看藏書地圖"
echo ""
echo " 2️⃣ 本 repo wiki**只有 grep 字面、只有這一個 repo** ⇒ 比 KBDB 弱很多)"
if [ -n "$PRE_HITS" ]; then
echo " ⚠️ **這裡已經有命中了**,先讀這幾行:"
printf '%s\n' "$PRE_HITS" | sed 's|^system-dev/wiki/| |'
else
echo " Grep/Read system-dev/wiki/ 用你自己的關鍵字"
echo " (本閘 grep 沒命中,但它只認字面、搜尋詞還是從你指令猜的)"
fi
echo ""
echo " 3️⃣ 源碼/原文(**最貴**:要讀很多、容易讀到已作廢的實作)"
echo ""
echo "wiki 沒有答案 → 那時再翻原文,本閘就會放行(查過 wiki 即解鎖本輪)。"
} >&2
exit 2
fi
# grep wiki(不分大小寫、含行號),最多 12 行避免洗版
HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true)
# 🔴 grep 零命中時**不能靜默退出**——那正是今天失敗的模式(2026-07-21):
# grep 查不到 → 以為 wiki 沒記載 → 結論「這件事沒查過」。
# 但 grep 只認字面,查不到往往只代表「沒猜中用詞」。
# → 零命中反而是**最該改用語意搜尋**的時刻,必須出聲。
if [ -z "$HITS" ]; then
echo "════════════════════════════════════════════════"
printf '🔍 grep 在 wiki 找不到「%s」——但這**不代表沒記載**\n' "$QUERY"
echo "════════════════════════════════════════════════"
echo "grep 只認字面,查不到通常只是「沒猜中用詞」。**改用語意搜尋再確認一次**:"
echo " kbdb_search(q=\"<用一句話描述你要找什麼>\", mode=\"semantic\")"
echo " 不知道該查哪個庫 → kbdb_get_map()|要沿關係展開 → kbdb_graph_neighbors()"
echo ""
echo "實例:查「CF 上的 git 託管」時 grep 全零命中,語意搜尋第一筆就命中"
echo "Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea,負責人 15 天前就記了)。"
echo ""
exit 0
fi
COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ')
echo "════════════════════════════════════════════════"
printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT"
echo "════════════════════════════════════════════════"
printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |'
echo ""
echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。"
echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。"
echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對,"
echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。"
echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。"
echo ""
echo "🔎 以上是 **grep(最弱的查法)** 的結果,只認字面,且搜尋詞是從你的指令**猜**出來的"
echo " (很可能太籠統而命中一堆無關的,同時漏掉真正的主題詞)。"
echo " **重要判斷一律補一次語意搜尋**——它不需要你猜對用詞:"
echo " kbdb_search(q=\"<一句話描述你要找什麼>\", mode=\"semantic\")"
echo " 實例:查「CF 的 git 託管」時 grep 猜到的詞是 cloudflare → 命中 12 處全無關、"
echo " 真正的答案(Artifacts)一筆沒撈到;語意搜尋第一筆就命中。"
echo ""
exit 0