#!/bin/bash # subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki # # 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。 # agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。 # # 🔑 設計轉向(leo 2026-07-21): # 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**, # 跟「我記得讀 wiki」是同一個病。leo 點破: # 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki, # 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」 # → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條 # 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。 # # 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。 # 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。 set -euo pipefail INPUT=$(cat) PROMPT=$(printf '%s' "$INPUT" | python3 -c " import json,sys try: d=json.load(sys.stdin) print(d.get('tool_input',{}).get('prompt','')) except Exception: print('') " 2>/dev/null || echo "") [ -z "$PROMPT" ] && exit 0 # 上游已經交代了 → 不必重複注入 if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then exit 0 fi # 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要) if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then exit 0 fi # 🗺️ 真身地圖注入判斷(leo 2026-08-01:「你身為總管就是要能搞定這些跨 repo, # 你才不會發出錯誤的命令給 subagent」)——偵測到部署/真身相關關鍵詞就把地圖注入。 NEED_MAP=0 if printf '%s' "$PROMPT" | grep -qiE "install|portal|landing|部署|deploy|worker|bundle|wrangler|真身|上線|出貨|arcrun-rag|console-ui"; then NEED_MAP=1 fi export NEED_MAP python3 - <<'PY' import json, os, pathlib guidance = """【自動注入 0:**先看你的 skill 清單有沒有現成的**】 **動手前先掃一眼 system prompt 的 available skills**——那裡的東西是「已經被驗證過的做法」, 比你自己從 repo 文件推測可靠得多。命中就 `Skill(skill="")` 讀它,照它做。 判準:任務裡出現的**專有名詞**(產品名/平台名/工具名,如 Arcrun、Cloudflare、n8n…), 到 skill 清單裡找同名或近義的那支。**有就一定要先讀**,不要跳過去自己翻程式碼。 > 為什麼這條排在「查 wiki」前面(2026-07-30 實測四次考試逼出來的): > 叫 haiku「幫我用 Arcrun 做 X」,它三次都失敗—— > ①跑去 call MCP 拿到 401 ②自己 find repo 猜著寫 YAML ③讀 repo 文件後用了 > **不存在的 `ON_TRUE`/`ON_FALSE` 邊**(n8n 式推測)。 > 第四次只多給一句「你的 skill 清單裡有一支相關的」→ 它 `Skill(arcrun)` → **一次寫對**。 > ⇒ 差別不在能力,在**它沒想到要看清單**。而本 hook 原本只教「查 wiki/grep」, > 反而把它導向 repo 文件(`grep skill` 於本檔=0)。 --- 【自動注入 1:查任何東西之前,先查 wiki】 你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。 **接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。** 🔴 **查法有強弱之分,一律從最強的開始——沒有那個能力才降級。** (leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback, 但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」) **① 語意搜尋(最強,優先)**——有 Arcrun RAG MCP 就用它,用**自然語言問句**,不是關鍵字: kbdb_search(q="<用一句話描述你要找什麼>", mode="semantic") 不確定該查哪個庫 → 先 kbdb_get_map() 看藏書地圖 要沿關係展開 → kbdb_graph_neighbors() **② 關鍵字搜尋**——語意不可用時:kbdb_search(q="...", mode="keyword") **③ grep(最弱,最後手段)**——連 MCP 都沒有時: grep -rin "<關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/ 🔴 **為什麼順序是硬規定(2026-07-21 實際事故)**: 查「CF 上的 git 託管」時只用了 grep,搜 Gitea/freeze/D43 等字面詞 → **零命中**, 結論寫成「這件事沒查過、申請表沒送」。 事後用**同一個問題**跑語意搜尋,**第一筆就命中**(score 0.858): 「Cloudflare Artifacts:假設內建 git 倉庫機制的 CF 功能,成立則可全 CF 化」, 還帶出三元組「Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea」—— **負責人 15 天前就記在筆記裡了。** → **grep 只認字面,要求你先猜對那個詞;語意搜尋不需要你猜對。** 用 grep 查不到 ≠ wiki 沒記載,只代表你沒猜中用詞。 🔴 **凡結論涉及「某人沒做某事」,回報前必須先用語意搜尋查該事的記載**—— 這種結論錯了會變成**指控**,成本遠高於技術判斷錯誤。 為什麼這是划算的: • wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。 • 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。 從稿子推論會系統性得出過時結論。 • wiki 沒記載,才值得花力氣翻原文。 • **凡結論涉及「某人沒做某事」,回報前必須先 grep 該事在 wiki 的記載**—— 這種結論錯了會變成指控,成本遠高於技術判斷錯誤。 三條硬規則: 1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突, 不要自行用 code 推翻 wiki。 2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。 那是當時狀態,不是永久禁令;條件已滿足就是可動。 (2026-07-20 實際事故:agent 只看到「不可動」就回報不能動, 實際上解除條件早已滿足,害負責人被擋三輪。) 3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。 """ # 🗺️ 注入 2:跨 repo 真身地圖(NEED_MAP=1 時才附) if os.environ.get('NEED_MAP') == '1': parts = [] for f, title in [ ('system-dev/wiki/install-update-chain.md', '安裝/更新鏈(改了什麼→怎麼到用戶手上)'), ('system-dev/wiki/deployment-map.md', '網址↔Worker/Pages↔後端 對照表'), ]: fp = pathlib.Path(f) if fp.exists(): body = fp.read_text(encoding='utf-8', errors='ignore') parts.append(f"### {title}\n來源:`{f}`(**這是實測釘死的真相源,勝過你從程式碼推論**)\n\n{body[:9000]}") if parts: guidance += ("\n\n---\n\n【自動注入 2:🗺️ 跨 repo 真身地圖——**先讀這個,不要自己猜哪份是活的**】\n\n" "🔴 **病根(leo 2026-08-01 重話)**:「哪個已經廢棄了你也不知道,總不能每次都花很多時間\n" "做簡單的『找到程式碼在哪裡』的工作?」——同名/相似檔案有很多份,\n" "**問題不是找不到,是找到太多份而不知道哪份還活著**。\n\n" "**規則**:① 動手前先在下方地圖找你要改的東西 ② 地圖沒有才自己驗\n" "③ **驗完立刻補進地圖**(帶佐證:怎麼證明它是活的)④ 地圖與程式碼衝突 → **以地圖為準**並回報衝突。\n\n" + "\n\n".join(parts)) print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "additionalContext": guidance } }, ensure_ascii=False)) PY exit 0