Files
ISEP/hooks/history-first-guard.sh
T
Leo 5909c2a43a 逃生門要真的走得通:touch /tmp/.kbdb-down 從來沒放行過(inkstone/ISEP#122)
實撞(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>
2026-09-02 11:11:39 +08:00

171 lines
9.2 KiB
Bash
Executable File
Raw 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`——放行但留痕,且回覆裡要說明。
#
# 🔴 **逃生門要真的走得通**inkstone/ISEP#1222026-09-02 實撞,本檔第二次修):
# 上面那句訊息叫人 `touch /tmp/.kbdb-down`,而 `touch` 造出來的是**空檔**
# ⇒ 舊版 `t=$(cat "$f")` 讀到空字串 ⇒ 算式當成 0 ⇒ 「距今 17 億秒」
# ⇒ 永遠不新鮮 ⇒ **照著訊息打完,還是被擋**。
# 實測(2026-09-02KBDB 兩支工具都回 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