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
56 lines
3.6 KiB
Bash
56 lines
3.6 KiB
Bash
# hooks/lib/path-resolve.sh — 共用:判斷一個檔案路徑「歸不歸某個 git repo 管」。
|
||
# 不是獨立掛的閘(沒進 hooks.json),給其他 PreToolUse 閘 `source` 用的函式庫。
|
||
#
|
||
# 背景(inkstone/InkStoneCo#22):sdd-guard.sh 曾經把 scratchpad 暫存檔
|
||
# (`/private/tmp/.../scratchpad/foo.py`)誤判成「repo 裡的 code 變動」而擋下——
|
||
# 因為它只會「猜專案根($CLAUDE_PROJECT_DIR 或 cwd)+往上找 3-specs」,
|
||
# 猜錯或猜不到時,找不到 3-specs 就一律當「找不到 SDD」擋下,連「這條路徑根本不在
|
||
# 任何 repo 裡、SDD 這件事天生管不到它」都沒判斷過。
|
||
#
|
||
# path_in_git_worktree 提供一個不必先猜對專案根的判法:直接問 git
|
||
# 「這個路徑在不在某個 repo 的工作樹裡」。不必窮舉暫存區的路徑關鍵字(/tmp、scratchpad…),
|
||
# 任何真的不在 git repo 裡的路徑,一律視同「這是暫存/非受管檔案」。
|
||
#
|
||
# 同一個 `${CLAUDE_PROJECT_DIR:-$(pwd)}` 猜根目錄寫法,實測(2026-08-20)還出現在:
|
||
# component-guard.sh、factory-idle-guard.sh、github-contact-guard.sh、
|
||
# history-first-guard.sh、main-and-prod-push-guard.sh、no-ticket-no-dispatch.sh、
|
||
# not-my-branch-guard.sh、release-tag-guard.sh、skill-deploy-drift-guard.sh、
|
||
# stage-before-prod-guard.sh、unpushed-police.sh、wiki-first-police.sh。
|
||
# 另有 empty-handed-stop-guard.sh、issue-status-autoflip.sh 直接寫 `$CLAUDE_PROJECT_DIR`
|
||
# (無 `:-` fallback)——
|
||
# (claim-verify-police.sh/subagent-claim-worksheet.sh 已於 ISEP#60 整組移除,
|
||
# 它們守的東西改由 Gitea 原生三格承接,見 ISEP#59/PR #58)
|
||
# 這批在該變數未設時行為又不一樣,同一個病的另一種長相。
|
||
# 這些全部沒有本檔「先確認到底在不在 repo 裡」的判斷;本檔先在 sdd-guard.sh 落地,
|
||
# 其餘要不要跟進、要不要改用這支共用函式,另案處理,不在本票(#22)範圍內一次改完。
|
||
#
|
||
# ⚠️ **`sdd-guard.sh` 已於 `inkstone/ISEP#91` 退役**(執行 leo「取消 Active SDD」的裁決),
|
||
# 所以本檔目前**沒有任何 caller**——但它不是死 code:上面那份清單裡的十幾支閘全都還在用
|
||
# 「猜專案根」那個寫法,本檔就是要給它們用的解法。**不要因為沒人 source 就順手刪掉**,
|
||
# 刪掉等於把 #22 學到的東西一起丟了。
|
||
# (本段只加註解,不動下面任何一行邏輯——`8718658` 那輪修的判定行為原封不動。)
|
||
#
|
||
# 用法:
|
||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/path-resolve.sh"
|
||
# if ! path_in_git_worktree "$FILE_PATH"; then
|
||
# # 不在任何 git repo 裡 ⇒ 這支閘通常管不到,多半該放行
|
||
# fi
|
||
|
||
# path_in_git_worktree <path>
|
||
# 回傳 0=這個路徑落在某個 git 工作樹裡;1=不在任何 git repo 裡(含路徑本身不存在的情況)。
|
||
# 做法:從路徑的目錄部分開始,往上找到「第一個真的存在的祖先目錄」,
|
||
# 對那個目錄問 `git rev-parse --is-inside-work-tree`。
|
||
# 為什麼要往上找存在的祖先,不能直接對 dirname 問:
|
||
# 要在 repo 裡建一個還沒建立的子目錄下的新檔案時,dirname 也不存在,
|
||
# 若不往上找,`git -C <不存在的目錄>` 會直接失敗 ⇒ 誤判成「不在 repo 裡」
|
||
# ⇒ 放行了本來該擋的東西(fail-open 的洞,不是這支函式該製造的)。
|
||
path_in_git_worktree() {
|
||
local p="$1" d
|
||
d=$(dirname -- "$p")
|
||
while [ ! -d "$d" ] && [ "$d" != "/" ]; do
|
||
d=$(dirname -- "$d")
|
||
done
|
||
[ -d "$d" ] || return 1
|
||
git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||
}
|