#!/usr/bin/env bash # history-first-guard.sh — 修舊東西前,先查它修過幾次(PreToolUse: Edit|MultiEdit) # # 病根(leo 2026-08-02 三句,同一天): # ①「拿掉獨立的登記動作 ← 哪裡來的?**你不要把舊版放上去**」 # ②「這個修了無數次,**你不要再把舊版弄回來,不要再搞不清楚機制**」 # ③「**你已經好幾次把好的修壞,因為你去修以前沒查記錄,問了早就解決的問題**」 # # 當天實錄(三次都是同一個病): # A 把 leo 08-01 才修好的 t159 庫登記提案拆掉(沒查 wiki 就重新設計) # B 把 07-28 已拍板的「庫自動出現」做成選擇題丟回去問 leo(定案寫在 commit 429b2d9 裡) # C 版本號改 semver 時沒 grep 誰在消費它 ⇒ 打爆 trayCloudVersionStale(把好的修壞) # D release.mjs 列舉式重建 manifest ⇒ 吃掉 daemon 欄(把好的修壞) # # 為什麼要**這一道**:wiki-first-search.sh 只在「grep 時」提醒且 exit 0(無牙齒), # 而真正危險的時機是「**要動一個已經存在的東西**」那一刻。 # 查 hook 盤點:16 支裡它是唯一該管查記錄卻沒有 exit 2 的 ⇒ 我今天三次跳過都沒被擋。 # # 機制:Edit/MultiEdit 一個**既有 code 檔**時,用該檔名/函式名去 grep wiki+tasks, # 有歷史紀錄就把命中摘要推到眼前並擋一次(exit 2),要求先讀再改。 # 讀完再送一次同樣的編輯即放行(同一檔 10 分鐘內只擋一次,不無限鬼打牆)。 # # 豁免:新檔/文件/測試/SDD 本身/wiki 本身;查無歷史紀錄一律放行。 INPUT="$(cat)" FILE_PATH=$(printf '%s' "$INPUT" | python3 -c ' import sys, json try: print(json.load(sys.stdin).get("tool_input", {}).get("file_path", "") or "") except Exception: print("") ' 2>/dev/null || echo "") [ -z "$FILE_PATH" ] && exit 0 [ -f "$FILE_PATH" ] || exit 0 # 新檔不擋 # 只管 code 檔 case "$FILE_PATH" in *.ts|*.tsx|*.js|*.jsx|*.mjs|*.go|*.py|*.sh) ;; *) exit 0 ;; esac # 這些不擋:測試、SDD、wiki、hook 自己 case "$FILE_PATH" in *_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;; *system-dev/docs/3-specs/*|*system-dev/wiki/*|*/.claude/hooks/*) exit 0 ;; esac ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" BASE=$(basename "$FILE_PATH") STAMP="/tmp/.history-guard-$(printf '%s' "$FILE_PATH" | shasum | cut -c1-12)" # ── 第 0 道:問過 KBDB 了沒(2026-08-08 leo 兩度點破後補上的牙齒)──────────── # # 🔴 為什麼加在**最前面**:leo 定的成本效益序是 # KBDB(一次看所有庫,最便宜)→ repo 的 wiki/tasks → git log -S → 讀源碼(最貴)。 # 原本這道閘只在文字裡「建議」補一次 kbdb_search,零機械檢查 # ⇒ 我 08-08 直接跳過,leo 當場抓到:「你自己寫了 hook,結果一次次改都沒產生用途」。 # ⇒ 順序要做成環境邊界,不是靠我每次記得(leo 母原則:環境邊界取代邏輯判斷)。 # # 時戳由 kbdb-asked-stamp.sh(PostToolUse,matcher 對 kbdb_* 工具)寫下。 # 逃生口:KBDB 真的連不上時 `touch /tmp/.kbdb-down`——放行但留痕,且回覆裡要說明。 KBDB_STAMP=/tmp/.kbdb-asked KBDB_DOWN=/tmp/.kbdb-down kbdb_fresh=0 now=$(date +%s) for f in "$KBDB_STAMP" "$KBDB_DOWN"; do if [ -f "$f" ]; then t=$(cat "$f" 2>/dev/null || echo 0) [ $((now - t)) -lt 3600 ] && kbdb_fresh=1 fi done if [ "$kbdb_fresh" -eq 0 ]; then cat >&2 <<'KEOF' 🧠 KBDB 先問警察:這一小時內你**沒有問過 KBDB**,卻已經要改源碼了。 【leo 2026-08-08】「KBDB 可以看到所有庫,**成本最低**,要求先查⋯⋯ **KBDB 的知識庫就是要建來取代你的記憶的**。要做的就是不要每次我來提醒。」 成本效益序(貴的排後面): KBDB(一次看所有庫)→ 該 repo 的 wiki/tasks → git log -S → 讀源碼(最貴, 只看得到「現在長怎樣」,看不到「為什麼變成這樣」) 先做這兩步(花不到十秒): kbdb_get_map() # 先看有哪些庫、該進哪一庫 kbdb_search(q="<一句話描述這次要改什麼>", mode="semantic") 📌 **查了沒命中也算數**——那筆「查了沒有」就是一份餵食清單, 代表這段知識還沒進 KBDB,收工時該把它餵進去(08-08 實例: mistakes.md 那條三元組一字不差卻 0 命中 ⇒ 開發史根本沒進 KBDB)。 🚪 KBDB 真的連不上:`touch /tmp/.kbdb-down` 後重送,並在回覆裡明說。 KEOF exit 2 fi # 10 分鐘內同一檔已提示過 → 放行(讀完就能繼續改,不鬼打牆) if [ -f "$STAMP" ]; then then_=$(cat "$STAMP" 2>/dev/null || echo 0) [ $((now - then_)) -lt 600 ] && exit 0 fi # 查 wiki+tasks 有沒有這個檔的歷史 HITS=$(grep -rn --include="*.md" -- "$BASE" \ "$ROOT/system-dev/wiki" "$ROOT/system-dev/docs/3-specs" 2>/dev/null | head -8) # 🔴 2026-08-04 補(leo:「凡是有改就要入 wiki,凡是要改都要查 wiki, # 不是有限制,為什麼還會漏」):wiki grep **只認字面**,同一個 bug 若在 # **別的分支**已經修好,wiki 那幾行不會告訴你——今天就這樣重修了一次 # `trayCloudVersionStale`(08-02 `dde7a5b` 已在 fix/cis-round3-landing-favicon 修好, # 而我在 feat/daemon 上看到的是沒修過的舊 code,就當新 bug 從頭做一遍, # 還做得比原版差:原版有 compareSemver 逐段整數比較,我的只判「是不是 semver」)。 # ⇒ 這裡直接把「這個檔在所有分支的近期 commit」秀出來,讓「別的分支已修過」無所遁形。 GITLOG="" if git -C "$(dirname "$FILE_PATH")" rev-parse --git-dir >/dev/null 2>&1; then GITLOG=$(git -C "$(dirname "$FILE_PATH")" log --all --oneline -8 \ --format='%h %d %s' -- "$FILE_PATH" 2>/dev/null | head -8) fi # 兩邊都查無 → 真的是新東西,放行 [ -z "$HITS" ] && [ -z "$GITLOG" ] && exit 0 date +%s > "$STAMP" cat >&2 <" -- <檔案> git log --all --oneline -- <檔案> # ← 上面那段已經幫你列了 查到就 \`git show :<檔案>\` 取回原版,**不要自己重寫一份**。 Q1 這個檔/這段邏輯**修過幾輪**?每輪的真兇分別是什麼? Q2 我現在想做的改動,**有沒有哪一輪已經試過並被否決**? (危險訊號:「我想到一個更根本的做法」出現在老問題上, 多半是重新發明一個已被試過或已被 leo 否決的方案) Q3 leo 對這件事說過的話,我是**回去找原文**,還是憑印象詮釋? (08-02 實錄:「沒有登記這回事」指的是前端別有人工按鈕, 我卻詮釋成「拆掉後端登記機制」=差一個世代的誤讀) 📌 grep 只認字面。**查不到不等於沒記載**——補一次語意搜尋: kbdb_search(q="<一句話描述>", mode="semantic")/kbdb_get_map() (讀完再送一次同樣的編輯即放行;同一檔 10 分鐘內不再打擾。) EOF exit 2