Files
ISEP/hooks/history-first-guard.sh
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:41:46 +08:00

154 lines
7.9 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.
#!/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 wikitasks
# 有歷史紀錄就把命中摘要推到眼前並擋一次(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.shPostToolUsematcher 對 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 <<EOF
🚓 歷史警察:你要改的 \`$BASE\` **有前科**——先看它修過幾次,再決定怎麼改。
【leo 2026-08-02】「你已經好幾次把好的修壞,**因為你去修以前沒查記錄**,
問了早就解決的問題」
【leo 2026-08-02】「這個修了無數次,不要再把舊版弄回來,**不要再搞不清楚機制**」
── wiki/tasks 裡關於這個檔的紀錄 ──
$HITS
── 這個檔在**所有分支**的近期 commit(← 別的分支可能已經修過同一個 bug)──
$GITLOG
━━━ 改之前先回答(答不出來就是還沒查夠)━━━
Q0 🔴 **這個 bug 是不是別的分支已經修好了?**(08-04 實錄:我在 feat/daemon 重修了
08-02 已在另一分支修好的 semver 比較,還修得比原版差)
機械查法(五秒,別憑感覺):
git log --all --oneline -S"<關鍵函式名>" -- <檔案>
git log --all --oneline -- <檔案> # ← 上面那段已經幫你列了
查到就 \`git show <sha>:<檔案>\` 取回原版,**不要自己重寫一份**。
Q1 這個檔/這段邏輯**修過幾輪**?每輪的真兇分別是什麼?
Q2 我現在想做的改動,**有沒有哪一輪已經試過並被否決**?
(危險訊號:「我想到一個更根本的做法」出現在老問題上,
多半是重新發明一個已被試過或已被 leo 否決的方案)
Q3 leo 對這件事說過的話,我是**回去找原文**,還是憑印象詮釋?
(08-02 實錄:「沒有登記這回事」指的是前端別有人工按鈕,
我卻詮釋成「拆掉後端登記機制」=差一個世代的誤讀)
📌 grep 只認字面。**查不到不等於沒記載**——補一次語意搜尋:
kbdb_search(q="<一句話描述>", mode="semantic")kbdb_get_map()
(讀完再送一次同樣的編輯即放行;同一檔 10 分鐘內不再打擾。)
EOF
exit 2