Files
ISEP/hooks/tests/isep-presence-beacon.test.sh
T
Leo 044ef289fe ship-check 送到得了任何 session:內容原樣搬過來,歸屬寫下來讓機器去比(inkstone/ISEP#122 → comment 6096)
## 這一份是原樣搬過來的,一個字都沒改

skills/ship-check/SKILL.md ← inkstone/InkStoneCo `bd52fa3`(PR #114,**還沒併**)
  搬完 md5 f565e4df1b18cc8aaf057f5fa64a9e15,跟來源逐位元組相同(cmp 通過)
  595 → 651 行;描述裡多了部落格/GitHub 鏡像/n8n/pages deploy 這些觸發詞
  ——舊描述那幾個詞**一個都沒有**,所以「我要發一篇部落格文章」那個情境
    根本觸發不到它,而那正是 leo 這次要解的問題。

## 為什麼不是「改成指針」也不是「ISEP 不再自帶」

雲端的 project dir 是**薄殼根**,InkStoneCo 只是它底下的一個目錄
(docs/governance/cloud-wiring.md 記著這件事)
⇒ `InkStoneCo/.claude/skills/` 在雲端**不會被載入**,只有 plugin 這一份會
⇒ ISEP 不帶全文 = 雲端拿不到 = 又變回「薄殼是真身的子集」,
   而那正是這個 repo 成立時要殺掉的病(README 開頭)。
所以 ISEP 必須帶全文,但它是**搬運工不是作者**:內容改在真相源,這裡只放複本。

## 歸屬不能靠人記得——實查證明兩個方向都會發生

skills/ 與 commands/ 那 9 個檔案在 0.1.0(c263866)從 InkStoneCo 複製過來一次,
之後**再也沒有同步過**(git log 只有那一顆)。到今天已經分家兩個,方向相反:

  skills/ship-check/SKILL.md   InkStoneCo 651 行 / ISEP 595 行   ← 那邊新
  commands/sdd-check.md        ISEP 81 行 / InkStoneCo 65 行     ← 這邊新
                               (InkStoneCo 那份還在教 ISEP#91 已退役的「唯一 active SDD」)

⇒「ISEP 一定比較新」與「InkStoneCo 一定比較新」兩句都是錯的。
  這是票上第 4 題「為什麼會有兩份」的答案:不是誰忘了同步,
  是**兩份都會被就地編輯**,而沒有任何東西會喊一聲。

## 所以機制是「寫下來 + 讓機器去比」,而且不新開一支閘

- docs/file-ownership.tsv —— 哪一份是真相源、取自哪顆 commit、當時的 sha256
- 比對長在**既有的信標**上(isep-presence-beacon.sh → hooks/lib/beacon_report.py):
  它的 ② 已經在做「同名而內容不同」這件事,只是**只掃 scripts/**。
  這次把 skills/commands/agents 一起納進去(②b),
  再加一格 ②c 用 sha256 單邊驗——**雲端沒有 InkStoneCo 可以比,那是唯一還作數的檢查**。
  🔴 刻意不開新閘:ISEP 最常見的錯是重造一支平行的閘
  (docs/governance/dispatch-and-reply-format.md §1.6 記著同一課)。

判準是「檔名一樣**而內容不同**」,同步過的不吵——誤攔比漏擋嚴重。
全部只講不擋(SessionStart 本來就不該擋人)。

## 實跑(這棵樹,真的 InkStoneCo)

  🟡 會自動載入的東西兩邊各有一份,而且內容不同:
    - skills/ship-check/SKILL.md ↔ .claude/skills/ship-check/SKILL.md
      (真相源=inkstone/InkStoneCo:… ⇒ 內容改在那裡,改完原樣搬進 ISEP、更新 commit/sha256、升版)
    - commands/sdd-check.md ↔ .claude/commands/sdd-check.md
      (真相源=ISEP 這一份 ⇒ 專案那份是舊複本,同步過去或刪掉它)

兩個方向各講對了自己的出路。

測試:hooks/tests/isep-presence-beacon.test.sh 14 → 26 條,全綠、全離線。
README/docs/hooks-inventory.md/docs/TESTING.md(A18 改 26 條、新增 A29)都跟著改了。
六個數字在這棵樹上實數:61 支閘/84 條註冊/7 位工人/7 支命令/2 支 skill/49 支腳本
——這次沒有增減,但仍然是數出來的,不是沿用上一版。

(本 commit 也帶著上一顆「逃生門」那件事的兩列文件:hooks-inventory 第 255 列與 TESTING A29。)

🔴 待總管定版:改了會被載入的東西就要升版,否則 plugin update 是 no-op。
🔴 InkStoneCo 那半不是我做的(不同 repo):那邊的 .claude/skills/ 與 .claude/commands/
   還留著兩份舊複本,該同步或刪掉;在那之前信標會每次開場點名它們。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:12:22 +08:00

196 lines
11 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
# isep-presence-beacon.sh 的迴歸測試(inkstone/ISEP#90 ②③)
#
# 信標的價值全在鑑別力,所以這支要同時證明兩件相反的事:
# ① 該報的三件真的報得出來(版本落差/舊複本遮蔽正門/退役機制的殘骸)
# ② **不管加多少報告,那行綠色信標永遠在**——它消失=leo 會判定這個 session 零閘。
# 所以連 python 掛掉、網路不通、訊息裡有引號,都要還有一行合法 JSON。
#
# 🔴 全程離線(ISEP_BEACON_SKIP_NET=1,版本用 ISEP_FAKE_MAIN_VERSION 造),
# 造假的 plugin root 與 project dir 都在 TMP 底下,不碰真 repo。
set -u
HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/isep-presence-beacon.sh}"
REAL_HOOKS="$(cd "$(dirname "$HOOK")" && pwd)"
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
export ISEP_BEACON_SKIP_NET=1 ISEP_BEACON_CACHE_DIR="$TMP"
PASS=0; FAIL=0; N=0
# mkplugin <版本> → 造一份假的 plugin root(只放信標真的會讀的東西)
mkplugin() {
d="$TMP/plugin-$1"; mkdir -p "$d/.claude-plugin" "$d/hooks/lib" "$d/scripts"
printf '{"name":"isep","version":"%s"}\n' "$1" > "$d/.claude-plugin/plugin.json"
printf '{"hooks":{"Stop":[{"hooks":[{"command":"hooks/a.sh"},{"command":"hooks/b.sh"}]}]}}\n' \
> "$d/hooks/hooks.json"
cp "$HOOK" "$d/hooks/$(basename "$HOOK")"
cp "$REAL_HOOKS/lib/beacon_report.py" "$d/hooks/lib/beacon_report.py"
printf '#!/bin/sh\necho real\n' > "$d/scripts/ticket"
# inkstone/ISEP#122:會自動載入的東西(skillcommandagent)也要造得出來
mkdir -p "$d/skills/demo" "$d/commands" "$d/agents" "$d/docs"
printf 'PLUGIN 版的 skill\n' > "$d/skills/demo/SKILL.md"
printf 'PLUGIN 版的 command\n' > "$d/commands/demo.md"
printf 'PLUGIN 版的 agent\n' > "$d/agents/demo.md"
printf '%s\n' "$d"
}
# mkown <plugin root> <列...> → 寫一份 docs/file-ownership.tsv(欄位用真正的 TAB
mkown() {
d="$1"; shift; mkdir -p "$d/docs"
{ printf '# 測試用\n'; for row in "$@"; do printf '%s\n' "$row"; done; } \
| sed 's/|/\t/g' > "$d/docs/file-ownership.tsv"
}
# run <plugin root> <project dir> → 印出 systemMessageJSON 壞掉就印 __BADJSON__
run() {
CLAUDE_PLUGIN_ROOT="$1" CLAUDE_PROJECT_DIR="$2" bash "$1/hooks/$(basename "$HOOK")" 2>/dev/null \
| python3 -c 'import json,sys
try: print(json.load(sys.stdin)["systemMessage"])
except Exception: print("__BADJSON__")'
}
check() { # check <說明> <輸出> <該出現|!不該出現>...
desc="$1"; out="$2"; shift 2; N=$((N+1)); ok=1; why=""
for w in "$@"; do
case "$w" in
"!"*) if printf '%s' "$out" | grep -qF -- "${w#!}"; then ok=0; why="不該出現卻出現了:${w#!}"; fi ;;
*) if ! printf '%s' "$out" | grep -qF -- "$w"; then ok=0; why="少了:$w"; fi ;;
esac
done
if [ "$ok" = 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
else printf ' ❌ %s —— %s\n' "$desc" "$why"; printf '%s\n' "$out" | sed 's/^/ /'; FAIL=$((FAIL+1)); fi
}
echo "── 基本盤:那行綠色信標永遠在 ────────────────────────────────"
P=$(mkplugin 0.9.0); J="$TMP/proj-clean"; mkdir -p "$J"
out=$(run "$P" "$J")
check "① 乾淨環境:只有一行信標,版本與閘數都在" "$out" \
"🟢 ISEP v0.9.0 已載入" "2 支閘" "!🔴" "!🟡" "!__BADJSON__"
echo "── ①版本落差 ────────────────────────────────────────────────"
out=$(ISEP_FAKE_MAIN_VERSION=0.9.0 run "$P" "$J")
check "② 載入版=main 版 → 不吵" "$out" "🟢 ISEP v0.9.0" "!落後"
P39=$(mkplugin 0.3.9)
out=$(ISEP_FAKE_MAIN_VERSION=0.9.0 run "$P39" "$J")
check "③ 載入 0.3.9、main 0.9.0 → 要說它落後(雲端那個現場)" "$out" \
"🟢 ISEP v0.3.9 已載入" "落後 ISEP main" "0.9.0" "inkstone/ISEP#67"
out=$(ISEP_FAKE_MAIN_VERSION=0.1.0 run "$P" "$J")
check "④ 載入版比 main 新 → 說「沒發版」,不說「落後」" "$out" "比 ISEP main 新" "!落後 ISEP main"
echo "── ②舊複本遮蔽正門 ──────────────────────────────────────────"
J2="$TMP/proj-shadow"; mkdir -p "$J2/scripts"
printf '#!/bin/sh\necho OLD\n' > "$J2/scripts/ticket"
out=$(run "$P" "$J2")
check "⑤ 專案裡有內容不同的同名腳本 → 要點名(ticket 那件)" "$out" \
"舊複本" "scripts/ticket"
J3="$TMP/proj-same"; mkdir -p "$J3/scripts"; cp "$P/scripts/ticket" "$J3/scripts/ticket"
out=$(run "$P" "$J3")
check "⑥ 複本內容一模一樣(同步過的)→ 不吵" "$out" "!舊複本"
J4="$TMP/proj-body"; mkdir -p "$J4/InkStoneCo/scripts"
printf '#!/bin/sh\necho OLD\n' > "$J4/InkStoneCo/scripts/ticket"
out=$(run "$P" "$J4")
check "⑦ 雲端排法:複本在 \$TOP/InkStoneCo/ 底下也要抓到" "$out" \
"舊複本" "InkStoneCo/scripts/ticket"
echo "── ③退役機制的殘骸 ──────────────────────────────────────────"
J5="$TMP/proj-orphan"; mkdir -p "$J5/.claude/pending-verification" "$J5/.claude/hooks"
out=$(run "$P" "$J5")
check "⑧ plugin 的 hooks/scripts 完全沒提到的目錄 → 報殘骸" "$out" \
"已退役機制的產物" ".claude/pending-verification"
check "⑨ Claude Code 自己的目錄(hooks)不算殘骸" "$out" "!.claude/hooks"
# 現行機制的產物不該被報:讓 plugin 的某支 hook 提到它
mkdir -p "$J5/.claude/pending-main-push"
printf '#!/bin/sh\n# 寫到 pending-main-push/\n' > "$P/hooks/x.sh"
out=$(run "$P" "$J5")
check "⑩ hooks 裡有東西提到它 → 那是現行機制的產物,不報" "$out" "!pending-main-push"
# 🔴 這條守的是第一版真的犯過的 bug:內層迴圈的變數名跟外層的 `d` 撞,
# 第二個名字之後全部被靜靜跳過 ⇒ 真的存在的殘骸一聲不吭地消失。
mkdir -p "$J5/.claude/verified-claims"
out=$(run "$P" "$J5")
check "⑪ 同一個 .claude 底下有多個殘骸 → 每一個都要報(變數撞名的迴歸)" "$out" \
".claude/pending-verification" ".claude/verified-claims"
echo "── 最重要:加了報告也不准讓信標消失 ──────────────────────────"
# python 掛掉(把 report 換成一定會爆的內容)→ 仍要有一行合法 JSON 的信標
PB=$(mkplugin 0.9.0); printf 'raise SystemExit(3)\n' > "$PB/hooks/lib/beacon_report.py"
out=$(run "$PB" "$J")
check "⑫ 組訊息的 python 整支掛掉 → 退回乾淨的那一行,不是沉默" "$out" \
"🟢 ISEP v0.9.0 已載入" "!__BADJSON__"
rm -f "$PB/hooks/lib/beacon_report.py"
out=$(run "$PB" "$J")
check "⑬ 連 report 檔都不見 → 一樣要有信標" "$out" "🟢 ISEP v0.9.0 已載入" "!__BADJSON__"
# 訊息裡有引號/換行時 JSON 仍要合法(舊版是 printf 手拼的,這正是它會壞的地方)
J6="$TMP/proj-quote"; mkdir -p "$J6/scripts"
printf '#!/bin/sh\necho "OLD"\n' > "$J6/scripts/ticket"
out=$(run "$P" "$J6")
check "⑭ 報告內容含引號 → JSON 仍然合法" "$out" "🟢 ISEP" "!__BADJSON__"
P2=$(mkplugin 1.0.0) # ⑫⑬ 會刪掉 $P 的 report,這一組要自己一份
echo "── ②b 會自動載入的東西也有分身(inkstone/ISEP#122)──────────────"
# 🔴 這一組守的是 ship-check 那件:plugin 帶著舊描述,而舊描述在「我要發部落格文章」
# 的情境根本不會被觸發 ⇒ 那個 session 永遠載不到出貨流程,而且不會有任何症狀。
JS="$TMP/proj-skill"; mkdir -p "$JS/.claude/skills/demo"
printf '專案版的 skill(比較舊)\n' > "$JS/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JS")
check "⑮ 專案有同名 skill 而內容不同 → 要點名" "$out" \
"會自動載入的東西兩邊各有一份" "skills/demo/SKILL.md" ".claude/skills/demo/SKILL.md"
check "⑯ 歸屬表沒登記 → 誠實說未定,不要替人猜方向" "$out" "歸屬未定"
JS2="$TMP/proj-skill-same"; mkdir -p "$JS2/.claude/skills/demo"
cp "$P2/skills/demo/SKILL.md" "$JS2/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JS2")
check "⑰ 內容一模一樣 → 不吵(誤攔比漏擋嚴重)" "$out" "!會自動載入的東西兩邊各有一份"
JC="$TMP/proj-cmd"; mkdir -p "$JC/.claude/commands"
printf '專案版的 command(比較舊)\n' > "$JC/.claude/commands/demo.md"
out=$(run "$P2" "$JC")
check "⑱ command 也要掃(sdd-check 那件)" "$out" "commands/demo.md" ".claude/commands/demo.md"
JA="$TMP/proj-agent"; mkdir -p "$JA/.claude/agents"
printf '專案版的 agent(比較舊)\n' > "$JA/.claude/agents/demo.md"
out=$(run "$P2" "$JA")
check "⑲ agent 也要掃" "$out" "agents/demo.md" ".claude/agents/demo.md"
JCL="$TMP/proj-cloud"; mkdir -p "$JCL/InkStoneCo/.claude/skills/demo"
printf '專案版的 skill(比較舊)\n' > "$JCL/InkStoneCo/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JCL")
check "⑳ 雲端排法:真身在 \$TOP/InkStoneCo/ 底下也要抓到" "$out" \
"InkStoneCo/.claude/skills/demo/SKILL.md"
echo "── 歸屬表要講得出「往哪個方向修」──────────────────────────────"
PO=$(mkplugin 1.0.1)
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|-"
out=$(run "$PO" "$JS")
check "㉑ 真相源=InkStoneCo → 說「內容改在那裡」並給得出路徑" "$out" \
"真相源=inkstone/InkStoneCo:.claude/skills/demo/SKILL.md" "內容改在那裡" "!歸屬未定"
mkown "$PO" "skills/demo/SKILL.md|inkstone/ISEP|-|-|-"
out=$(run "$PO" "$JS")
check "㉒ 真相源=ISEP → 說「專案那份是舊複本」" "$out" \
"真相源=ISEP 這一份" "舊複本" "!歸屬未定"
echo "── ②c 沒有專案那一份也要驗得出來(雲端唯一作數的檢查)────────────"
# 雲端的 project dir 是薄殼,沒有 InkStoneCo 可以比 ⇒ 只剩「檔案 vs 表上的 sha256」
SHA=$(shasum -a 256 "$PO/skills/demo/SKILL.md" | cut -d" " -f1)
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|$SHA"
out=$(run "$PO" "$J")
check "㉓ sha256 對得上(而且專案那份不存在)→ 一個字都不說" "$out" \
"🟢 ISEP v1.0.1 已載入" "!跟歸屬表對不上" "!會自動載入的東西兩邊各有一份"
printf '被就地改過\n' >> "$PO/skills/demo/SKILL.md"
out=$(run "$PO" "$J")
check "㉔ 檔案被就地改過 → 就算沒有專案那份,也要抓得到" "$out" \
"跟歸屬表對不上" "skills/demo/SKILL.md" "abc1234"
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|-"
out=$(run "$PO" "$J")
check "㉕ 表上 sha 欄是 \`-\` → 這一格閉嘴(還沒登記不是錯)" "$out" "!跟歸屬表對不上"
echo "── 加了這兩格也不准讓信標消失 ────────────────────────────────"
out=$(run "$P2" "$JS")
check "㉖ ②b 有報告時,那行綠色信標仍在且 JSON 合法" "$out" \
"🟢 ISEP v1.0.0 已載入" "!__BADJSON__"
echo
printf '通過 %s 條,失敗 %s 條(共 %s 條)\n' "$PASS" "$FAIL" "$N"
[ "$FAIL" = 0 ] || exit 1