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
10 KiB
已知誤解 / 踩過的坑
這是 ISEP 自己的坑,不是 InkStoneCo 的(不轉抄,複製即 fork,fork 即漂移)。 撞到新坑就 append 一條;session 開場只推最近幾條標題(見
hooks/session-start-recall.shpush 5/5),全文在這裡。
⚠️ MISTAKE: 「環境設定」曾經拆成兩份,各自會漂
症狀: 本機在跑 InkStoneCo/.claude/(真身),雲端跑的是 generate-shell-payload.py
另外產生塞進 GitHub 私 repo 的一份(薄殼)。inkstone/InkStoneCo#57 實測:薄殼比真身
少 7 支閘,其中兩支是前一天才立的;#14 更早查到雲端 33 支 guard 一支都沒生效。
正確做法: 只留一份——ISEP 這個 repo 本身就是唯一真相源,本機與雲端裝同一個 plugin。
改動一律只改這裡,然後兩邊各自 /plugin update。不要再改
InkStoneCo/.claude/hooks/(退場中,早晚會刪)。
原因: 「一份東西兩個副本」在沒有機制強制同步的情況下必然漂移——差異不是誰疏忽,
是結構本身允許漂移發生。
日期: 2026-08-20(ISEP c263866 建立時就是為了解這個病)
⚠️ MISTAKE: hook 路徑寫死會在雲端斷
症狀: 舊版 hook 若用 $CLAUDE_PROJECT_DIR/.claude/hooks/... 或寫死的絕對路徑指向 hook
腳本自己,雲端執行時的 cwd 不是本機那個真身目錄,路徑就對不到、hook 直接失效
(正是上一條「雲端 33 支 guard 一支都沒生效」的根因)。
正確做法: hook 指自己(找到自己在哪、要 source 的其他 hook 檔)一律用官方
${CLAUDE_PLUGIN_ROOT}。腳本內部要指專案裡的檔案(如 system-dev/wiki/、
system-dev/docs/)才用 $CLAUDE_PROJECT_DIR——那些檔案本來就该住在被操作的
那個 repo 裡,跟 hook 自己的路徑是两回事,别混。
原因: 兩種路徑指的是完全不同的東西(「plugin 安裝到哪」vs「正在操作哪個專案」),
混用就是這條坑的直接原因。
日期: 2026-08-20(README「路徑規約」段記錄,ISEP 0.1.0 把 51 條 hook 路徑全部改過一輪)
⚠️ MISTAKE: 「裝好了」不等於「它在跑」
2026-08-20 實查:ISEP repo 建好、README 寫著 0.1.0、41 支閘都在裡面——
但 claude plugin list 裡根本沒有 ISEP。本機仍然在跑 InkStoneCo/.claude/,
Gitea 上一個 release tag 都沒有。
⇒ 「東西做出來了」與「有人在用它」是兩件事,而只有後者算交付。
⇒ 判準:去執行環境查它有沒有被載入(claude plugin list / claude plugin details),
不要從 repo 裡有什麼檔案去推論。
⚠️ MISTAKE: 判準寫在閘裡了,但那個閘掛在做完之後才跑的時機上
票: inkstone/InkStoneCo#55
日期: 2026-08-26
症狀: leo 一天內好幾次被丟純技術路徑選擇,當場問「今天已經好幾次問我, 為什麼 hooks 沒有攔下來?」,其中一次他直接說「這種問題不要問我, 我要的是你解決了以後給我 prod」。
實查: 總管問 leo 走的動作是 AskUserQuestion 這個工具,而
hooks.json 裡 AskUserQuestion 出現 0 次——沒有任何 matcher,它是裸的。
判準其實早就寫好了(self-drive-police.sh / self-drive-judge.sh 用的就是四題公式),
但那兩支只掛在 Stop 與 SubagentStop。
原因: 判準對了,時機錯了。 Stop 是回合結束後才跑——問題早就送到 leo 眼前、
他早就被打斷了,這時再反問 AI「你查過了嗎」,成本已經轉嫁出去了。
正確做法: 攔截點要長在那個動作發生的那一刻(PreToolUse / AskUserQuestion)。
新增 hooks/ask-user-question-guard.sh。
🔴 推廣:以後看到「規則寫了卻沒被攔下來」,先問的不是「判準對不對」,
而是「這支閘掛在哪個事件上、那個事件發生時傷害造成了沒有」。
⚠️ MISTAKE: hook 訊息用沒加引號的 heredoc,反引號會被當成命令執行
票: inkstone/InkStoneCo#55
日期: 2026-08-26
症狀: ask-user-question-guard.sh 擋下之後,stderr 冒出
line 218: system-dev/wiki/: is a directory,而訊息裡
「去查 system-dev/wiki/」和「touch /tmp/.ask-ok-<session_id>」兩行
變成空白。閘照擋 exit 2,所以測試若只看離開碼完全看不出來。
原因: 寫成 cat >&2 <<EOF(heredoc 標記沒加引號)⇒ shell 會對內容做展開,
而本 repo 的 hook 訊息慣例上大量使用反引號標路徑與指令
⇒ 每一組反引號都被當成命令替換真的去執行。
正確做法: hook 的訊息一律用 cat <<'EOF'(標記加單引號)。
需要塞變數就留 __PLACEHOLDER__,事後用 python 換掉——
不要用 sed,正體中文加上訊息裡的 /、&、\ 讓跳脫非常脆。
迴歸測試要檢查訊息內容,不能只檢查離開碼
(hooks/tests/ask-user-question-guard.test.sh 的 ⑩b 就是這一條)。
⚠️ MISTAKE: 閘只驗了規則的殼,沒驗規則本身
no-ticket-no-dispatch.sh 掛在派工的當下,檢查「派工單裡有沒有一行 【工單】owner/repo#N」。
規則的原文卻是「不准把票上已經有的東西再抄一遍進派工單⋯⋯派工單只寫票號」。
⇒ 於是可以把 40 行任務全寫在 prompt 裡、票號補一行,閘照樣放行。 ⇒ 2026-08-27 一天之內這樣做了 5 次,每一次票上都沒有那份任務。 leo:「這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?」
根因不是那支閘寫壞了,是它驗的東西比規則小。 「有沒有票號」是規則最容易機械化的那一格,所以它被實作了; 「任務有沒有真的落在票上」比較難,所以沒有——而漏掉的那格才是規則的本體。
⇒ 判準:寫完一支閘,回頭把規則原文逐句對一次,問「這一句被驗到了嗎」。
只驗得到最容易的那一格 ⇒ 那支閘會製造「有在管」的錯覺,比沒有閘更危險。
⇒ 同款:history-first/KBDB-first/stage-first/AskUserQuestion 裸奔,全是這個形狀。
修法(v0.5.0):dispatch-format-guard.sh——派工單 = 票號,多一個字都擋。
規則變得比原本更嚴,反而更好驗:判準從「內容夠不夠」變成「這一行是不是【工單】欄位」,
純結構、不用語意判官、每次結果一樣。
規約:docs/governance/dispatch-and-reply-format.md。
日期: 2026-08-27(inkstone/ISEP#30 comment 4322/4325/4327)
⚠️ MISTAKE: 「這是 session 才知道的事」被當成寫進 prompt 的正當理由
派工鐵律允許派工單帶「這個 session 才知道、票上還沒有的事」。
總管照字面理解,把 TCC 權限、main 是哪顆 commit、正本能從哪裡 clone 三件事寫進 prompt。
leo 當場:「這些為什麼不寫到票裡?」
⇒ 「票上還沒有」不是把它寫進 prompt 的理由——它就是「去把它寫上票」的指令。 ⇒ 實害(同日):總管停掉重派 3 次,前兩次的任務與 session 事實全部隨 prompt 蒸發。 票活得比任何一個 agent 久,prompt 不是。
判準:「這句話換一張票還成立嗎?」
還成立 ⇒ 共通規定(docs/governance/dispatch-and-reply-format.md §2,機器自動注入)。
只有這次成立 ⇒ 寫進那張票。兩種都不進派工單。
日期: 2026-08-27(inkstone/ISEP#30 comment 4327)
⚠️ MISTAKE: fail-closed 的閘,會把「執行裁決」這個動作本身變成不可執行
票: inkstone/ISEP#91(裁決原文在 inkstone/InkStoneCo#40 → comment 2942)
日期: 2026-08-31
症狀: leo 2026-08-16 裁定「取消 Active SDD」——任務狀態搬到 Gitea 管, SDD 只記「起初的樣子」,文件不帶任務 checkbox。 11 天過去,這個裁決一個字都沒被執行。
實查: sdd-guard.sh 是 fail-closed 的:動 code 檔時 status: active 的 SDD
不是恰好 1 份就擋(0 份也擋),路徑所在的 repo 沒有 3-specs 也擋。
⇒ 照裁決把最後那份 active 拿掉 → 變 0 份 → 任何人動任何
.ts/.py/.go 全部被擋。當時沒出事,純粹是因為剛好還剩 1 份。
而 ISEP 這個 repo 自己根本沒有 3-specs——本票開工第一件事就實測到:
改自己的 hooks/lib/*.py 當場被這支閘擋下(exit 2)。它一直在誤攔,只是沒人回報。
原因: 不是有人偷懶,是沒有人把裁決和那支閘連起來看。 裁決被讀到了、也沒人反對,但執行它的第一步會當場鎖死自己, 於是每個人都在那一步前面停下來,而**「我停下來了」不會留下任何痕跡**。
正確做法:
- 收到「取消某個制度」的裁決,第一個動作是去數還有幾支閘在執行那個制度, 並且先問那些閘是 fail-open 還是 fail-closed。 fail-closed 的那幾支決定了執行順序:先讓閘退役,再拿掉它要的東西,顛倒就鎖死。
- 退役要驗的不是「檔案刪了沒有」,是「那個擋還會不會發生」。
hooks/tests/sdd-guard-retired.test.sh把「裁決執行完之後的世界」丟給hooks.json上整組寫檔閘(清單當場從hooks.json讀,不寫死), 不准有任何一支用 SDD 當理由擋下來 ⇒ 換個檔名種回來照樣紅。
📌 一個沒人敢執行的裁決,看起來跟一個沒人記得的裁決一模一樣。 差別只有在**去問「執行它的第一步會發生什麼」**的時候才看得出來。
📌 KBDB 缺這一段: 2026-08-31 用 kbdb_search(mode='semantic') 查
「SDD 生命週期/單一活性/取消 Active SDD」0 命中(最接近的是 2026-08-09
一張講 SDD × Gitea 整合摩擦的卡,那是裁決之前)。⇒ 這條開發史還沒進 KBDB。