5909c2a43a
實撞(2026-09-02,做這張票的路上):KBDB 兩支工具都回 Connection closed,
照 history-first-guard.sh 印出來的那行 `touch /tmp/.kbdb-down` 打完再送同一個編輯,
**被一模一樣地擋第二次,訊息一字不差**。
根因(打出來的,不是推的):
$ touch /tmp/.kbdb-down
$ t=$(cat /tmp/.kbdb-down); echo "[$t]" → []
$ echo $((now - t)) → 1788318047 ≥ 3600
touch 造的是空檔,而那支閘讀的是**檔案內容**當時戳
⇒ 空字串被當成 0 ⇒「距今 17 億秒」⇒ 永遠不新鮮 ⇒ 恆擋。
⇒ 那行逃生門是印出來好看的,**沒有人照著打過一次**。
跟本票 comment 6071(scripts/ticket 沒有 handback 這個動詞)、
inkstone/ISEP#125(worktree 閘的逃生門原文照打 rc=2)是同一句話。
改法:內容不是數字就改用檔案的 mtime——touch 做的正是更新 mtime,
所以訊息那一行從此真的走得通;kbdb-asked-stamp.sh 寫數字的舊格式照樣相容。
路徑加 KBDB_STAMP_DIR 覆寫,只為了讓測試不去動這台機器真正的戳記
(清掉別的 session 的戳記=把閘弄成隨機的)。
沒有把閘弄鬆:過期的戳記照樣不算數(測試第 ⑦ 條守這件事),
文件/新檔/測試檔本來就不擋的三條也各有一格守著。
驗(hooks/tests/history-first-guard.test.sh,10 條,全離線):
修好後 10/10
修好前(路徑隔離) 9/10 —— ④「擋人的換成歷史警察了嗎」紅
⚠️ 第 ③ 條兩版都綠(它只驗離開碼,而過了第 0 道還有歷史警察會擋)
真正分辨得出新舊的是 ④,檔頭寫了怎麼重現這個對照。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
9.2 KiB
Bash
Executable File
171 lines
9.2 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`——放行但留痕,且回覆裡要說明。
|
||
#
|
||
# 🔴 **逃生門要真的走得通**(inkstone/ISEP#122,2026-09-02 實撞,本檔第二次修):
|
||
# 上面那句訊息叫人 `touch /tmp/.kbdb-down`,而 `touch` 造出來的是**空檔**
|
||
# ⇒ 舊版 `t=$(cat "$f")` 讀到空字串 ⇒ 算式當成 0 ⇒ 「距今 17 億秒」
|
||
# ⇒ 永遠不新鮮 ⇒ **照著訊息打完,還是被擋**。
|
||
# 實測(2026-09-02,KBDB 兩支工具都回 Connection closed 時):
|
||
# $ touch /tmp/.kbdb-down → 再送同一個編輯 → 一模一樣的擋,訊息一字不差
|
||
# ⇒ 那行逃生門是印出來好看的,**沒有人照著打過一次**
|
||
# (同族:ISEP#125 worktree 閘印的逃生門原文照打 rc=2;本票 comment 6071)。
|
||
#
|
||
# 改法:**內容不是數字就用檔案的 mtime**——`touch` 做的正是更新 mtime,
|
||
# 所以訊息裡那一行從此真的走得通,而 `kbdb-asked-stamp.sh` 寫數字的舊格式照樣相容。
|
||
# 路徑用 `KBDB_STAMP_DIR` 可覆寫,**只為了讓迴歸測試不去動這台機器真正的戳記**
|
||
# (測試把別的 session 的戳記清掉=把閘弄成隨機的)。
|
||
KBDB_STAMP="${KBDB_STAMP_DIR:-/tmp}/.kbdb-asked"
|
||
KBDB_DOWN="${KBDB_STAMP_DIR:-/tmp}/.kbdb-down"
|
||
kbdb_fresh=0
|
||
now=$(date +%s)
|
||
for f in "$KBDB_STAMP" "$KBDB_DOWN"; do
|
||
[ -f "$f" ] || continue
|
||
t=$(cat "$f" 2>/dev/null || true)
|
||
case "$t" in
|
||
''|*[!0-9]*) t=$(date -r "$f" +%s 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0) ;;
|
||
esac
|
||
[ "${t:-0}" -gt 0 ] || continue
|
||
[ $((now - t)) -lt 3600 ] && kbdb_fresh=1
|
||
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
|