c7af690c2b
leo 2026-08-16 在 inkstone/InkStoneCo#40 → comment 2942 裁定「取消 Active SDD」, 11 天沒被執行。原因不是有人偷懶,是**執行它的第一步會鎖死自己**: sdd-guard.sh 是 fail-closed 的 · 動 code 檔時 status: active 不是恰好 1 份 → 擋(0 份也擋) · 路徑所在的 repo 沒有 3-specs → 也擋 ⇒ 照裁決把最後那份 active 拿掉 → 變 0 份 → 任何人動任何 .ts/.py/.go 全被擋。 而 ISEP 這個 repo 自己就沒有 3-specs——本票開工第一件事實測到: $ echo '{"tool_name":"Edit","tool_input":{"file_path":".../hooks/lib/dispatch_parse.py"}}' \ | bash hooks/sdd-guard.sh 🚫 SDD 協議攔截:… 找不到任何 SDD exit 2 **它一直在誤攔 ISEP 自己,只是沒人回報。** ── 這一版做了什麼 ──────────────────────────────────── · hooks/sdd-guard.sh 刪除,hooks.json 取消註冊(85 → 84 條,只少這一條) · commands/sdd-check.md 改寫:SDD 只記「起初的樣子」,任務本體在 Gitea 票; 找不到 SDD 不再是停下來的理由,找不到票才是 · agents/inkstoneco-hand.md 拿掉「任何時刻只允許一份 status: active」那條紅線 · hooks/lib/path-resolve.sh 只加註解:它的唯一 caller 走了,但別順手刪 (#22 學到的東西住在裡面,十幾支閘還在用它要修的那個寫法) ── 迴歸測試:測的不是「檔案刪了沒」,是「那個擋還會不會發生」── hooks/tests/sdd-guard-retired.test.sh(通過 8/失敗 0,離線): 把「裁決執行完之後的世界」(沒有 3-specs 的 repo、兩份 status: active 的 repo) 丟給 hooks.json 上**整組** Write|Edit|MultiEdit 的閘——清單當場從 hooks.json 讀、 不寫死——不准有任何一支用 SDD/3-specs 當理由擋下來。 ⇒ 日後有人換個檔名把同一個形狀種回來,這支照樣紅。 🔴 判準刻意不是「一支閘都不准擋」:同組還住著跟 SDD 無關、且看 session 狀態 決定擋不擋的閘(history-first/subagent-first)。把它們算失敗,這支測試會在別人 改別的東西時無故變紅,紅久了就沒人看——誤攔比漏擋更該修,對測試一樣成立。 **紅的證明**:把 sdd-guard 暫時復原(檔案+註冊)重跑 → 通過 1/失敗 7, 四條行為格全部指名 sdd-guard.sh。已還原。 ── 自己跑過的 ────────────────────────────────────── · hooks/tests/sdd-guard-retired.test.sh 通過 8/失敗 0 · 逐支點名 hooks.json(README「裝什麼」那道指令) 84 條,對 main 做集合差: 少了 sdd-guard.sh × 1,多出 0 支,其餘一支不差 · 盤點數字全部在這棵樹上實數,不是加減推: ls hooks/*.sh|wc -l = 61(原 62) grep -c '"command":' hooks/hooks.json = 84(原 85) agents 7/commands 7/skills 2/scripts 48(皆未變動) · 盤點表對帳三格(hooks-inventory 自己寫死的那三道):三格皆無輸出 · scripts/check-version-consistency.sh ✅ 0.16.2 一致 · claude plugin validate . ✅ Validation passed · hooks/tests/ 全部離線測試 24 支重跑:本次改動 0 退步 (dispatch-format-guard 40/52、prod-write-guard 18/19-fail、 stage-before-prod-guard 9/7-fail、另 3 支需帶參數/建不起沙盒—— **六支在 gitea/main 上逐支重跑結果一模一樣,是既有狀態不是本次造成**) ── 沒做、也不該由這張票做的 ───────────────────────── · inkstone/InkStoneCo 那半(拿掉 active SDD + 刪 CLAUDE.md「單一活性鐵律」段) ——那是 inkstoneco-hand 的 repo;而且順序上本來就要等這一版出去、 兩邊 /plugin update 之後才動得,先拿掉就鎖死 · checkbox 分診(掛 inkstone/InkStoneCo#49,票上明寫不要另開票) · 版本號:待總管定版 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
155 lines
7.2 KiB
Bash
Executable File
155 lines
7.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# sdd-guard 退役的迴歸測試(inkstone/ISEP#91)
|
||
#
|
||
# ── 這支在守什麼 ────────────────────────────────────────────────────
|
||
# leo 2026-08-16 在 inkstone/InkStoneCo#40 裁定「**取消 Active SDD**」:
|
||
# 任務狀態搬到 Gitea 管,SDD 只記「起初的樣子」。
|
||
# 但 `sdd-guard.sh` 是 **fail-closed** 的——
|
||
# 動 code 檔時,active SDD 不是「恰好 1 份」→ 擋(0 份也擋)
|
||
# 路徑所在的 repo 沒有 3-specs → 也擋
|
||
# ⇒ 照裁決把 active SDD 拿掉,會把「動任何 .ts/.py/.go」整個鎖死。
|
||
# 這就是那個裁決 11 天沒人敢執行的真正原因。
|
||
#
|
||
# 所以退役的順序是「**先讓閘退役,再拿掉 active SDD**」,而這支測的是第一步做完了沒有。
|
||
#
|
||
# 🔴 它測的不是「檔案刪掉了沒有」,而是**那個擋還會不會發生**:
|
||
# 把整組 `Write|Edit|MultiEdit` 的 PreToolUse 閘,拿去撞「裁決執行完之後的世界」
|
||
# (一個沒有 3-specs 的 repo、以及一個有兩份 active SDD 的 repo),一支都不准擋。
|
||
# ⇒ 有人日後用別的檔名把同一個形狀種回來,這支照樣會紅。
|
||
#
|
||
# 用法:hooks/tests/sdd-guard-retired.test.sh
|
||
# 🔴 全程在 TMP 底下建假 repo,跑完自己清;不動任何真 repo、不打網路。
|
||
|
||
set -u
|
||
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
REPO_ROOT="$(cd "$HOOKS_DIR/.." && pwd)"
|
||
TMP=$(mktemp -d)
|
||
trap 'rm -rf "$TMP"' EXIT
|
||
|
||
PASS=0; FAIL=0
|
||
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
|
||
bad() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
|
||
|
||
# ── ① 閘本體與它的註冊都不在了 ────────────────────────────────────
|
||
echo "── ① 退役:檔案與註冊 ──"
|
||
|
||
if [ -e "$HOOKS_DIR/sdd-guard.sh" ]; then
|
||
bad "hooks/sdd-guard.sh 還在(應已刪除)"
|
||
else
|
||
ok "hooks/sdd-guard.sh 已刪除"
|
||
fi
|
||
|
||
if grep -q 'sdd-guard' "$HOOKS_DIR/hooks.json"; then
|
||
bad "hooks.json 還註冊著 sdd-guard(照裁決做會鎖死所有 code 寫入)"
|
||
else
|
||
ok "hooks.json 已無 sdd-guard 註冊"
|
||
fi
|
||
|
||
# hooks.json 要仍然是合法 JSON,而且別的閘一支都沒被順手弄掉
|
||
# (README:合錯的時候「語法合法、閘卻不見了」,不會有任何東西喊一聲)
|
||
REGISTERED=$(python3 - "$HOOKS_DIR/hooks.json" <<'PY'
|
||
import json, sys
|
||
d = json.load(open(sys.argv[1]))
|
||
print(sum(len(g.get("hooks", [])) for ev in d["hooks"].values() for g in ev))
|
||
PY
|
||
) || REGISTERED=""
|
||
if [ -n "$REGISTERED" ]; then
|
||
ok "hooks.json 仍是合法 JSON(註冊 $REGISTERED 條)"
|
||
else
|
||
bad "hooks.json 解析失敗"
|
||
fi
|
||
|
||
# ── ② 真正的驗收:裁決執行完之後,動 code 檔不能被擋 ──────────────
|
||
echo "── ② 行為:把「拿掉 active SDD 之後的世界」丟給整組寫檔閘,一支都不准擋 ──"
|
||
|
||
# 這兩個假 repo 就是舊 sdd-guard 一定會擋下的兩種狀態:
|
||
# A. 完全沒有 3-specs(=裁決執行完的樣子,也是 ISEP 自己現在的樣子)
|
||
# B. 兩份 status: active(=舊的「單一活性鐵律」違反)
|
||
REPO_NO_SDD="$TMP/repo-no-sdd"
|
||
mkdir -p "$REPO_NO_SDD/src"
|
||
git init -q "$REPO_NO_SDD"
|
||
|
||
REPO_MULTI="$TMP/repo-multi-active"
|
||
mkdir -p "$REPO_MULTI/system-dev/docs/3-specs/a" "$REPO_MULTI/system-dev/docs/3-specs/b" "$REPO_MULTI/src"
|
||
git init -q "$REPO_MULTI"
|
||
printf -- '---\nstatus: active\n---\n# A\n' > "$REPO_MULTI/system-dev/docs/3-specs/a/design.md"
|
||
printf -- '---\nstatus: active\n---\n# B\n' > "$REPO_MULTI/system-dev/docs/3-specs/b/design.md"
|
||
|
||
# 撈出所有掛在 Write|Edit|MultiEdit 上的 PreToolUse 閘(照 hooks.json 的實況,不寫死清單)
|
||
mapfile -t WRITE_HOOKS < <(python3 - "$HOOKS_DIR/hooks.json" <<'PY'
|
||
import json, sys
|
||
d = json.load(open(sys.argv[1]))
|
||
for g in d["hooks"].get("PreToolUse", []):
|
||
m = g.get("matcher", "") or ""
|
||
if "Write" in m or "Edit" in m:
|
||
for h in g.get("hooks", []):
|
||
print(h["command"].split("/")[-1])
|
||
PY
|
||
)
|
||
|
||
if [ "${#WRITE_HOOKS[@]}" -eq 0 ]; then
|
||
bad "撈不到任何 Write|Edit 的 PreToolUse 閘——測試本身失效了,先修這裡"
|
||
fi
|
||
|
||
# 判準:擋下來的理由**是不是 SDD**。
|
||
#
|
||
# 🔴 這裡刻意不寫成「一支閘都不准擋」——同一組 matcher 上還住著好幾支跟 SDD 無關、
|
||
# 而且看 session 狀態決定擋不擋的閘(`subagent-first-guard`/`history-first-guard`…)。
|
||
# 把它們算成失敗,這支測試就會在別人改別的東西時無故變紅,
|
||
# 紅久了就沒人看——`docs/TESTING.md` 開頭那句「誤攔比漏擋更該修」對測試一樣成立。
|
||
# 所以失敗的定義是:**有閘擋下來,而且它擋的理由指向 SDD/3-specs**。
|
||
# 其餘的擋只印出來給人看,不判失敗。
|
||
probe() { # probe <說明> <file_path>
|
||
local desc="$1" path="$2" sdd_blocked="" other_blocked=""
|
||
local payload out rc
|
||
payload=$(python3 -c "import json,sys;print(json.dumps({'tool_name':'Write','tool_input':{'file_path':sys.argv[1],'content':'export const x = 1;\n'}}))" "$path")
|
||
for name in "${WRITE_HOOKS[@]}"; do
|
||
[ -x "$HOOKS_DIR/$name" ] || continue
|
||
out=$(printf '%s' "$payload" | "$HOOKS_DIR/$name" 2>&1)
|
||
rc=$?
|
||
[ "$rc" -eq 2 ] || continue
|
||
if printf '%s' "$out" | grep -qE 'SDD|3-specs'; then
|
||
sdd_blocked="$sdd_blocked $name"
|
||
else
|
||
other_blocked="$other_blocked $name"
|
||
fi
|
||
done
|
||
if [ -n "$sdd_blocked" ]; then
|
||
bad "$desc —— 仍被 SDD 理由擋下:$sdd_blocked"
|
||
else
|
||
ok "$desc"
|
||
[ -n "$other_blocked" ] && echo " (另有與 SDD 無關的閘擋下,不算失敗:$other_blocked)"
|
||
fi
|
||
}
|
||
|
||
# 刻意讓 $_root(CLAUDE_PROJECT_DIR/pwd)跟這些假 repo 對不上——
|
||
# 舊 sdd-guard 在這個情境下走的正是 fail-closed 那條分支。
|
||
unset CLAUDE_PROJECT_DIR
|
||
cd "$TMP" || exit 1
|
||
|
||
probe "repo 沒有 3-specs(裁決執行完的樣子)→ 寫 .ts 不被擋" "$REPO_NO_SDD/src/x.ts"
|
||
probe "repo 沒有 3-specs → 寫 .py 不被擋" "$REPO_NO_SDD/src/x.py"
|
||
probe "兩份 status: active(舊「單一活性」違反)→ 寫 .ts 不被擋" "$REPO_MULTI/src/x.ts"
|
||
|
||
# ISEP 自己就是「沒有 3-specs 的 repo」——退役前,改自己的 .py 當場被擋。
|
||
probe "ISEP 自己的 hooks/lib/*.py → 不被擋(退役前這一格是紅的)" "$REPO_ROOT/hooks/lib/dispatch_parse.py"
|
||
|
||
# ── ③ 會被載入的那幾份,不能再有人讀到「只准一份 active SDD」 ──────
|
||
echo "── ③ 條文:載入面不再宣告單一活性鐵律 ──"
|
||
# 只查**會被載入**的那幾份(commands / agents / skills / hooks 腳本)。
|
||
# docs/ 與 wiki/ 不查——它們得說得出「退役了什麼」,不然這件事沒有歷史。
|
||
STALE=""
|
||
while IFS= read -r f; do
|
||
grep -qE '只(准|允許)一份|單一活性' "$f" && STALE="$STALE $f"
|
||
done < <(find "$REPO_ROOT/commands" "$REPO_ROOT/agents" "$REPO_ROOT/skills" -name '*.md' 2>/dev/null
|
||
find "$REPO_ROOT/hooks" -maxdepth 1 -name '*.sh' 2>/dev/null)
|
||
if [ -n "$STALE" ]; then
|
||
bad "這些載入面還在宣告「只准一份 active SDD」:$STALE"
|
||
else
|
||
ok "commands/agents/skills/hooks 都不再宣告單一活性鐵律"
|
||
fi
|
||
|
||
echo
|
||
echo "結果:通過 $PASS / 失敗 $FAIL"
|
||
[ "$FAIL" -eq 0 ] || exit 1
|