Files
ISEP/docs/hooks-inventory.md
T
Leo 5ac06abc95 工人有名字+回覆也是派工+未經調查不寫診斷(inkstone/ISEP#86/#87/#88)
三張票同一族(誰在派、派給誰、派的內容住哪裡),做在同一條分支:

#86 工人名單:agents/ 七位有名字的工人+scripts/roster+hooks/roster-guard.sh
    派工用 Task 的 subagent_type 指名,派工單格式一個字都沒改;
    指對名字就把那位的檔案原文注入(你是誰/先讀什麼/你的紅線)。
    【身份】欄同時吃得下工人名字(原本三個角色照舊)。

#87 未經調查不寫診斷:hooks/diagnosis-evidence-guard.sh + investigate-first-stamp.sh
    三個結構訊號(派過人查沒/有沒有走得過去的出處/有沒有份量),
    一個關鍵字比對都沒有;轉述有出處不會被誤擋。

#88 回覆也是派工:不另造閘,把攔截點加掛上去。
    hooks/lib/dispatch_parse.py 的 tool_channel() 一次列全所有通往 subagent 的路
    (SendMessage/雲端 session・trigger/claude -p);擋下來時把那段內容原文印出來。
    subagent 往上回報(to: "main")=交件不是派工,刻意不管。

順手修掉一個真的會咬人的 flake:dispatch-format-guard 原本開四支 python 各讀一個欄位,
機器忙的時候某個欄位會靜靜變空字串(實測連跑 10 次有 1 次「豁免了卻還是被擋」)。
四個欄位改成一次讀完。

版本號待總管定(plugin.json 只更新了描述裡的數字,版本沒動)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
2026-08-28 01:15:09 +00:00

343 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 56 支閘,白話盤點表
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
>
> **最高原則(票上原文)**:每一條規則你都要能在 30 秒內看懂它在管什麼。
## 一句話結論
`hooks/` 底下有 **56 個 `.sh` 檔**`hooks.json` 實際掛上 **75 條註冊**(同一支閘常被多種情境同時掛上);
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
> 現在的寫法是實際數出來的:
> `ls hooks/*.sh | wc -l` 56`grep -c '"command":' hooks/hooks.json` 75。
> ⚠️ **冒號不能省**`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`(更新期限快取,不是閘)。
> **兩個事件是同一件事的兩半**:只注入 = 又一個會被忽略的提醒;只查核 = 罰它做一件拿不到資料的事。
> 這兩個數字是併之前當場數出來的,不是從上一版加減推出來的。
> 🔴 **`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` 與雲端 sessiontrigger 那幾支)。
> 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。
> **同一個病,只是換一個檔案。**
> 📌 **`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`)——**同一個病,只是換一欄。**
---
## 怎麼讀這張表
| 符號 | 意思 |
|---|---|
| 🛑 擋 | 條件不滿足就**真的擋下**這個動作(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` 一樣擋**——那是「沒有期限」穿了一件期限的衣服,盤點時每一格看起來都有值,其實一格都沒有。只管「建里程碑」這個動作,查詢/改別的欄位都放行。 | 🛑 擋 |
| `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`(教訓紀錄)新增一條「機制可以防止」的教訓,卻沒附對應票號就擋下——沒有票號的教訓沒有人會回頭處理。 | 🛑 擋 |
| `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 要派工給別的 AIsubagent)之前(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`、雲端 sessiontrigger、以及 Bash 的 `claude -p`)——回覆也是派工,同一套規矩;擋下來時會把那段內容**原文印出來**方便直接貼上票。subagent 往上回報(`to: "main"`)不管,那是交件不是派工。 | 🛑 擋(+注入) |
| `roster-guard.sh` | 派工要**指名派給誰**`Task``subagent_type` 得是 `agents/` 名單上的名字(`isep-hand``arcrun-hand``scout`…),沒指名或指了不存在的名字就擋下,並把整份名單印出來。指對了它會**自動把那位工人的檔案原文注入**給它——你是誰、負責哪個 repo、開工前先讀什麼、你的紅線是什麼(這些每個工人都不一樣,共通規定補不了)。名單是資料:加一個工人=在 `agents/` 加一個 `.md`,不必改任何 hook。 | 🛑 擋(+注入) |
## 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 注入) |
| `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-23inkstone/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`mergerejectchanges 各一個動作)。 | 🛑 擋(響過就退讓) |
| `countdown-guard.sh` | **每一則回覆都自己說出「這件事已經花了多久/距今天收工線還剩多久」**,而且不必有人記得。一支閘掛兩個事件:你送出訊息時它先把算好的那一行放到 AI 眼前(`UserPromptSubmit`),AI 想收工時它再看這一回合的回覆裡到底有沒有那一行,沒有就擋一次要它補(`Stop`)。**只做前半 = 又一個會被忽略的提醒**,所以兩半綁在一起。已過時間從**這段對話的第一則訊息**算起(=一個 release 的起點);收工線是台北 16:00,過線後四小時內顯示「已超過」,不會把超時悄悄換算掉。子 session、純工具回合、已經提醒過一次的,全部放行。 | 🛑 擋(至多攔一次) |
| `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
(無輸出)
```
🔴 **上面那兩行驗不出「表格漏列」**——它們比的是 `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` 的說明文字寫「42 支機械閘(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 支,逐行核對過源碼(不是只讀檔頭):
1. **`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` 一律放行。帶憑證的「實名讀」也放行但會留一筆紀錄。**與表格描述一致。**
2. **`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 兩個白名單)。
3. **`empty-handed-stop-guard.sh`**——表格寫「這輪零動作卻想停就擋,至多攔一次」。
源碼核對:讀 transcript 數這回合 `tool_use` 出現次數,0 次 → exit 2;`stop_hook_active` 已為真(代表已經擋過一次)→ 直接放行,不會卡死。**與表格描述一致。**
4. **`no-ticket-no-dispatch.sh`**——表格寫「派工單沒工單號,或票已關閉/不存在就擋」。
源碼核對:從 `tool_input.prompt` 抓 `【工單】owner/repo#N`,抓不到 → exit 2;抓到但打 Gitea API 查到 `state=closed` 或查不到(`missing`)→ exit 2
查不到網路(token 拿不到、API 連不上)→ 放行(fail-open,避免網路抖動卡死工作)。**與表格描述一致**,且多了一個表格沒特別寫的細節:網路問題不擋。
5. **`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 等於宣稱我逐支重讀過源碼,而我沒有。**
> 要恢復一個數字,就重跑一次上面「抽驗」那件事,然後把當天的日期寫上去。
---
## 這份表怎麼跟實況對帳(半年後怎麼發現漂移)
1. **有沒有新閘沒被收進這張表**:跑本文「落差偵測」段落**第二組**(`inv_fs` vs `inv_table` 的兩行 `comm`**加第三格 C**,比對 `hooks/*.sh` 的檔名清單跟這張表列出的閘名清單。**不是第一組**——第一組比的是 `hooks.json`,只驗證有沒有註冊,驗不了有沒有寫進這張人話表。2026-08-27 漏掉的那兩列,第一組是綠的、第二組才紅。**抓表格列的那組 `grep` 一定要帶 `^\| \`` 錨點**,理由與拿掉之後會冒出的 3 個假警報,就寫在那一段裡。
2. **有沒有閘的行為跟這裡寫的不一樣了**:抽幾支重新讀一次源碼,跟這裡的「一句話」對一遍——就是本文「抽驗 5 支」做的事,可以照同樣方法定期重做。
3. **`.claude-plugin/plugin.json` 與 `README.md` 的數字**:這兩處各自寫了一次「幾支、幾條註冊」,前面已經抓到一次對不上(42/52 vs 實際 43/53)。這兩個數字沒有機制保證跟著 `hooks/` 目錄自動更新,是本表發現的第一個具體漂移案例。