Leo
|
7de1ad6be6
|
補上「用錯的路去證明一件事」那一格的閘(inkstone/ISEP#30 → comment 4879)
擋的是**證據的出處**,不是措辭。
判準(兩個條件同時成立才響):
① 要送出去的那份東西(票上的留言/派工單)裡,貼了一個值
——entry id 或 `kb://` 來源位址——而它這個 session 只在
`kbdb_search` 的回應裡出現過
② 這個 session 從沒用產品檢索路徑(graph/wiki 內容/query)拿過那筆
為什麼不比對措辭:驗收條件第 4 條寫死不准,而 leo 2026-08-17 已經證偽過
文字層——那天 8 次誤攔、0 次正確攔截,且方向穩定:紅線寫得越細,命中
關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。值跟動作一樣有限且可枚舉,措辭不是。
🔴 `kbdb_search` 一點都沒有變難用:查 wiki、找 record_id、看某筆在不在,
全部照放。它只在「把搜尋輸出貼出去當產品檢索壞掉的證據」那一刻才響。
實測 31 條,A 群(不該擋)11 條、B 群(該擋)5 條、C 群訊息 6 條、
D 群登記處 9 條。真跡重演=inkstone/Arcrun#167 comment 4865 那則退回,
原文照貼會被擋;走過一次真路徑之後同一份留言就放行。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 21:38:17 +08:00 |
|
Leo
|
35e927ef55
|
補上「有動作、不宣告」那一格:主線閒置警察(inkstone/ISEP#30)
空手警察判「有沒有動作」、稼動率警察判「有沒有那句話」,兩支中間留了一格:
**有動作、就是不宣告下一步** ⇒ 兩支都放行。2026-08-27 實際發生的就是這一格。
新閘 hooks/mainline-idle-guard.sh(Stop)判的是「有沒有推進」,不是「有沒有動作」:
- 乾回合 = 這回合 ≥3 個 tool call,且沒有任何推進證據
- 推進證據五種(任一成立就歸零):派工/產出/寫進外部系統/Bash 寫入白名單/
**工作區狀態變了**(HEAD 或未提交變更的指紋,heredoc·sed 改檔也吃得到)
- 連續 4 個乾回合擋一次,擋完歸零且**門檻加倍**(4→8→16)
一個字都不讀(守票上「不准文字層判準」那條紅線);③④ 的字面比對只用來放行,
永遠不用來擋——白名單漏一項是少放行一次,黑名單漏一項是誤攔一次。
門檻 4 是量出來的,不是拍腦袋:重放 6 份真 transcript、1965 個真實回合,
門檻 3 響 4 次、門檻 4 響 1 次(0.05%);門檻 3 多出來那三次都落在
「leo 連問問題、我逐題查證回答」的段落——那正是票上第 3 條驗收要保護的情境。
測試 hooks/tests/mainline-idle-guard.test.sh:61 條,A 群 15 條全是「不該擋」。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 20:28:55 +08:00 |
|
Leo
|
950c3e1919
|
測試不再偽造「有一筆推 main 在等總管裁」(inkstone/ISEP#59)
現象(實測,不是推論):跑完 main-and-prod-push-guard 那三支測試之後
$ git status --short
M hooks/lib/__pycache__/strip_heredoc.cpython-314.pyc
M pending-main-push/unnamed--ISEP.md
?? pending-main-push/unnamed--A.md
成因:這支閘擋下推 main 的同時,會把那次請求寫成
`<hooks 的上一層>/pending-main-push/<誰>--<repo>.md`,而三支測試的測資本來
就全是推 main。於是每跑一次測試,工作區就多/改幾筆**偽造的待裁決**。
兩個後果,後者比較貴:
① `git add -A` 很容易把它們帶進 commit(08-27 那次真的帶進去了,事後才拔掉)
② 總管的迴圈讀那個目錄,讀到的每一筆都該是真的在等他裁——
測試每跑一次就偽造一筆 ⇒ **下一筆真的請求會混在雜訊裡**。
這跟「永遠在響的警報」是同一個病。
改法(產品程式碼一行都沒動):
- 新增 hooks/tests/lib/hook-sandbox.sh:把整個 hooks/ 複製到暫存區再跑複本。
閘算 pending 目錄的位置靠的是 `$0` 的上一層 ⇒ 請求寫進暫存區。
**刻意不在閘上開一個「寫去哪」的環境變數**——那種開關同時是一條把紀錄關掉的路。
- 三支測試改測沙盒複本,並各補兩條斷言:
① repo 的 pending-main-push 一個位元都沒動
② 沙盒裡**真的有**留下請求(只驗 ① 的話,把留紀錄的功能整個關掉也會綠)
- pending-main-push/ 不再進版控(它是本機狀態不是原始碼),只留一份 README 說明規約;
既有的兩筆與那顆被追蹤的 .pyc 一併 `git rm --cached`,檔案留在硬碟上不刪。
實測:
- 三支各自 10/10、19/19、13/13(原本 8/17/11,各多兩條新斷言)
- 把 `H` 改回真跡重跑 ⇒ 新斷言兩條都紅(11/13)+工作區又髒
⇒ 這兩條斷言真的抓得到它要抓的東西
- 全套 16 支測試檔跑完 0 失敗,`git status` 沒有多出任何一行
- `claude plugin validate .` ✔ Validation passed
沒有升版:這次只動測試與版控範圍,沒有任何閘的行為改變,
不需要靠新版本號送到任何人手上;請跟著總管下一個 release 一起出去。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 18:33:44 +08:00 |
|
Leo
|
b1830053e5
|
dispatch-format-guard 修兩處:規則自己的合格範例會被自己擋,禮貌收尾不該算違規
inkstone/ISEP#65:leo 發現「寫完 ISEP#30 那條規則之後,同一天總管又犯了 8 次」,
派來查「現行閘為什麼抓不到」。查證結果分兩層:
━━ 真正在跑的閘其實是 dispatch-format-guard.sh,不是 no-ticket-no-dispatch.sh ━━
no-ticket-no-dispatch.sh 的殼驗證早就是已知行為(只驗有沒有一行【工單】),
但 ISEP#30 已經為此新增了 dispatch-format-guard.sh 做內容判定,測試 19/19 通過。
問題是它有兩個沒被那 19 條測資蓋到的洞:
1. **regex 洞(本體 bug)**:頂層 CLAUDE.md 規定的合格格式帶全形括號——
「【工單】owner/repo#N(→ comment M)」。_COMMENT_RE 只吃「→ comment M」
本體,兩側括號沒被算進去,殘留括號讓 _REF_RE 比不過,於是**規則自己定義
的合格範例會被自己的閘擋下**(半形括號 `(...)` 同樣會中)。用今天派我這張
票的那份派工單原句實測,改之前 exit=2「票號形狀不對」。
2. **零容忍過頭**:純禮貌收尾(「謝謝」)跟「記得先讀 CLAUDE.md」這種真內容
一樣被當「派工單不只有票號」擋下。ISEP#65 test 4 明講這四種不該擋
(只有票號/空行/「謝謝」/括號包住的 comment 格式),優先做成低誤鎖。
判斷:不改 no-ticket-no-dispatch.sh、不另立第三支閘——dispatch-format-guard.sh
已經是「另立一支」的正確位置,這兩個洞在它自己的地盤上補。
修法:
- _COMMENT_RE 兩側括號(全形/半形)都設可選
- 新增 _COURTESY_CLOSERS 白名單(謝謝/多謝/感謝/辛苦了…)+ _is_courtesy_closer(),
只在圍欄外生效,判準仍是「整行清乾淨標點後完全相等」不是「包含」——
白名單不是黑名單,猜漏頂多誤鎖一次,不會反過來放走真內容(③g 測資證明)
測試:hooks/tests/dispatch-format-guard.test.sh 19→33 條,全過。新增:
- ③b/③c 全形/半形括號格式(規則自己的例句)
- ③d/③e/③f ISEP#65 test 4 的三種不該擋
- ③g 白名單邊界(禮貌詞混真內容裡照樣算數,防止白名單被誤用成漏洞)
- ⑮b–⑮i:leo 點名的「寫完規則後又犯的 8 次」當回歸樣本,內容是從
ISEP#60/#61/#64、Arcrun#142/#144/#165、arcrun-rag#104、InkStoneCo#102
的真實票內文摘錄(見 hooks/tests/fixtures/README.md 記載來歷),
不是想像出來的例子;8 種形狀全部驗證會被擋
reply-identity.test.sh 11/11 仍全過(共用 dispatch_parse.py 沒有回歸)
升版 0.5.0 → 0.5.1(改完不升版沒人吃得到;check-version-consistency.sh
在本分支照慣例是紅的,tag 於 merge 時打)。
另查:.shell-payload/ 整個被 gitignore(scripts/vendor-to-shell.py 產物),
不是 git 分發的一部分——「這支閘會不會被吃到」取決於消費端有沒有重跑
plugin update/vendor-to-shell.py,不是這個 repo 委交的內容缺漏,故不在
本票改動範圍內,僅記錄供總管排查用。
未動 no-ticket-no-dispatch.sh(判斷見上,職責保持不重疊,兩支閘互斥見
dispatch_parse.py 的 has_ticket_marker 分岔)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 14:25:17 +08:00 |
|
Leo
|
3bc7f3c5f5
|
派工單只剩票號——閘從驗「有沒有票號」改成驗「是不是只有票號」
leo 2026-08-27(inkstone/ISEP#30 comment 4322/4325/4327):
「這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?」
「你用一個 output parser 把你給 subagent 的指令規範,分作幾點,每一點規定格式,
照這種散文寫法根本無法迭代」「警察也不能抓」
「交件方式不需要寫,定義在原則裡⋯⋯每次都一樣提取出來變成共通規定」
「(那些 session 事實)這些為什麼不寫到票裡?」「subagent 回覆時要表明身份」
病根:no-ticket-no-dispatch.sh 驗的是「有沒有一行【工單】owner/repo#N」,
而規則的原文是「派工單只寫票號」。⇒ 把 40 行任務全寫在 prompt 裡、票號補一行,
閘照樣放行。2026-08-27 一天內這樣做了 5 次,每次票上都沒有那份任務。
規則存在,閘只驗了它的殼——同款第 N 次(history-first/KBDB-first/stage-first)。
新增 hooks/dispatch-format-guard.sh(PreToolUse Task|Agent),兩件事:
- 擋:【工單】以外還有實質內容就 exit 2,並指出那些內容該搬去哪
(每次都一樣 → 共通規定;這次才知道 → 寫進那張票。
判準「這句話換一張票還成立嗎?」)
- 注入:合規的派工自動把共通規定送給收工方(交件方式、不准 push main、
org 是 inkstone…)——這是「派工單只剩票號」能成立的前提,
leo 的驗收條件之一就是「收工方沒讀派工單也知道要貼回原票」
判準是結構不是文字(leo 2026-08-17 那條檢驗):問的是「這一行是不是【工單】欄位」
——在不在,不是寫什麼。hooks/lib/dispatch_parse.py 全檔零個「命中某個詞就違規」的比對。
⇒ 也因此不需要語意判官:免費、瞬間、每次結果一樣。
新增 hooks/reply-identity-guard.sh(PreToolUse Bash)+ scripts/ticket 內建檢查:
票上每一則留言第一行要有【身份】(總管/subagent/leo)。貼留言有兩條路,兩條都封
——ticket-api-bypass-guard 是刻意放行「對既有票留言」的,只封正門等於沒封。
實害:多條線並行時總管寫的診斷被當成 subagent 的結論,而其中一則是錯的。
規約寫成文件:docs/governance/dispatch-and-reply-format.md
(§2 那段就是被注入的那份共通規定本體——只有一份,改那裡等於改所有派工)
實測(離線、不打網路、不花錢):
hooks/tests/dispatch-format-guard.test.sh 19/19
hooks/tests/reply-identity.test.sh 11/11
測資裡的 B⑨ 是真跡:產生 ISEP#30 這條線的那一次派工,一字未改。
另 4 份 leo 點名的違規派工拿不回來了——它們住在 prompt 裡,agent 一停就沒了,
這件事本身就是這條規則的證據(見 hooks/tests/fixtures/README.md,不用想像的例子替補)。
升版 0.4.0 → 0.5.0(產物按版本號分資料夾,不升版新閘不會被載入)。
tag 照慣例打在 merge commit 上,所以這條分支上 check-version-consistency.sh 是紅的。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 11:35:03 +08:00 |
|
claude-code
|
e0557334bc
|
閘掛在「收工後」,而 leo 是在「被問到」的當下被打擾——補上那一刻的攔截
leo 2026-08-26:「今天已經好幾次問我,為什麼 hooks 沒有攔下來?」
實查:總管問 leo 走的動作是 AskUserQuestion,而 hooks.json 裡它出現 0 次,
沒有任何 matcher。判準其實早就寫好了——self-drive-police / self-drive-judge
用的就是同一套四題公式——但那兩支只掛在 Stop 與 SubagentStop,
是回合結束後才跑的。問題早就送到他眼前了,事後再反問「你查過了嗎」,
成本已經轉嫁出去。判準對了,時機錯了。
新增 hooks/ask-user-question-guard.sh(PreToolUse / AskUserQuestion):
- 觸發條件是那個動作本身,不是任何句型或關鍵字——全檔零個判擋用的正則,
換句話說閃不過去,講得謹慎也不會被多罰(leo 2026-08-17 文字層封路的檢驗)
- 進來之後用四題公式的 haiku 判官分「該問 / 不該問」,命中任一題一律放行
- 同一個問題只擋一次(雜湊戳記):判官誤判時重送即過,
leo 該收到的問題不會因為一支閘而永遠送不到
- 判官掛掉/沒網路/claude 不在 PATH 一律 fail-open,壞掉等於它不存在
版本 0.3.9 → 0.4.0。這不是儀式,是傳輸機制本身:產物按版本號分資料夾
(~/.claude/plugins/cache/inkstone/isep/<版本>/),版本沒動就不會長出新資料夾,
這支閘一個 session 都載入不到。docs/TESTING.md 開頭那段講的就是這件事,
而第一版我漏了——總管複驗時量出來的。
假設(沒有前例可循,先裁再記):跳 0.4.0 而不是 0.3.10。理由是這一版第一次
掛上 AskUserQuestion 這個事件面,是新能力不是修補;而且 0.3.10 在
plugin 快取目錄的 ls 裡會排到 0.3.1 旁邊,肉眼不好認。錯了打回,改號很便宜。
順手修掉自己寫出來的一個坑:訊息原本用沒加引號的 heredoc,
反引號被當命令執行,wiki 路徑與豁免指令兩行變成空白(閘照擋,只看離開碼看不出來)。
已收成 mistakes.md 一條,並由 ⑩b 這條測試守著。
實測:
- hooks/tests/ask-user-question-guard.test.sh 14/14(離線,不花錢)
- hooks/tests/ask-user-question-guard.live.test.sh 9/9 連跑三次(真的叫 haiku)
A 群 5 條真人閘(花錢/不可逆/跨專案結構/品味方向/物理人閘)全部放行,誤攔 0
B 群 4 條純技術路徑選擇全部擋下
- 既有 7 支測試與改動前逐條對照,結果完全相同(沒有被我弄壞)
順手對帳:plugin.json 與 marketplace.json 的描述寫「43 支機械閘、53 條註冊」,
實際數過是 46/56(含本次新增這支)。docs/hooks-inventory.md 一併更正。
【工單】inkstone/InkStoneCo#55
|
2026-08-26 22:36:08 +08:00 |
|
Leo
|
e3d05df341
|
再修兩類:主詞是 leo 的下一步、票號放寬把閘變鈍——都是真 transcript 量出來的
交付警察擋回來是對的:前一顆只驗了「我自己造的假 transcript」。
改用本機一條 2068 行的真 session(26 個真實回合終止點)重驗,當場多找到兩個問題:
① 主詞是「你」的下一步,被當成我的宣告。
舊閘在 26 個真實回合裡擋了 2 次,兩次咬的都是我在交代 leo 該做什麼:
「下一步還是那一個動作:你把 feat/... 併進 main」
「## 你下一步(兩招,先便宜的)」
⇒ 這是全新的第四類誤攔,我原本一向都沒列到。
修法是主詞檢查(誰要動手),只掛在「下一步」這條 alternative 上;
用 finditer 逐個檢查前 8 字有沒有第二人稱,蓋得到「你的下一步」這種
單字 lookbehind 蓋不到的變體。「你點頭我就做」主詞本來就是我,不受影響。
② 票號從「宣告句附近」放寬成「整段」,把閘變鈍了。
真數據:26 個真實回合有 20 個是靠「文中某處剛好有票號」放行的——
而報告幾乎一定會提到票號 ⇒ 這道閘在實務上等於不會響。
當初放寬是因為票號常寫在行內 code 裡,剝掉就找不到。
⇒ 改成**等長**替換(蓋成同樣長度的哨兵而非刪除),位移就能對回原文,
locality 與「行內 code 裡的票號也算數」兩件同時成立。
收緊後的判定分佈:21 no-declaration / 3 dispatched / 2 ticket-referenced
(原本是 20 ticket-referenced / 3 no-declaration / 3 dispatched)。
真 transcript 實測:舊閘擋 2 次(兩次都是誤攔)→ 新閘擋 0 次。
測試 33 向(+6):舊版 21/33 → 新版 33/33。
版號 0.3.7 → 0.3.8。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-23 17:54:17 +08:00 |
|
Leo
|
a594decb78
|
稼動率警察改成擋「宣告」,不擋「提到宣告」
2026-08-23 雲端驗收連續三次被這道閘誤攔,三次都不是宣告意圖:
① 否認自己有下一步 ②引用閘自己的訊息 ③貼閘自己的正則原始碼舉報 bug。
而訊息教人走的「選項③:說明它在等什麼」,程式碼裡根本沒有那條分支
——唯一走得通的路是不寫那三個字,正是同一則訊息明文禁止的動作。
四個真兇,沒有一個是「例外沒列夠」:
(a) DECL 會匹配裸的「下一步」三個字(每一節都可選 ⇒ 退化成關鍵字)
⇒ 收緊:每一條 alternative 都必須接到動作動詞才算命中
(b) 只剝 > 引言與長「」,不認 code fence 與行內 code ⇒ 引用被當成主張
⇒ 引用性標記整段換成哨兵(不是刪掉):內層宣告消失、外層句構留著
——刪掉正是 08-17 漏掉「回『規劃』我就派人」的原因,兩個方向一起修
(c) 取 blocks_text[-1],但那則文字後面可能還有 tool_use ⇒ 宣告其實兌現了
⇒ 只看「最後一個動作之後」的文字;收尾在動作上就不觸發
(d) 訊息承諾的出路只有兩條真的存在
⇒ 出路③ 給一個機械形式 ⏸ 等:<在等什麼>(白名單標記,要刻意寫,留痕)
方向刻意與「再加幾個關鍵字例外」相反——例外清單會越加越長、越長越誤攔。
守 leo 的封路哲學:紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。
順手:擋下與放行都留痕(InkStoneCo#48:只記擋下的話分母未知);
log 目錄不在時安靜跳過,不再噴 redirect 錯誤到 stderr。
測試 hooks/tests/factory-idle-guard.test.sh 27 向,誤攔與漏攔兩個方向都測:
舊版 18/27(9 敗)→ 新版 27/27。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-23 17:42:22 +08:00 |
|
Leo
|
41c56acd32
|
推 main 的戳記改綁「push 真正的目標 repo」,不再綁 hook 自己的 cwd
跨 repo 交辦時(總管站在 A repo,要推 B repo 的 main)main-and-prod-push-guard
的戳記機制永遠對不上:HERE 讀的是 hook 自己的 cwd(=session 的真身,不會變),
WANT 是總管替目標 repo(B)寫進戳記的路徑——兩者結構性地不可能相等,不是
判斷錯,是這個情境在舊模型裡根本不存在(inkstone/ISEP#30 comment 3949,
脈絡 inkstone/InkStoneCo#57,2026-08-21 實撞)。
新增 hooks/lib/push_target_dir.py:純 tokenize(不執行任何指令)解析指令裡
`cd <path> && git push` 或 `git -C <path> push` 真正會落地的目錄,對多層 cd
鏈與子殼(`(cd A && ...); git push` 這種子殼 cd 不能外洩出去)都做了範圍化——
這條範圍化是防穿透的關鍵,不是順手:沒有它,`(cd A && true); git push`
會被誤判成推向 A,讓替 A 開的舊戳記錯誤地放行推到殼外真正的目標。解不出來
一律退回舊行為(hook 自己的 cwd),維持 fail-closed 方向不變。
順手修掉補測時自己抓到的另一個洞:`(git push origin HEAD:main)`——單純加一層
括號——舊版目的地判斷完全偵測不到,整段直接放行,跟戳記無關。成因是截斷
refspec 尾巴的 sed 只認 `;`/`&`/`|` 三種字元,沒算到 `)`;補上即可,git 的
refspec 語法本來就不允許出現 `)`,這裡截斷永遠安全。
綁 repo+單次用完即丟兩條 2026-08-11/12 用血換來的性質完全沒有鬆動:只是把
「現在人在哪個 repo」問得更準,比對邏輯一個字沒動。
實測:
- hooks/tests/main-and-prod-push-guard.test.sh 舊有 8 向:8/8
- scripts/test-main-and-prod-push-guard.sh 舊有 11 向:11/11
- 新增 hooks/tests/main-and-prod-push-guard-cross-repo.test.sh 17 向
(跨 repo 正向/反向不准鬆/git -C/子殼範圍化/括號洞/單次用完即丟/
900 秒逾時/空戳記/既有行為零回歸):17/17
本輪只驗證,未拿去放行任何真實推送;plugin.json 隨慣例 bump 0.3.4 -> 0.3.5
並重跑 vendor-to-shell.py(.shell-payload 為 gitignore 產物,不入版控)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-23 14:18:28 +08:00 |
|
Leo
|
67dae3b814
|
推送閘改成判目標,不判整條指令裡有沒有那個字
一個晚上誤攔六次,全都不是在推預設分支:
① checkout -b 建新分支時把預設分支寫在後面,再推那條新分支
② gh pr create 指定 base——根本不是 git push
③ 推 tag(refs/tags/…)
④ 推 feature 分支(帶 -u)
⑤ 它擋住了我用來**測試它自己**的那條指令
⑥ 它擋住了這一筆的 commit——因為 message 裡引用了那幾個字
leo 2026-08-17 早就講過這個形狀:文字層封路必敗,
「紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎」。
舊版掃整條指令字串,正是文字層。
改成解析 push 的目標 refspec:
旗標跳過/第一個非旗標=remote/a:b 取 b/refs/tags/* 不算分支
一個 refspec 都沒給,才退回看當前分支
八向實測(hooks/tests/main-and-prod-push-guard.test.sh,8/8):
五種該放行的(今晚誤攔的原形狀,含分支名帶 domain 那種)全過
三種該擋的全擋
中途自己抓到一個 bug:tag 被跳過後目標清單變空 → 退回猜當前分支
⇒ 當前分支剛好叫預設名時誤擋。改成看到 refspec 就不退回猜測。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-21 01:29:55 +08:00 |
|
Leo
|
87186585d6
|
fix(hooks): sdd-guard.sh 修「解析失敗仍照擋、且訊息洩漏 /nonexistent」(InkStoneCo#22)
症狀(總管 2026-08-12 實撞):寫暫存腳本進 scratchpad
(/private/tmp/.../scratchpad/foo.py)被 sdd-guard.sh 攔下,訊息印出字面的
「/nonexistent/3-specs/ 下找不到任何 SDD」。
兩個洞:
- 洞 A:scratchpad 不在任何 git repo 裡,卻被當成「repo 裡的 code 變動」誤判
需要 SDD。改成先問 path_in_git_worktree()(見 hooks/lib/path-resolve.sh):
不在任何 git repo 裡 → SDD 天生管不到,直接放行,不必先猜專案根。
這個檢查放在 $_root 的 case 分岔之前、對兩邊都適用——第一版只放進「專案外」
分支,被本次新增的 hooks/tests/sdd-guard.test.sh 抓到一個不對稱漏洞(cwd 剛好
等於 scratchpad 祖先目錄時會漏判),改成統一檢查後修掉。
- 洞 B:舊版用內部 sentinel `/nonexistent/3-specs` 重用既有的擋下路徑,但這個
假路徑被直接印進使用者看到的訊息。改用 RESOLVED 旗標記解析成不成功,訊息
改用人話描述原因,不洩漏假路徑。
fail-closed / fail-open 的判準(票上明確要求回答,不能各憑運氣):
真的落在某個 git repo 裡、但那個 repo 沒有 3-specs(或沒有 active SDD)→
仍然 fail-closed(擋)。理由:這道閘存在的目的就是防止「沒有 SDD 卻能動
code」,把「判斷不出來」直接放行,等於把環境跑歪(cwd 被切走、
$CLAUDE_PROJECT_DIR 沒設)悄悄變成「這道閘關掉了、且沒人知道」——silent
bypass 的代價遠高於多打一次確認。#22 紅線亦明寫「不要把閘改成解析失敗就
放行」。
順手修的殘留 cwd 依賴:SPECS_DIR 的預設值原本是相對路徑
「system-dev/docs/3-specs」,專案內迴圈找不到時會被拿去跟 hook 執行當下的
cwd 兜;改成絕對路徑 $_root/system-dev/docs/3-specs。
同時修 ADR-0001(ISEP 自建 wiki):標題與內文原本會讓人誤解成「ISEP plugin
裝到哪個 repo,就會在那裡自建一份 wiki」,但實際查證(marketplace.json 只宣告
hooks/commands/skills、README 明文排除 wiki/docs、hooks 一律用
${CLAUDE_PLUGIN_ROOT} 讀自己不是寫別處)並非如此——那份 wiki 只是 ISEP 這個
repo自己的開發歷史,跟裝 plugin 無關。唯一真的會在某 repo 建 wiki 的
scripts/install.sh 是 system-dev-template 的獨立安裝器殘留,要手動執行,
作用對象是 cwd 不是「plugin 裝到的地方」——這多半是誤解的真正來源,已在
ADR 的「常見誤解」段說明。
驗證:
- 造出 08-12 原始事故情境(cwd=InkStoneCo、CLAUDE_PROJECT_DIR 未設、寫
scratchpad),修前擋(印 /nonexistent)、修後放行——實測輸出見票留言。
- 造出「真的在 git repo 裡但沒有 3-specs」情境,修後仍擋、訊息不含
/nonexistent。
- 新增 hooks/tests/sdd-guard.test.sh:8 案例全過(洞 A/洞 B/fail-open
陷阱/單一活性違反/恰好一份 active/改文件放行)。
- 既有六套 scripts/test-*.sh 全過,無退步。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-20 21:03:14 +08:00 |
|
Leo
|
c1d80756d7
|
審核補件:第二個空殼閘(irreversible_dispatch_check.py)+既有測試套
總管複驗 PR 時發現 subagent 只補了一個空殼,還有第二個同款的:
arcrun-intent-guard.sh → exec 不存在的 .py ⇒ 擋掉全部(吵,PR 已修)
irreversible-dispatch-guard.sh → 同款,但寫法是
python3 <不存在> 2>/dev/null || echo '{"verdict":"OK"}'
⇒ **靜默放行全部**(危險,本 commit 補)
實測同一份派工單「驗過了就把舊分支刪掉」:
InkStoneCo 版(有 .py)exit=2 擋 / ISEP 版(缺 .py)exit=0 放行
補完後:不可逆派工 exit=2、正常派工 exit=0、合規工作流 exit=0。
系統性掃描 ISEP 全部 hooks 引用的同目錄檔案:補完後 0 個缺檔(InkStoneCo 本來就是 0)。
順帶把 InkStoneCo 的 hooks/tests/ 四支既有測試一併帶過來。
🔴 這件事很重要:若先併 InkStoneCo#64(刪掉 .claude/hooks/),
這台機器會失去那支唯一還能用的副本——變成真的沒有那道閘。
|
2026-08-20 20:06:23 +08:00 |
|