沒人會叫的事會自己叫+讀不完的必讀檔要被整理(inkstone/ISEP#93、inkstone/ISEP#89)

兩張票放同一條分支:都是「該發生卻不會自己發生的事」,觸發點都在 session 邊界。

## inkstone/ISEP#93 —— 逾期和掛著沒人接的事會主動叫

- scripts/isep-nag        撈三種沒人會叫的事(逾期 milestone/等 leo 的票/掉在地上的棒子)
- scripts/isep-notify     發 Telegram,而且**發不出去的時候不會安靜**
- hooks/overdue-nag-guard.sh  SessionStart 跑一次(不輪詢、不 fan-out、不掛 Actions)
- hooks/tests/overdue-nag.test.sh  35 條,全程離線

實跑撈得出票上點名的那五個逾期 milestone(08-24 三個、08-26 兩個)。
沒東西可報時會說「查過了,沒有」——安靜跟壞掉長得一模一樣。

🔴 工單補的那個限制(今天實測出來的)已經處理:
「能不能發得出去」取決於這個 session 載到的 ISEP 是哪一版
(prod-write-guard v0.10.0 才認得出 notify_leo 不是部署,而 hook 註冊路徑
在 session 啟動當下就寫死了)。所以 isep-notify 會**先拿真的要送的那一則
去問這個 session 註冊的那支閘**,把判定寫成檔(誰都查得到),然後:
  · 放行 ⇒ 送,並驗內層 data.data.ok(外層 200 不算送到)
  · 會擋 ⇒ **不繞路**,改貼回票上並把原文印在眼前
用 git 歷史裡的真跡(c263866 那一版閘)測過「會擋」那條路。

 實測發現通道本身現在是斷的:實例上找不到 notify_leo 工作流(404)。
   不是閘、不是網路、不是金鑰。詳情與修法寫在 docs/TESTING.md 最後一段。
   退路兩次都走通了(inkstone/ISEP#93 comment 5182、5184)。

## inkstone/ISEP#89 —— wiki 太長時有人整理

- scripts/wiki-compress   audit/plan/apply/verify/bench 五個動詞
- hooks/wiki-size-guard.sh  SessionStart 點名太長的檔;寫檔時擋「沒走流程的壓縮」
- hooks/tests/wiki-compress.test.sh  25 條,全程離線、不碰真的 wiki

設計上最重要的一條:**只搬不改**,正文一個字都不動。
「合併同類、濃縮成一行」要重寫正文,而重寫的當下沒有人會發現弄丟了什麼。
所以機器只做「搬 + 目錄 + 標 ×N/↻」,合併留給人。

拿現在的 mistakes.md 複本實壓過(不動真的 wiki):
  7,681 行 → 1,199 行
  260 條一條都沒少(verify 用內文雜湊逐條對帳)
  80 個查詢命中率 80/80,定位成本 2,956 → 131 行,**快 22.5 倍**(bench)
verify 反向測過:真的弄丟一條時它抓得到(抓不到的對帳表比沒有更糟)。

## 盤點數字

在這棵樹上當場數的,不是拿上一版加減推的:
  ls hooks/*.sh | wc -l              → 55(was 53)
  grep -c '"command":' hooks.json    → 71(was 68)
plugin.json/README/hooks-inventory 三處同步改。
**版本號沒動**(0.11.0),待總管定版。
This commit is contained in:
Claude Code
2026-08-28 01:04:16 +00:00
parent bebbbd2c11
commit da9bd53cae
12 changed files with 2190 additions and 7 deletions
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# wiki-size-guard.sh — 讀不完的必讀檔要被講出來;壓它的時候不准沒人知道
# inkstone/ISEP#89
#
# ── 為什麼要有這一支 ──────────────────────────────────────────────────
# leo 2026-08-27 說那天「**失去記憶**」。而記憶失效的其中一個原因是:
# **東西還在,只是沒人讀得完。** `mistakes.md` 實測 7,681 行、260 條,
# 而它是「做新功能前讀一遍」等級的必讀檔。**讀不完的必讀檔,等於沒有。**
#
# 既有三支管的是別的事,一支都接不到這裡:
# · `wiki-secret-scan.sh` 寫進去的內容安不安全
# · `wiki-first-search.sh` 有沒有先查 wiki
# · `wiki-first-police.sh` 收工前有沒有查過
# **沒有任何一支管「太大了」。**
#
# ── 兩個事件,是同一件事的兩半 ────────────────────────────────────────
# ① SessionStart ── 講:哪幾個檔已經沒有人讀得完了(📝 記錄,不擋)
# ② Write|Edit|MultiEdit ── 擋:**大幅刪減 wiki 檔**卻沒走正式流程 ⇒ 擋一次
#
# 只有 ① = 又一個會被忽略的提醒。
# 只有 ② = 罰它做一件沒人告訴它該做的事。
#
# ── 🔴 ② 擋的判準是「動作」不是「措辭」(leo 2026-08-17)────────────────
# 「自然語言的變體是無限的,blacklist 永遠追不完……封路哲學之所以有效,
# 是因為它封的是**動作**——動作有限且可枚舉。」
# 本支比的是**字元數**:wiki 檔的內容淨縮水超過門檻 ⇒ 那就是一次壓縮,
# 不管它在 commit 訊息裡怎麼講。加字、改字、小修一律不碰。
#
# 為什麼壓縮要擋:**壓縮會弄丟東西,而弄丟的當下沒有人會發現**(票上原話)。
# 走 `scripts/wiki-compress` 有兩件手寫做不到的事——
# · `--ticket` 逼票號,並把「這一版壓掉了什麼」寫進 `.compress-log.md`(留痕)
# · `verify` 用內文雜湊**逐條對帳**,「沒弄丟」是算出來的,不是宣稱的
#
# 出口(留痕式,同 `wiki-secret-ok` 的慣例):真的要手改,
# 在寫入的內容裡放一個 `wiki-compress-ok`,並在 commit 說明理由。
set -uo pipefail
ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
TOOL_PY="$ROOT/scripts/wiki-compress"
INPUT="$(cat)"
EVENT=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try: print(json.load(sys.stdin).get("hook_event_name", "") or "")
except Exception: print("")
' 2>/dev/null || printf '')
# ══ ① SessionStart:講出來哪幾個檔已經沒人讀得完 ═══════════════════════
if [ "$EVENT" = "SessionStart" ]; then
[ -f "$TOOL_PY" ] || exit 0
PROJ="${CLAUDE_PROJECT_DIR:-$PWD}"
DIRS=""
for d in "$PROJ/system-dev/wiki" "$PROJ/.claude/wiki" \
"$PROJ/InkStoneCo/system-dev/wiki"; do
[ -d "$d" ] && DIRS="$DIRS $d"
done
[ -n "$DIRS" ] || exit 0
# shellcheck disable=SC2086
# 🔴 不要寫成 `OUT=$(…) || OUT=""``audit` 有東西可報時**刻意回離開碼 1**,
# 加上 pipefail,那個 `||` 會把剛拿到的結果整個清成空的
# ⇒ **有 7 個檔爆表,而這一段一聲不吭**。(2026-08-28 寫的時候當場踩到。)
OUT=$(python3 "$TOOL_PY" audit $DIRS 2>/dev/null | grep '🔴 太長' | head -6 || true)
[ -n "$OUT" ] || exit 0
python3 - "$OUT" <<'PY'
import json, sys
rows = [r for r in sys.argv[1].split("\n") if r.strip()]
msg = "📚 wiki 有 %d 個檔沒人讀得完了" % len(rows)
ctx = "\n".join([
"📚 **這幾個 wiki 檔已經超過『讀得完』的門檻**inkstone/ISEP#89):",
"",
] + rows + [
"",
"🔴 讀不完的必讀檔,跟沒有那個檔的差別只在於**它讓人以為有**。",
"leo 2026-08-27 說那天「失去記憶」——東西都還在,只是沒人讀得完。",
"",
"要整理:`python3 scripts/wiki-compress plan <檔>` 看怎麼壓,",
"然後 `apply <檔> --ticket owner/repo#N`**只搬不改**,正文一個字都不動),",
"壓完一定要 `verify` 逐條對帳。**壓縮這件事本身要有票**,不准順手做完沒人知道。",
])
print(json.dumps({"systemMessage": msg,
"hookSpecificOutput": {"hookEventName": "SessionStart",
"additionalContext": ctx}},
ensure_ascii=False))
PY
exit 0
fi
# ══ ② Write|Edit|MultiEdit:大幅刪減 wiki 檔 ⇒ 擋一次 ═══════════════════
VERDICT=$(printf '%s' "$INPUT" | python3 -c '
import json, os, re, sys
try:
d = json.load(sys.stdin)
except Exception:
print("SKIP:bad-json"); raise SystemExit
ti = d.get("tool_input") or {}
path = ti.get("file_path") or ""
# 只管 wiki 底下的 .md。別的檔案縮水是它們自己的事。
if not path.endswith(".md") or not re.search(r"(^|/)(wiki)/", path):
print("SKIP:not-wiki"); raise SystemExit
def size(s):
return len(re.sub(r"\s+", "", s or ""))
# 留痕式出口:內容裡放了 wiki-compress-ok ⇒ 放行(同 wiki-secret-ok 的慣例)
blob = json.dumps(ti, ensure_ascii=False)
if "wiki-compress-ok" in blob:
print("SKIP:opt-out"); raise SystemExit
before = after = 0
if "content" in ti: # Write:跟磁碟上現有的比
try:
with open(path, encoding="utf-8", errors="replace") as f:
before = size(f.read())
except Exception:
print("SKIP:new-file"); raise SystemExit # 新檔不是壓縮
after = size(ti.get("content"))
else: # Edit / MultiEdit
edits = ti.get("edits") or [ti]
for e in edits:
if not isinstance(e, dict):
continue
before += size(e.get("old_string"))
after += size(e.get("new_string"))
# 判準:**淨縮水的量**要夠大,比例也要夠大。兩個都要,才不會抓到小修。
# · 至少砍掉 800 個非空白字元 —— 改錯字、刪一段話都到不了
# · 而且砍掉的超過原本的 45% —— 「重寫一段」通常增減相抵
if before < 800:
print("SKIP:too-small"); raise SystemExit
cut = before - after
if cut < 800 or cut < before * 0.45:
print("SKIP:not-a-compression"); raise SystemExit
print("BLOCK:%s:%d:%d" % (os.path.basename(path), before, cut))
' 2>/dev/null || echo "SKIP:crash")
case "$VERDICT" in
BLOCK:*) ;;
*) exit 0 ;;
esac
# 至多擋一次:被擋兩次的閘會被想辦法繞過,而繞過的閘等於不存在。
SID=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try: print(json.load(sys.stdin).get("session_id", "") or "x")
except Exception: print("x")
' 2>/dev/null || printf 'x')
STAMP="/tmp/.wiki-size-guard-$SID"
[ -f "$STAMP" ] && exit 0
touch "$STAMP" 2>/dev/null || true
FILE=$(printf '%s' "$VERDICT" | cut -d: -f2)
BEFORE=$(printf '%s' "$VERDICT" | cut -d: -f3)
CUT=$(printf '%s' "$VERDICT" | cut -d: -f4)
cat >&2 <<MSG
📚 這是一次 wiki 壓縮,不是一次編輯(inkstone/ISEP#89
檔案:$FILE
這一筆會砍掉 $CUT 個字(原本 $BEFORE 個)——超過一半。
🔴 **壓縮會弄丟東西,而弄丟的當下沒有人會發現。**(票上原話)
所以壓縮不能順手做,它要留得下痕跡、而且要能逐條對帳。
改走這條(三步,機器會幫你證明沒弄丟):
python3 scripts/wiki-compress plan <檔>
python3 scripts/wiki-compress apply <檔> --ticket owner/repo#N
python3 scripts/wiki-compress verify <檔>.before-compress <檔> <搬去的檔...>
它跟手改的差別只有兩件,但這兩件正是手改做不到的:
· **--ticket 逼票號**,並把「這一版壓掉了什麼」寫進 \`.compress-log.md\`
· **verify 用內文雜湊逐條對帳**——「沒弄丟」是算出來的,不是你說的
它的做法是**只搬不改**:舊的整條原封不動搬去 archive,原檔留一份目錄。
實測 \`mistakes.md\` 7,681 行 → 1,199 行,**260 條一條都沒少,查詢快 22.5 倍**。
真的要手改(例如只是刪掉一段過期的說明):
在寫入的內容裡放一個 \`wiki-compress-ok\`,並在 commit 說明理由。
本閘**至多擋一次**,不會卡死。
MSG
exit 2