adb7009b67
信標原本只證明「有一份 plugin 載入了」,不證明「載入的是哪一份」—— 2026-08-27 雲端載的是 0.3.9、main 是 0.9.0,中間差 7 個 release,而它照樣是綠的。 那個看不見的落差的實際後果:0.3.9 裡還活著兩支 v0.9.0 已整支刪掉的 hook, 於是 .claude/pending-verification/ 被清掉之後又長回來。 三格(都只報告、不擋、拿不到答案就閉嘴): · 版本落差:匿名讀 ISEP main 的 plugin.json(D20 判準下屬於「讀」),快取 6 小時、 逾時 6 秒。落後就講清楚差幾版與怎麼修,並指向 inkstone/ISEP#67。 · 舊複本遮蔽正門:plugin 的 scripts/* 在專案樹(含雲端的 $TOP/InkStoneCo/)裡 有同名但**內容不同**的複本就點名。實例=InkStoneCo/scripts/ticket 的取 token 邏輯還停在「只認名叫 gitea 的 remote」,而雲端的 Gitea 叫 origin ⇒ 一律死在「拿不到 gitea token」⇒ 人被逼去繞過正門直接打 API, 而那正是 ticket-api-bypass-guard.sh 在防的事。 · 退役機制的殘骸:.claude/ 底下的目錄,若 plugin 的 hooks/scripts 一個字都沒提到 ⇒ 產生它的東西已經不在這一份裡了。判準是「plugin 現在還認不認得它」, **不是關鍵字黑名單**(leo 2026-08-17 已證明那條路 8 次誤攔、0 次正確攔截)。 訊息改由 hooks/lib/beacon_report.py 序列化:三段報告含引號與換行, 用 shell 內插拼 JSON 一個引號就會讓整行信標消失——而那正是「零閘狀態」的長相。 fail-open:python 掛掉、檔案不見、網路不通,一律退回原本那一行乾淨的信標。 測試 hooks/tests/isep-presence-beacon.test.sh:14/14(全離線)。 第 ⑪ 條守的是寫這支時真的犯過的 bug(內層迴圈變數跟外層的 d 撞名, 第二個殘骸之後全部被靜靜跳過)——把那個 bug 放回去,該條立刻轉紅。
70 lines
4.1 KiB
Bash
Executable File
70 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# isep-presence-beacon.sh — SessionStart:報出「ISEP 真的載入了,幾版、幾支閘」
|
||
#
|
||
# 這不是閘,是**信標**。存在的理由是 2026-08-20 的雲端事故:
|
||
# 雲端 session 的閘全滅,而三個驗證步驟全部回綠——因為它們沒有鑑別力
|
||
# (`git tag` 在三支閘的白名單裡;`Skills(9)/Hooks(5)` 剛好是薄殼自己的 .claude/ 產生的數字)。
|
||
#
|
||
# 🔴 鑑別力就是這支的全部意義:
|
||
# 這行出現 ⇒ plugin 一定載入了(因為它自己就住在 plugin 裡)
|
||
# 這行不見 ⇒ plugin 沒載入,那個 session 是零閘狀態
|
||
# ——沒有第三種情況,也沒有「剛好也會過」的巧合。
|
||
set -uo pipefail
|
||
|
||
ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
||
[ -n "$ROOT" ] || exit 0
|
||
|
||
VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
||
"$ROOT/.claude-plugin/plugin.json" 2>/dev/null | head -1)"
|
||
VER="${VER:-未知}"
|
||
# 🔴 數「真的被註冊的」,不是數目錄裡有幾個 .sh(leo 的雲端驗收 2026-08-23 抓到):
|
||
# 舊寫法 `ls hooks/*.sh` 把 `pre-write-guard.template.sh`(樣板,不是閘)
|
||
# 與兩支沒掛註冊的輔助檔一起算進去 ⇒ 報 45,實際註冊 42。
|
||
# 這個數字是 leo 判斷「這個 session 有沒有閘」的唯一介面——**多報就是假綠**。
|
||
# (查過歷史:本檔自 daa1674 建立以來只有那一版,沒有別的分支修過這段。)
|
||
GATES="$(grep -oE 'hooks/[a-zA-Z0-9._-]+\.sh' "$ROOT/hooks/hooks.json" 2>/dev/null \
|
||
| sort -u | wc -l | tr -d ' ')"
|
||
case "$GATES" in
|
||
''|*[!0-9]*|0)
|
||
GATES="$(ls "$ROOT"/hooks/*.sh 2>/dev/null | grep -cv '\.template\.sh$' | tr -d ' ')" ;;
|
||
esac
|
||
|
||
# 🔴 講清楚「這一份是誰」(leo 2026-08-23 的雲端驗收卡在這裡整整一輪):
|
||
# 同一台機器上 ISEP 可能有兩份——marketplace 裝的 plugin 快取,
|
||
# 與 repo 裡 vendor 進去的複製本。**兩份都會印這一行**,而版本號一樣時分不出誰在說話。
|
||
# 「閘到底從哪一份走」正是決定「另一份能不能拆」的唯一判準
|
||
# ⇒ 讓路徑自己講:快取在 plugins/cache/ 底下,vendor 的在 repo 的 .claude/ 底下。
|
||
# (查過歷史:本檔只有 daa1674 建立、b1f399f 改閘數兩輪,沒有人試過標示來源,也沒有被否決的方案。
|
||
# KBDB 語意搜尋這段開發史:0 命中,該餵。)
|
||
case "$ROOT" in
|
||
*/plugins/cache/*) SRC="plugin(marketplace 裝的)" ;;
|
||
*/.claude/isep*) SRC="vendor(repo 裡的複製本)" ;;
|
||
*) SRC="來源不明" ;;
|
||
esac
|
||
|
||
# ── 交給 lib/beacon_report.py 組訊息 ─────────────────────────────────
|
||
#
|
||
# 🔴 為什麼改成走 python 而不是繼續用 printf 拼 JSON(inkstone/ISEP#90):
|
||
# 下面要多報三件事(版本落差/舊複本遮蔽正門/退役機制的殘骸),
|
||
# 而那三段的內容含引號與換行。**用 shell 內插拼 JSON,一個引號就會把整包 JSON 弄壞,
|
||
# 而壞掉的 JSON 讓信標整行消失** —— 那正是本檔要偵測的「零閘狀態」的長相。
|
||
# ⇒ 訊息本身可以變複雜,但**序列化這件事不准手拼**。
|
||
#
|
||
# 🔴 fail-open:python 掛掉、網路不通、什麼都不准讓這行消失。
|
||
# 信標消失=leo 會判定這個 session 沒有閘(本檔第 8 行寫的判準),
|
||
# 所以組不出加料版就退回原本那一行乾淨的訊息。
|
||
MSG="🟢 ISEP v${VER} 已載入(${GATES} 支閘|來源:${SRC}|${ROOT})"
|
||
FALLBACK="{
|
||
\"systemMessage\": \"${MSG}\",
|
||
\"hookSpecificOutput\": {
|
||
\"hookEventName\": \"SessionStart\",
|
||
\"additionalContext\": \"${MSG}。這行是 ISEP plugin 自己發的——看得到它就表示閘真的生效了。若某個 session 從頭到尾沒有這行,那個 session 是零閘狀態,先修 plugin 再做事,不要用『跑得動』當證據。\"
|
||
}
|
||
}"
|
||
|
||
OUT="$(python3 "$ROOT/hooks/lib/beacon_report.py" "$VER" "$GATES" "$SRC" 2>/dev/null || true)"
|
||
case "$OUT" in
|
||
\{*) printf '%s\n' "$OUT" ;;
|
||
*) printf '%s\n' "$FALLBACK" ;;
|
||
esac
|