## hooks.json:這次 git 自動合成功了,而那正是最危險的情況
`hooks.json` 是 JSON——**合錯了語法照樣合法、閘卻不見了,不會有任何東西喊一聲。**
所以「沒有衝突」不等於「沒掉東西」,一律再做兩件事:
① 逐支點名(指令已寫進 README「裝什麼」那段)→ 82 條,60 支各自幾條全部列出
② 跟**兩個父節點**做集合對照:
merged 82 = main 79 + 我的 3
從 main 掉了:0 條
從我這邊掉了:0 條
兩邊都沒有卻冒出來的:0 條
`#105` 三支與我的兩支同時在,位置也對:
roster-guard.sh PreToolUse Agent / Task
diagnosis-evidence-guard.sh PreToolUse Bash
investigate-first-stamp.sh PostToolUse Agent|Task / SendMessage
overdue-nag-guard.sh SessionStart
wiki-size-guard.sh SessionStart / PreToolUse Write|Edit|MultiEdit
(總管提醒的 `roster-guard` vs `mainline-focus-guard` 那種「同一陣列位置兩邊各一支」
這次沒有再發生衝突,但兩支都在,各 2 條。)
## 三處文字衝突:兩邊都留,數字重數
- plugin.json 版本吃 main 的 **0.15.0**(總管已定版,**我沒有動它**);
描述欄補上 main 新增的「7 位有名字的工人」,三個數字重數
- README.md 吃 main 的新表格結構(多了 agents/commands/skills/scripts 四列),
數字重數;並把累積成**四列**的 `hooks/` 收成一列
- hooks-inventory 標頭被貼成兩份、「一句話結論」被貼成**三份**(數字還互相打架),
各收成一份;版本註記兩邊都留
## 盤點(全部在合併後的樹上實數,不是加減推的)
ls hooks/*.sh | wc -l 58 → 60
grep -c '"command":' hooks/hooks.json 79 → 82
ls agents/*.md | wc -l 7
ls -p scripts | grep -v / | wc -l 48
未掛進 hooks.json 的 .sh 3(與本頁原本寫的一致,重數過)
📌 抓到的漂移(每次併都有一個,這次三個):
① hooks-inventory 的標頭 ×2、一句話結論 ×3
② README 的 `hooks/` 列 ×4,其中兩列是舊數字
③ plugin.json 描述欄寫「49 支腳本」,而用本頁寫死的那道指令實數是 48
## 測試(合併後全部重跑,帶對參數)
hooks/tests 23 支全綠:ask-user-question 14、countdown 20、diagnosis-evidence 26、
dispatch-format 52、factory-idle 33、gate-ok 17、gitea-arm-check 16、
presence-beacon 14、cross-repo 19、main-and-prod-push 10、mainline-focus 24、
mainline-idle 61、milestone-account 22、**overdue-nag 36**、pr-verdict 52、
prod-write 37、reply-identity 11、roster 24、sdd 8、search-is-not-proof 31、
stage-before-prod 16、unpushed-police 10、**wiki-compress 25**
scripts/test-* 13 支全綠。
claude plugin validate . ✔ passed;check-version-consistency ✅ 0.15.0 = v0.15.0。
兩支新 SessionStart hook 併完實跑過,閘判定探針對 v0.15.0 的 prod-write-guard 回 pass。
🔴 跑之前清 `CLAUDE_CODE_CHILD_SESSION`;四支要帶參數
(三支帶 hook 絕對路徑、cross-repo 帶 hook 絕對路徑、gitea-arm-check 帶 repo 根目錄)
——不帶會紅,而那是假紅。
43 KiB
60 支閘,白話盤點表
回應
inkstone/InkStoneCo#40:「如果加入了,我應該可以白話文看到 hooks 的內容?」 這份表就是那個「白話文」——不用點開任何.sh檔,一行看懂一支閘在管什麼。最高原則(票上原文):每一條規則你都要能在 30 秒內看懂它在管什麼。
一句話結論
hooks/ 底下有 60 個 .sh 檔,hooks.json 實際掛上 82 條註冊(同一支閘常被多種情境同時掛上);
其中 3 支檔案存在但沒被掛上(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
🔴 這兩個數字上一版是錯的(2026-08-26 實際數過才發現):本頁原本寫「43 個檔、53 條註冊」, 而當時真實是 45 個檔、55 條註冊——中間有兩支閘進來時沒有回頭改這裡。 現在的寫法是實際數出來的:
ls hooks/*.sh | wc -l= 60;grep -c '"command":' hooks/hooks.json= 82。 ⚠️ 冒號不能省:grep -c '"command"'(沒冒號)會連"type": "command"一起數到,回 120。 本頁 2026-08-27 之前寫的是沒冒號那版——照著它跑會拿到一個跟本頁不符的數字。 一份會偷偷過期的盤點表,跟沒有盤點表差不多——見本頁最後「怎麼跟實況對帳」那段。📌
0.6.0(inkstone/ISEP#59,2026-08-27)這兩個數字是併完之後當場數出來的:ISEP#60移除了claim-verify-police.sh/subagent-claim-worksheet.sh這對自造機制(−2 支、−16 條),ISEP#58補進comment-carries-task-guard.sh/baton-handback-guard.sh(+2 支、+8 條), 加上ISEP#72的reply-identity-guard.sh已在 main 上。 一加一減之後檔數不變、註冊條數落在 59——這正是「不能靠加減推算、只能實數」的例子: 上一版寫 58,就是把兩批改動的加減兜出來、沒有真的數過。📌
0.10.0(inkstone/ISEP#63,2026-08-28)+1 支、+3 條:countdown-guard.sh一支同時掛在UserPromptSubmit(注入倒數)與Stop(查核有沒有戴), 加上 SessionStart 多掛一條scripts/countdown-milestone-refresh.sh(更新期限快取,不是閘)。 兩個事件是同一件事的兩半:只注入 = 又一個會被忽略的提醒;只查核 = 罰它做一件拿不到資料的事。 這兩個數字是併之前當場數出來的,不是從上一版加減推出來的。
📌
inkstone/ISEP#82(2026-08-28)+1 支、+3 條:mainline-focus-guard.sh同時掛在Agent與Task(派工的兩個入口都要守,只守一個等於沒守), 加上 SessionStart 多掛一條scripts/mainline refresh(更新主線的進度/期限,不是閘)。 53→54 支、68→71 條,兩個數字是加完之後當場數出來的(指令見本頁最後一段)。 ⚠️ 同時修掉一行重複:上一版「一句話結論」那一句被貼了兩遍 (53/68 那組數字本身是對的,只是印了兩次)。
🔴
0.6.1(2026-08-27)補回表格漏掉的兩列:isep-presence-beacon.sh(E 組)、milestone-due-guard.sh(A 組)。標頭寫 48 支的同時,下面的表格只列得出 46 支 ——數字是對的,人話少了兩行。發現的方法與防它再犯的指令,見下面「落差偵測」第二組comm。📌
0.7.0(inkstone/ISEP#30,2026-08-27)進來一支:mainline-idle-guard.sh(F 組,Stop)。 48→49 支、59→60 條,兩個數字都是加完之後當場數出來的(指令同上)。📌
inkstone/ISEP#86/#87/#88(2026-08-28)進來三支、+7 條:roster-guard.sh(D 組,Task+Agent各一條)、diagnosis-evidence-guard.sh(A 組,Bash)、investigate-first-stamp.sh(H 組,Agent|Task與SendMessage各一條), 外加dispatch-format-guard.sh多掛兩處——Bash(接claude -p這條路)與 新的「回覆 subagent」matcher(SendMessage與雲端 session/trigger 那幾支)。 53→56 支、68→75 條,兩個數字都是在這棵樹上當場數出來的 (ls hooks/*.sh | wc -l/grep -c '"command":' hooks/hooks.json), 不是拿上一版加減推的。順手改掉兩個過期的數字:README.md的「48 支/59 條註冊/23 支腳本」 ——實數是 53/68/35(那是併之前 main 上的實況),本版之後是 56/75/36。 同一個病,只是換一個檔案。📌
inkstone/ISEP#93+#89(2026-08-28)進來兩支、+3 條:overdue-nag-guard.sh(E 組,SessionStart)與wiki-size-guard.sh(E 組 + B 組,兩個事件)。 58→60 支、79→82 條——兩個數字都是在「把 main 併進來之後」的樹上當場數出來的, 不是拿上一版加減推的(本頁自己上面就記著「不能靠加減推算、只能實數」)。🔴 這次併
hooks.json學到一件比數字更重要的事:它是 JSON, 合錯了語法照樣合法、閘卻不見了,不會有任何東西喊一聲。 「同一個陣列位置兩邊各有一支」時(本次是roster-guardvsmainline-focus-guard), 正解是展開成兩個項目,不是二選一——選一邊就會讓一支真的閘靜靜消失。 ⇒ 合完一律再跑一次逐支點名(指令寫在README.md「裝什麼」那段), 並跟兩個父節點做集合對照:從 main 掉了幾條、從自己這邊掉了幾條、冒出幾條。 本次三個答案都是 0。📌 順手抓到的漂移(每次併都抓到一個,這次是三個): ① 本頁的「一句話結論」被貼成三份、標頭被貼成兩份(數字還互相打架); ②
README.md的hooks/那列累積成四列,其中兩列是舊數字; ③plugin.json描述欄寫「49 支腳本」,而用本頁寫死的那道指令 (ls -p scripts | grep -v / | wc -l,只數檔案)在合併後的樹上實數是 48。 同一個病,第 N 次,只是換一欄。
📌
0.10.0(inkstone/ISEP#81,2026-08-28)進來一支:pr-verdict-guard.sh(F 組,Stop)。 51→52 支、64→65 條,兩個數字都是加完之後當場數出來的(指令同上)。 順手改掉一個過期的數字:描述欄長期寫「27 支腳本」,實數是 34 (ls -p scripts | grep -v / | wc -l)——同一個病,只是換一欄。
📌
inkstone/ISEP#85(2026-08-28)進來一支:milestone-account-guard.sh(A 組,PreToolUse/Bash)。 53→54 支、68→69 條,兩個數字都是加完之後在自己的樹上當場數出來的 (指令同上),不是拿上一版加一——system-dev/wiki/mistakes.md記著這條: 那個數字是 leo 的驗收介面,多報就是假綠。 順手修掉一個真的漂移:上一版「一句話結論」那一句被貼了兩次(同一行連續出現兩遍), 而標頭寫 52、內文寫 53——同一頁裡三個數字互相矛盾,正是本頁自己警告過的那種病。 另外描述欄的「38 支腳本」是把kbdb-live-exam//lib//patches/三個目錄也數進去了; 用本頁寫死的那道指令(只數檔案)實數是 39。
怎麼讀這張表
| 符號 | 意思 |
|---|---|
| 🛑 擋 | 條件不滿足就真的擋下這個動作(exit 2),你或 AI 會看到一段紅字說明 |
| 📝 記錄 | 不擋任何東西,只是在背景寫一筆紀錄或送一段提示文字給 AI |
| 💀 未生效 | 檔案存在,但沒有掛進 hooks.json——目前是死的,不會被執行 |
「對你意味著什麼」欄一律用「如果你看到 X,代表 Y」的角度寫,不寫程式邏輯。
A. 你(或 AI)在終端機打指令的當下(PreToolUse / Bash)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
github-contact-guard.sh |
AI 想碰 GitHub(git push、gh api 等)就先擋下——兩個帳號曾經因為機器人式高頻寫入被永久 flag 過。只有讀取(clone/fetch)自由,寫入一律要你在終端機跑 github-arm.sh 解鎖。 |
🛑 擋 |
kbdb-api-wall-guard.sh |
AI 想繞過 KBDB 的 API、直接對資料庫下 SQL 或建新表就擋下——KBDB 的鐵律是「只能用 API、永遠只有三張表」。 | 🛑 擋 |
stage-before-prod-guard.sh |
AI 想動「出貨鏈」相關指令(含 arcrun-rag-bundles/github-arm/publish-github 字樣)卻沒先在 stage 環境驗過就擋下。 |
🛑 擋 |
main-and-prod-push-guard.sh |
兩層確認:① subagent 想推 Gitea main 分支 → 一定要交回總管,總管確認過才放行;② 任何 wrangler deploy 打正式環境 → 一定要你親手解鎖。 |
🛑 擋 |
prod-write-guard.sh |
AI 想做任何「會真的改到線上正式帳號(leo21c)」的操作(不只是 push,也包含直接呼叫會寫入的工具)就擋下,交回總管處理。 | 🛑 擋 |
not-my-branch-guard.sh |
AI 想 commit 到「別的 subagent 正在施工中」的分支(登記在 branch-holds.md)就擋下,避免兩個 AI 互踩同一條分支。 |
🛑 擋 |
leo21c-write-guard.sh |
AI 想寫東西到你私人帳號 leo21c 就擋下——這個帳號只准你自己用,AI 的實驗一律走別的測試帳號。 |
🛑 擋 |
release-tag-guard.sh |
AI 想打版本 tag,但 plugin.json 裡寫的版本號跟 tag 對不上就擋下——避免「這裡說 0.2.0、那裡說 0.1.0」各說各話。 |
🛑 擋 |
ticket-api-bypass-guard.sh |
AI 想繞過 scripts/ticket 工具、直接打 API 新增 Gitea 東西(開新票/milestone/label/PR,會漏掉「新增前先搜過」這道檢查)就擋下——不管是隱式 POST(urllib 傳 data= 沒寫 method)還是小寫 requests.post(...)。 |
🛑 擋 |
milestone-due-guard.sh |
AI 想開一個新的里程碑(milestone)卻沒填期限就擋下,填 9999-01-01 一樣擋——那是「沒有期限」穿了一件期限的衣服,盤點時每一格看起來都有值,其實一格都沒有。只管「建里程碑」這個動作,查詢/改別的欄位都放行。 |
🛑 擋 |
milestone-account-guard.sh |
AI 想直接打 API 把某個里程碑改成「已關閉」,卻還沒留下「估多久/花多久/差多少/為什麼差」就擋下。跟上一列是同一條線的兩端:那支管開里程碑要有真的期限,這支管關的時候要留下帳。走正門 scripts/milestone-account close 就會自己算(數字取自 Gitea 的 created_at/due_on/closed_at,不用手填),差超過 ±25% 才要你從七個固定代號裡挑一個,而且那個代號要有 Gitea 時間軸撐得住——不採信自述。純讀取/建里程碑/改期限/只是談論它/走正門,全部放行;真要手動關就在指令裡加 milestone-account-ok(留痕)。 |
🛑 擋 |
reply-identity-guard.sh |
直接打 Gitea API 貼留言到票上時,內文第一行沒有 【身份】(總管/subagent/leo)就擋下——票上多條線並行時,看不出哪一則是誰寫的。純讀取、走 scripts/ticket 正門、指令裡加 reply-identity-ok 都放行。 |
🛑 擋 |
comment-carries-task-guard.sh |
AI 要在票裡留一則「等某某上線才驗得了」這種還沒做完的事,卻沒有把它開成一張子票時,擋一次。留言沒人會回頭讀,票的 open/closed 撈一次就看得到。 | 🛑 擋(同一輪只擋一次) |
search-is-not-proof-guard.sh |
AI 用知識庫的搜尋找到一筆東西,然後把那筆東西的原始編號直接貼進票裡或派工單裡,當成「某個功能壞了」的證據——而它從沒用正規的檢索路徑去查過那筆——擋一次。你關心的是:不會再有人拿錯的方法量出一個錯的結論,然後退回一張其實已經做完的票。 | 🛑 擋(同一輪只擋一次) |
diagnosis-evidence-guard.sh |
你想把一段有份量的東西寫上票,而這個 session 沒派人去查過那張票、那段話裡也沒有任何走得過去的出處(檔案:行號/commit/comment 號/票號/網址/貼出來的實測輸出)就擋一次——未經調查的診斷寫在票上會長得像事實,工人會照著它去驗證而不是去查。派過人查、或把出處帶上,都放行;一句短回覆不管。 | 🛑 擋 |
B. AI 想寫檔案/改程式碼的當下(PreToolUse / Write·Edit·MultiEdit)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
guard-cross-project.sh |
總管(頂層)想直接改某個子 repo 的程式碼(非 .md)就擋下——頂層只做安排交辦,實作要進那個子 repo 自己做。 |
🛑 擋 |
wiki-secret-scan.sh |
要寫進 system-dev/wiki/ 的內容裡出現密碼/金鑰/身分證/信用卡等特徵就擋下,防止機敏資料意外留在會被反覆讀取的記憶空間裡。 |
🛑 擋 |
component-guard.sh |
AI 想自己新建一個零件(component)或亂接 service binding 就擋下——逼它先想「現成零件夠不夠用」,真要建要你解鎖。 | 🛑 擋 |
sdd-guard.sh |
AI 想直接動程式碼檔案,但找不到「唯一一份 active 規格文件(SDD)」對應這件事,或同時有一份以上 active 規格就擋下。 | 🛑 擋 |
credential-only-guard.sh |
AI 想把金鑰真身或自製佔位符(例如 __XXX_TOKEN__)寫進設定檔就擋下——金鑰只准放在統一的 credential 中心。 |
🛑 擋 |
arcrun-intent-guard.sh |
AI 寫的 Arcrun workflow 語法不對就擋下,而且直接把正確寫法回貼給它(不是只罵它錯,是教它怎麼改)。 | 🛑 擋(教學型) |
subagent-first-guard.sh |
這個對話從頭到尾都沒有派過任何 subagent,AI 卻要自己動手改程式碼,就先擋一次,逼它想一想「這件事能不能交給別人做」。 | 🛑 擋 |
mistake-needs-ticket-guard.sh |
AI 想往 mistakes.md(教訓紀錄)新增一條「機制可以防止」的教訓,卻沒附對應票號就擋下——沒有票號的教訓沒有人會回頭處理。 |
🛑 擋 |
wiki-size-guard.sh |
AI 想一口氣砍掉 wiki 檔一大半內容(淨縮水超過 800 字且超過原本 45%)就擋一次——那不是一次編輯,那是一次壓縮,而壓縮會弄丟東西,弄丟的當下沒有人會發現。出路是走 scripts/wiki-compress:它逼你附票號、把壓掉了什麼寫進 .compress-log.md,並用內文雜湊逐條對帳證明沒弄丟。只是改字、加字、小修一律不碰;真要手改就在內容裡放 wiki-compress-ok 留痕。 |
🛑 擋(至多攔一次) |
pending-changes-retired.sh |
AI 想寫東西進已經廢除的 pending-changes.md 檔案就擋下——這個檔案已停用,規格變更一律改開 Gitea 票。 |
🛑 擋 |
kbdb-api-wall-guard.sh在這裡也重複掛了一次(見 A 組)——它同時守著「下指令」跟「寫檔案」兩種情境,詳見下方「重複掛載」一節。
C. AI 要去翻程式碼/查資料之前(PreToolUse / Grep·Glob·Read·Bash)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
wiki-first-search.sh |
AI 這回合還沒查過 wiki就要去翻程式碼或查外部資料,就先擋下、逼它用你的關鍵字先搜一次 wiki(查過一次、不論有沒有找到,這回合後面就放行)。 | 🛑 擋 |
D. AI 要派工給別的 AI(subagent)之前(PreToolUse / Agent·Task)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
subagent-wiki-guard.sh |
AI 要派一個查證/實作類任務出去,就自動在派工單裡塞一句「先查 wiki」的提示——不擋,只是順手夾帶叮嚀。 | 📝 記錄 |
kbdb-api-wall-guard.sh |
同 A/B 組,只是這裡管的是「派工單裡有沒有寫出違反 KBDB 規約的指示」。 | 🛑 擋 |
micromanage-guard.sh |
派工單寫得太細(指名檔案函式、編號步驟、要求每做一項回報一次…)就擋下——subagent 該被當成有能力的同事,不是照抄劇本的工具。 | 🛑 擋 |
irreversible-dispatch-guard.sh |
派工單裡出現「刪分支」「drop table」「rm -rf」這類不可逆動作,卻沒寫「先停下來等回覆才執行」就擋下。 | 🛑 擋 |
no-ticket-no-dispatch.sh |
派工單裡沒有寫工單號(【工單】owner/repo#N),或那張票已經關閉/根本不存在,就擋下——沒有票號的工作沒有人追得到進度。 |
🛑 擋 |
dispatch-format-guard.sh |
派工單 = 票號,就這樣。 票號以外還寫了別的東西(背景、驗收條件、紅線、交件方式、「這個 session 才知道的事」)就擋下,並告訴你那些內容該搬去哪:每次都一樣的 → 共通規定;這次才知道的 → 寫進那張票。反過來,合規的派工它會自動把共通規定注入給收工方(交件方式、不准 push main、org 是 inkstone…),所以你不寫也不會漏。判準是「這一行是不是【工單】欄位」——在不在,不是寫了什麼,所以講得再謹慎也不會被多罰。純禮貌收尾(「謝謝」)不算違規;(→ comment M) 不論全形或半形括號都認得(ISEP#65 修過一次:CLAUDE.md 自己規定的合格寫法曾經被這支閘自己擋下)。ISEP#88 起同一支閘也掛在「回覆 subagent」那條路上(SendMessage、雲端 session/trigger、以及 Bash 的 claude -p)——回覆也是派工,同一套規矩;擋下來時會把那段內容原文印出來方便直接貼上票。subagent 往上回報(to: "main")不管,那是交件不是派工。 |
🛑 擋(+注入) |
roster-guard.sh |
派工要指名派給誰:Task 的 subagent_type 得是 agents/ 名單上的名字(isep-hand/arcrun-hand/scout…),沒指名或指了不存在的名字就擋下,並把整份名單印出來。指對了它會自動把那位工人的檔案原文注入給它——你是誰、負責哪個 repo、開工前先讀什麼、你的紅線是什麼(這些每個工人都不一樣,共通規定補不了)。名單是資料:加一個工人=在 agents/ 加一個 .md,不必改任何 hook。 |
🛑 擋(+注入) |
mainline-focus-guard.sh |
派出去的票不在「現在的主線」上,就擋一次,並問你是「補收」(它本來就該掛在主線上)還是「跳線」(真的插件事)。背景:2026-08-27 實查有 14 個 milestone 同時開著,其中「Mira 現代化」同名活在 5 個 repo ⇒ SOP 說的「只做該 milestone 的事」在現場沒有指涉對象,等於不存在。現在主線是一個查得到的事實(scripts/mainline,答案唯一),這支就是那個事實的用處。🔴 沒有標主線 ⇒ 一律放行;多張票只要有一張在主線上就放行;查不到那張票掛在哪也放行(讀不到 ≠ 不屬於);同一張票只擋一次,重送就過。跳線會留一筆紀錄——不留痕的話,跳線與專注在機器上長得一模一樣。 |
🛑 擋(同一張票至多一次) |
dispatch-format-guard.sh |
派工單 = 票號,就這樣。 票號以外還寫了別的東西(背景、驗收條件、紅線、交件方式、「這個 session 才知道的事」)就擋下,並告訴你那些內容該搬去哪:每次都一樣的 → 共通規定;這次才知道的 → 寫進那張票。反過來,合規的派工它會自動把共通規定注入給收工方(交件方式、不准 push main、org 是 inkstone…),所以你不寫也不會漏。判準是「這一行是不是【工單】欄位」——在不在,不是寫了什麼,所以講得再謹慎也不會被多罰。純禮貌收尾(「謝謝」)不算違規;(→ comment M) 不論全形或半形括號都認得(ISEP#65 修過一次:CLAUDE.md 自己規定的合格寫法曾經被這支閘自己擋下)。 |
🛑 擋(+注入) |
D2. AI 想開口問你問題的當下(PreToolUse / AskUserQuestion)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
ask-user-question-guard.sh |
AI 要跳出來問你一個問題的那一刻先攔一下,用小模型(haiku)照「四題公式」判這題該不該打擾你:花錢/不可逆/跨專案結構/品味方向/只有你做得到——命中任何一題就放行(那本來就該問你),四題全否(純技術實作選擇、問「要不要開始」)就擋回去要它自己裁。同一個問題只擋一次,它重送就過得去,所以判錯不會害你收不到問題;判官掛掉/沒網路也一律放行。 | 🛑 擋(同一題至多一次) |
為什麼要有這一組(leo 2026-08-26:「今天已經好幾次問我,為什麼 hooks 沒有攔下來?」): 在這之前
AskUserQuestion在hooks.json裡出現 0 次,一支閘都沒掛。 F 組那兩支自走警察(self-drive-police/self-drive-judge)判準一樣, 但它們掛在「收工」那一刻——問題早就送到你眼前了,事後再問 AI「你查過了嗎」已經來不及。 這一組補的是時機,不是判準。
E. 每個對話一開始(SessionStart)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
isep-presence-beacon.sh |
對話一開始印一行 🟢 ISEP vX.Y.Z 已載入(N 支閘|來源:…)。這行不是裝飾,是唯一能證明「這個 session 真的有閘」的東西——它自己就住在 plugin 裡,看得到它就表示 plugin 載入了;某個 session 從頭到尾沒有這行,那個 session 是零閘狀態,先修 plugin 再做事。同一台機器可能同時有兩份 ISEP(marketplace 裝的、repo 裡 vendor 的),所以那行會講出這次是哪一份在說話。 |
📝 記錄(context 注入) |
session-start-recall.sh |
對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) |
scripts/mainline refresh |
(不是閘,是腳本)對話一開始把現在的主線那條 milestone 的進度與期限更新一次,好讓每回合眼前那一行講的是今天的數字。拿不到 Gitea 就原封不動——寧可資料舊,不要把主線弄丟。跑一次就結束,不輪詢。 | 📝 記錄 |
overdue-nag-guard.sh |
對話一開始就去 Gitea 撈沒有人會叫的事:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,有事就發 Telegram 給你。沒東西可報時它會說「查過了,沒有」——安靜跟壞掉長得一模一樣。發不出去時不會靜默:它會先問這個 session 的 prod-write-guard 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,不輪詢、不掛排程。 |
📝 記錄(context 注入 + Telegram) |
wiki-size-guard.sh |
對話一開始講出「哪幾個 wiki 檔已經沒有人讀得完了」(預設超過 1200 行就點名)。讀不完的必讀檔,跟沒有那個檔的差別只在於它讓人以為有。 另一半掛在寫檔上,見 B 組。 | 📝 記錄(context 注入) |
skill-deploy-drift-guard.sh |
如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
F. AI 想結束這一輪、要收工的時候(Stop)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
empty-handed-stop-guard.sh |
這一輪 AI一個動作都沒做卻想停下來(等你回覆),就擋下並告訴它「你的命令就是完整授權,不用再等第二次確認」。 | 🛑 擋(至多攔一次) |
worklist-guard.sh |
AI 自己列過的待辦清單裡還有沒做完的步驟,卻想收工寫報告,就擋下,逼它做完剩下的步驟。 | 🛑 擋 |
factory-idle-guard.sh |
AI 該去派工卻沒派(工頭停工),就擋下要它交出「已經派工的憑證」,不是隨口說一句「我會催」就算數。過閘有四條路:現在就派工/把票號寫進那句話/寫一行 ⏸ 等:<在等什麼>/這一輪收尾在動作上。 2026-08-23(inkstone/ISEP#30)修好「引用被當成主張」——貼原始碼、引用它自己的訊息、否認自己有下一步,都不再被咬。 |
🛑 擋 |
mainline-idle-guard.sh |
AI 連續 4 個回合一直在查、一直在讀,卻什麼都沒交出去(沒派工、沒改檔、沒寫回票)就擋一次。補的是上面兩支中間那一格:「沒動作」空手警察管、「說了下一步卻沒派」稼動率警察管,而「有動作、但就是不說下一步」以前兩支都放行。這支一個字都不讀,只看有沒有派工/產出/工作區變動。響過一次門檻就加倍(4→8→16),不會變成一直在響的警報。 | 🛑 擋(響過就退讓) |
pr-verdict-guard.sh |
收工時清點「還有哪些 PR 沒有結論」——open、沒有人被指派、也沒有「要求修改」review 的 PR,就擋一次並點名是哪幾個(含開了幾天與網址);已經 merge 但分支還留著的也一起點名(票上寫死「併完當場刪掉那條 branch」)。背景:2026-08-27 實查,最久的三個 open PR 躺了兩星期,而當時 46 支閘沒有一支在管 PR——票看起來「已交付」,東西卻沒進 main、沒進版本,leo 手上永遠不會出現它。🔴 擋的是遺忘,不是等待:指派給誰(或掛 Human)就立刻不再點名它;響過門檻就加倍(1→4→8→16,每個 PR 各自算)。搭配 scripts/pr-verdict(merge/reject/changes 各一個動作)。 |
🛑 擋(響過就退讓) |
countdown-guard.sh |
每一則回覆都自己說出「這件事已經花了多久/距今天收工線還剩多久」,而且不必有人記得。一支閘掛兩個事件:你送出訊息時它先把算好的那一行放到 AI 眼前(UserPromptSubmit),AI 想收工時它再看這一回合的回覆裡到底有沒有那一行,沒有就擋一次要它補(Stop)。只做前半 = 又一個會被忽略的提醒,所以兩半綁在一起。已過時間從這段對話的第一則訊息算起(=一個 release 的起點);收工線是台北 16:00,過線後四小時內顯示「已超過」,不會把超時悄悄換算掉。子 session、純工具回合、已經提醒過一次的,全部放行。同一個注入點還帶第二行 🎯 現在的主線是哪一個(inkstone/ISEP#82)——那一行沒有另外立閘,因為主線的名字本來就掛在被查核的 ⏱ 那一行上。 |
🛑 擋(至多攔一次) |
browser-verify-guard.sh |
這一輪 AI 宣稱「前端驗過了」,卻沒有真的用瀏覽器工具載入過,就擋下——curl 抓到 HTML 不算驗過。 |
🛑 擋 |
self-drive-police.sh |
AI 想停下來問你「早就決定過的事」(用固定句型判斷,例如「要不要 X」「下一步做什麼」「這交給你」)就擋下,反問它查過 wiki/查過派工表了沒。 | 🛑 擋 |
self-drive-judge.sh |
跟上面同一件事,但改用小模型(haiku)判斷「換句話說」的請示句——防止 AI 只是把「要不要」改寫成「不確定是否符合期待」就閃過上一支閘。 | 🛑 擋 |
delivery-police.sh |
AI 宣稱「這件事做完了」,卻看不到任何實測證據(畫面截圖、指令輸出、HTTP 狀態碼…)就擋下。 | 🛑 擋 |
wiki-first-police.sh |
AI 做完事卻沒有把結論寫回 wiki 就想收工,就擋下——下次(或別的 AI)查 wiki 會查不到這次做過什麼。 | 🛑 擋 |
unpushed-police.sh |
AI 改好的東西還留在本機、沒有真的推送出去給別人用,卻想收工,就擋下——「改對了但沒送到」跟沒改是一樣的。 | 🛑 擋 |
🔴
claim-verify-police.sh已於inkstone/ISEP#60移除(連同下面原本的 G 組subagent-claim-worksheet.sh)。 這一對是自造的「待驗單」機制(一張沒有狀態、沒有持有人的 markdown),違反 D58: 不要硬做平台不支援的機制。它守的情境現在改由 Gitea 原生三格承接 (子票相依/s/*tag/指派——見inkstone/ISEP#59與 PR #58)。
G. Subagent 把工作交回來的時候(SubagentStop)
inkstone/ISEP#60 之後,這個時機沒有專屬掛載的閘了(原本唯一的一支 subagent-claim-worksheet.sh 已移除)。
仍會掛到這個時機的是 worklist-guard.sh/self-drive-police.sh/self-drive-judge.sh/delivery-police.sh/wiki-first-police.sh/unpushed-police.sh,
行為跟上面 F 組完全一樣,只是對象換成「subagent 交回來的這一輪」。詳見「重複掛載」一節。
H. 動作做完之後,純粹記一筆(PostToolUse,全部不擋)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
kbdb-asked-stamp.sh |
AI 真的查過 KBDB 之後,留一個時間戳——給前面 history-first-guard.sh 判斷「這輪有沒有先查過」用。 |
📝 記錄 |
subagent-first-stamp.sh |
AI 真的派過工之後,留一個時間戳——給 subagent-first-guard.sh 判斷用。 |
📝 記錄 |
issue-status-autoflip.sh |
AI 一派工出去,就自動把對應的 Gitea 票改成「進行中(s/doing)」,不必等人手動改標籤。 | 📝 記錄(自動改票) |
baton-handback-guard.sh |
一條派工線做完了,就去看它那張票有沒有指派給人/標籤有沒有說它卡在哪/有沒有寫下一步,缺哪一格就當場說出來。你關心的是:不會再有票做完了卻沒人接手,躺在那裡沒人發現。 | 📝 記錄(提醒,不擋) |
kbdb-evidence-stamp.sh |
AI 每次去知識庫拿資料,就記一筆「這是搜尋搜到的,還是走正規檢索路徑拿到的」。你關心的是:它等一下要拿這筆資料下結論時,有東西可以對照它走的是哪條路。 | 📝 記錄 |
investigate-first-stamp.sh |
你真的派人去查某張票之後,按「session + 票號」留一個時間戳——給 diagnosis-evidence-guard.sh 判斷「這件事有沒有人查過」用。按票分而不是按 session 分:早上派人查了 A 票,不該等於解鎖了 B 票的診斷。 |
📝 記錄 |
I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
history-first-guard.sh |
AI 要改一個舊檔案之前,先把這個檔案過去被改過幾次、被誰在什麼情況下改過的紀錄攤在它眼前,逼它回答「這是不是已經修過的老問題」再動手。 | 🛑 擋 |
目前沒生效的 3 支(存在但沒掛進 hooks.json)
| 檔名 | 為什麼沒掛 |
|---|---|
pre-write-guard.template.sh |
官方留的空殼範本,預設不攔任何東西。要用要自己手填禁令清單、自己去掛。檔頭寫明「別誤以為裝了它就有保護」。 |
pre-write-guard.sh |
同一個範本的另一份(看起來是填過一半的版本),同樣沒掛進 hooks.json。 |
shadow-table-guard.sh |
檔頭自己寫明「本檔目前是死的」——leo 2026-08-15 說過「提案封鎖方式,不是要你就去做」,所以先寫好、測過,但要不要真的掛上由你裁。 |
落差偵測(票上要求的「這件事本身值得被看到」)
方法:比對 hooks/*.sh 檔案清單 vs hooks.json 裡出現的檔名。
$ ls hooks/*.sh | xargs -n1 basename | sort > /tmp/fs_hooks.txt
$ grep -oE '[a-zA-Z0-9_-]+\.sh' hooks/hooks.json | sort -u > /tmp/registered_hooks.txt
# 有檔案、但 hooks.json 沒註冊到 → 上面「沒生效的 3 支」
$ comm -23 /tmp/fs_hooks.txt /tmp/registered_hooks.txt
pre-write-guard.sh
pre-write-guard.template.sh
shadow-table-guard.sh
# hooks.json 註冊了、但檔案不存在 → 應該是空的
$ comm -13 /tmp/fs_hooks.txt /tmp/registered_hooks.txt
countdown-milestone-refresh.sh ← 假警報:它在 scripts/ 不在 hooks/
🔴 上面那一筆是假警報,不是「指向空氣」的閘:inkstone/ISEP#63 之後,
SessionStart 上掛的東西不全是 hooks/*.sh——scripts/countdown-milestone-refresh.sh
是腳本不是閘。這組指令只比檔名、不看目錄,所以它會出現在這裡。
(inkstone/ISEP#82 又多掛一條 scripts/mainline refresh,那一條沒有 .sh
副檔名,所以不會落進這個名單。)照著跑會看到它,這裡先講清楚,
免得下一個人以為是自己弄壞的。
🔴 上面那兩行驗不出「表格漏列」——它們比的是 hooks.json,不是這張表。
「有閘、但這頁沒有它那一行人話」要另外一組指令才看得見,就是下面這組
(2026-08-27 就是靠它抓到本頁漏了 isep-presence-beacon.sh/milestone-due-guard.sh 兩列):
$ ls hooks/*.sh | xargs -n1 basename | sort > /tmp/inv_fs.txt
$ grep -oE '^\| `[a-zA-Z0-9_.-]+\.sh`' docs/hooks-inventory.md \
| grep -oE '[a-zA-Z0-9_.-]+\.sh' | sort -u > /tmp/inv_table.txt
# 有這支閘、但這頁沒有它那一列 → 要補人話
$ comm -23 /tmp/inv_fs.txt /tmp/inv_table.txt
(無輸出)
# 這頁列了、但檔案已經不在了 → 要刪那一列
$ comm -13 /tmp/inv_fs.txt /tmp/inv_table.txt
(無輸出)
🔴 **^\| \`` 這個錨點不能拿掉**——它的意思是「**這一行的第一欄就是這支閘**」, 也就是「表格真的列了它」。把錨點拿掉、改成抓全頁所有反引號裡的 *.sh`,
反向那半會吐出 3 個假警報(2026-08-27 實跑):
claim-verify-police.sh ← 出現在「已於 ISEP#60 移除」那段**移除紀錄**裡
subagent-claim-worksheet.sh ← 同上
github-arm.sh ← 它在 `scripts/`,不在 `hooks/`;是 github-contact-guard
那一列裡告訴你「怎麼解鎖」的逃生口
三個都不是幽靈,三個都不該刪:刪掉前兩個等於刪掉「G 組為什麼是空的」這段歷史, 刪掉第三個等於讓那一列不再告訴你怎麼過閘。 ⇒ 0 次正確攔截、3 次誤攔——照這個訊號去改,會把好東西改壞。
那「頁面上出現了一個根本不存在的檔名」怎麼辦?那要另外一格量,而且判準不是文字是事實:
# C. 頁面提到的每個 *.sh,現在存在,或者 git 歷史裡存在過
$ for n in $(grep -oE '`[a-zA-Z0-9._-]+\.sh`' docs/hooks-inventory.md | tr -d '`' | sort -u); do
[ -f "hooks/$n" ] || [ -f "scripts/$n" ] && continue
[ -n "$(git log --all --oneline --diff-filter=AD -- "hooks/$n" "scripts/$n")" ] && continue
echo "$n"
done
(無輸出)
「現在沒有、但曾經有」=歷史,合法;「從來沒有過」=打錯字或憑空捏造,才是幽靈。 這一格問的是 git,不是問這頁的用字,所以它不會因為誰換個寫法就失效。
📌 三格都要跑,而且它們量的是不同東西:
A1 缺人話、A2 表格有幽靈、C 全頁有幽靈。
標頭的 48/59 對不代表這頁沒漏——補進 2 支真的、同時留著 2 支假的,數字照樣湊得到 48。
數量相等不等於集合相等,所以驗的一律是集合(comm),不是 wc -l。
這次順便抓到的另一個落差(🕐 這一段是 2026-08-20 的快照,裡面每個數字都是那天的,不是現況——
留著是因為它是「兩份文件的數字不會自己跟上」這個病的第一個案例;現況數字看本頁標頭):
.claude-plugin/plugin.json 的說明文字寫「58 支機械閘(52 條註冊)」,
README.md 也寫「42 支 + hooks.json」「共 52 條註冊」——但實測是 43 支檔案、53 條註冊。
差 1 支、差 1 條,猜測是今天(2026-08-20)新增的 leo21c-write-guard.sh/release-tag-guard.sh/
ticket-api-bypass-guard.sh 這批(檔頭日期都是今天)加了之後,兩份文件的數字沒有跟著更新。
這兩個檔案本次刻意沒動(在你劃的紅線內:不准碰 .claude-plugin/),只在這裡把落差標出來給你看。
重複掛載(同一支閘在不只一個時機生效)
不是衝突,是同一支閘刻意守好幾個情境;列出來是因為你可能會納悶「怎麼同一句紅字出現在不同地方」:
| 閘名 | 掛了幾次 | 為什麼 |
|---|---|---|
kbdb-api-wall-guard.sh |
3 次 | 下指令(Bash)、寫檔案(Write/Edit)、派工(Task)三種情境都可能違反 KBDB 規約,各掛一次 |
prod-write-guard.sh |
2 次 | 一次守「下指令」,一次專門守「會直接寫進線上的 MCP 工具」(不是走終端機指令的那種) |
arcrun-intent-guard.sh |
2 次 | 一次守「所有寫檔案」,一次專門加強守「呼叫 Arcrun 部署/驗證工具」這個更精準的情境 |
micromanage-guard.sh / irreversible-dispatch-guard.sh / no-ticket-no-dispatch.sh |
各 2 次 | 各自同時掛在 Task 與 Agent 兩個矩比對名稱上——這兩個名稱應該是同一種派工動作的新舊叫法,兩個都掛保證不漏接 |
worklist-guard.sh / self-drive-police.sh / self-drive-judge.sh / delivery-police.sh / wiki-first-police.sh / unpushed-police.sh |
各 2 次 | 一次守「總管自己想收工」(Stop),一次守「subagent 交回工作」(SubagentStop)——同一套判準用在兩種角色身上 |
search-is-not-proof-guard.sh |
3 次 | 拿搜尋結果當證據的那個動作,可能是「貼進票裡」(Bash),也可能是「寫進派工單發包出去」(Task/Agent 兩個名稱各掛一次),三種都要接住 |
❓ 這裡有一件我看不出來是刻意還是遺留、需要人判斷:micromanage-guard.sh/irreversible-dispatch-guard.sh/no-ticket-no-dispatch.sh
三支都同時掛在 Task 跟 Agent 這兩個矩比對名稱上。如果這兩個名稱在目前版本的 Claude Code 裡指的是「同一種派工工具呼叫」,
那這是保險(兩個名字都接住,不怕哪天官方改名),沒問題;但如果其實只有一個名稱會真的觸發,另一個是舊名稱留下來沒清掉,
那就是「規則說是兩層防護,實際只有一層在動」。這個要靠實際觸發紀錄核對,我沒有把握單靠讀檔案判斷,標成 ❓ 而不是硬下結論。
抽驗 5 支:一句話 vs 實際邏輯逐條對照
隨機抽了跨越不同層級/時機的 5 支,逐行核對過源碼(不是只讀檔頭):
github-contact-guard.sh——表格寫「AI 想碰 GitHub 就先擋下,只有讀取自由」。 源碼核對:只擋gh api/repo/issue/pr/...子指令,以及git push/git remote add指向 github.com(含用 remote 名稱反解出網址的情況);git clone/fetch/pull/ls-remote、curl、go get一律放行。帶憑證的「實名讀」也放行但會留一筆紀錄。與表格描述一致。main-and-prod-push-guard.sh——表格寫「推 main 要總管戳記、prod 部署要你解鎖」。 源碼核對:git push目標含main/master才擋,且要/tmp/.main-push-ok戳記綁對 repo 路徑、15 分鐘內、用過即丟才放行;wrangler deploy/publish/versions deploy且指令裡看不出打的是 stage/youlin/geek6688 就擋,要.github-armed或 Gitea 票上核准碼才放行。與表格描述一致,且比表格寫得更細(例如 stage 與 geek6688 兩個白名單)。empty-handed-stop-guard.sh——表格寫「這輪零動作卻想停就擋,至多攔一次」。 源碼核對:讀 transcript 數這回合tool_use出現次數,0 次 → exit 2;stop_hook_active已為真(代表已經擋過一次)→ 直接放行,不會卡死。與表格描述一致。no-ticket-no-dispatch.sh——表格寫「派工單沒工單號,或票已關閉/不存在就擋」。 源碼核對:從tool_input.prompt抓【工單】owner/repo#N,抓不到 → exit 2;抓到但打 Gitea API 查到state=closed或查不到(missing)→ exit 2; 查不到網路(token 拿不到、API 連不上)→ 放行(fail-open,避免網路抖動卡死工作)。與表格描述一致,且多了一個表格沒特別寫的細節:網路問題不擋。wiki-secret-scan.sh——表格寫「寫進 wiki 的內容有密碼/金鑰/身分證/信用卡特徵就擋」。 源碼核對:只在file_path命中system-dev/wiki/*時啟動;用 6 類 regex(密碼賦值、PEM 私鑰、雲端金鑰前綴、JWT、連線字串內嵌帳密、身分證、信用卡)逐條檢查要寫入的內容; 行尾標記wiki-secret-ok可豁免。與表格描述一致。
五支全部核對通過,沒有發現表格描述跟實際邏輯對不上的情況。
我看不懂、需要人看的地方
micromanage-guard.sh/irreversible-dispatch-guard.sh/no-ticket-no-dispatch.sh的雙重矩比對(Task+Agent)是刻意保險還是舊名稱沒清掉——見上面「重複掛載」段落,我沒有把握單靠讀檔案判斷,需要看實際觸發紀錄或問總管。stage-before-prod-guard.sh跟main-and-prod-push-guard.sh的分工邊界:main-and-prod-push-guard.sh的檔頭明講自己是在「補stage-before-prod-guard.sh的破口」(那支只認 3 個關鍵字,抓不到wrangler deploy),但兩支都還掛著、都還在管「prod 出貨」這件事。這是「新的補洞、舊的continua」還是「舊的該退休了」,這份盤點表不下判斷,留給你在下一步的分類會議裡定奪。
除了以上兩點,其餘各支的行為都能從檔頭與源碼直接讀出,沒有「猜」的部分。
📌 這句話原本寫「其餘 41 支」——那是 43 支閘那一版(
v0.2.1)數的,之後沒有人重跑過。 2026-08-27 把數字拿掉而不是改成 46:改成 46 等於宣稱我逐支重讀過源碼,而我沒有。 要恢復一個數字,就重跑一次上面「抽驗」那件事,然後把當天的日期寫上去。
這份表怎麼跟實況對帳(半年後怎麼發現漂移)
- 有沒有新閘沒被收進這張表:跑本文「落差偵測」段落第二組(
inv_fsvsinv_table的兩行comm)加第三格 C,比對hooks/*.sh的檔名清單跟這張表列出的閘名清單。不是第一組——第一組比的是hooks.json,只驗證有沒有註冊,驗不了有沒有寫進這張人話表。2026-08-27 漏掉的那兩列,第一組是綠的、第二組才紅。抓表格列的那組grep一定要帶 `^| `` 錨點,理由與拿掉之後會冒出的 3 個假警報,就寫在那一段裡。 - 有沒有閘的行為跟這裡寫的不一樣了:抽幾支重新讀一次源碼,跟這裡的「一句話」對一遍——就是本文「抽驗 5 支」做的事,可以照同樣方法定期重做。
.claude-plugin/plugin.json與README.md的數字:這兩處各自寫了一次「幾支、幾條註冊」,前面已經抓到一次對不上(42/52 vs 實際 43/53)。這兩個數字沒有機制保證跟著hooks/目錄自動更新,是本表發現的第一個具體漂移案例。