c2638668e3
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>
154 lines
7.9 KiB
Bash
Executable File
154 lines
7.9 KiB
Bash
Executable File
#!/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 <<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
|