併進 main(#96 雲端接線/#100 v0.12.0/#99 主線唯一),解 TESTING.md 撞號(inkstone/ISEP#92)

唯一衝突在 docs/TESTING.md:兩條線各自加了一節、**都叫 A16**。
兩邊內容都留,我這一節改成 A19(main 已用掉 A16/A17/A18),
「目前狀態」表的那一列跟著搬到 A18 後面。

main 側本來就有的 A11/A12/A14/A15 重號**沒有動**——那是它兩條線各自編的,
不是這次造成的,動它會把 diff 炸開。

沒有碰的:.claude-plugin/plugin.json(維持 main 的 v0.12.0,版本由總管統一定)、
hooks/hooks.json、docs/hooks-inventory.md——這三個檔本線一個字都沒改,
自動合併拿到的就是 main 那份。

盤點數字在合併後的樹上重數(不是相加、不是抄別人的):
  hooks/*.sh 54 hooks.json 註冊 71 commands 7 skills 2 scripts 頂層 38
docs/hooks-inventory.md 寫的 54/71 **是對的**;README 寫的 48/59/23
**三個都錯**,已改成實數,並把「怎麼重數」的指令寫進去——
這種數字會漂是因為沒人知道它怎麼來的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
This commit is contained in:
claude-code
2026-08-28 00:48:49 +00:00
27 changed files with 3212 additions and 40 deletions
+145 -2
View File
@@ -91,6 +91,40 @@ bash scripts/test-baton-handback-guard.sh
- 「不該報」4 條任一紅 ⇒ 每條線收工都被念一次,警報會被學會忽略
- 「票已關」那條紅 ⇒ 棒子已經到終點還在催,那是最典型的假警報
### A14 — PR 一定要有結論:52 條
```
bash hooks/tests/pr-verdict-guard.test.sh
```
**該看到**`52 通過 / 0 失敗`。全程離線(走 `PR_VERDICT_FIXTURE`),
**不打 Gitea、不開測試 PR、不留任何測試票**;狀態檔走 `PR_VERDICT_STATE_DIR`
**它在守什麼**`inkstone/ISEP#81`2026-08-27 實查):最久的三個 open PR 躺了兩星期
`inkstone/arcrun-rag#91` 14 天、`inkstone/Arcrun#116` 14 天、`inkstone/Arcrun#104` 15 天),
而 v0.9.0 的 46 支閘**一支都沒有在管 PR**。票看起來是「已交付」,
東西卻沒進 main ⇒ 沒進版本 ⇒ **leo 手上永遠不會出現它**
**失敗**
- B 群(⑨⑩⑪,票上那三個真跡)任一紅 ⇒ 今天這個形狀會再發生一次
- C 群 ⑭⑰ 紅 ⇒ 驗收條件 1/3 沒過:「放著不管」抓不到,或「merge 了分支還在」放行
- **A 群任何一條紅 ⇒ 誤攔,這比漏擋嚴重**。②③特別重要:
指派給人/掛 Human 之後還在點名,就是 ISEP v0.6.0 divergence §B4 記過的那個坑
(「`s/review` 佇列非空即 block」⇒ 佇列本來就不會空 ⇒ **總管永遠停不下來**
- E 群 ㉙ 紅 ⇒ 閘訊息裡承諾的出路(「把它指派給那個人,本閘立刻不再點名它」)是假的
- D 群 ㉓ 紅 ⇒ 判準從「識別碼比對」滑成「只要提到票號就放行」,閘會被任何一句話關掉
> 📌 **真跡不只在 fixture 裡跑過**:同一支閘打真實 Gitea 也驗過,
> 一次點名 7 個沒有結論的 PR,含票上那三個(15/14/14 天)。
> 指令:`printf '{"session_id":"x","transcript_path":"/nonexistent"}' | bash hooks/pr-verdict-guard.sh`
> ——**唯讀,不改動任何 PR**。
### A15 — 給結論的那支工具自己說得出要打哪幾通 API
```
python3 scripts/pr-verdict list
python3 scripts/pr-verdict merge inkstone/ISEP#71 --dry-run
```
**該看到**`list` 印出 open PR 與「有沒有人被指派」;`merge --dry-run` 印出
`[dry-run] POST …/pulls/71/merge``[dry-run] DELETE …/branches/<分支>`**一通都不真的送出**。
**失敗**`--dry-run` 底下出現真實的寫入結果 ⇒ 這支工具沒辦法在按下去之前被檢查。
### A14 — 每一則回覆都自己說出拖了多久:20 條
```
bash hooks/tests/countdown-guard.test.sh
@@ -120,6 +154,49 @@ bash hooks/tests/countdown-guard.test.sh
subagent 的回覆不是給 leo 看的),所以在一條 subagent 裡跑,B 群會**全綠而且是假綠**。
測試檔自己會把它清掉——2026-08-28 第一次跑就撞到這個,才補上去的。
### A16 — 現在的主線是哪一個:24 條
```
bash hooks/tests/mainline-focus-guard.test.sh
```
**該看到**`通過 24 條,失敗 0 條`。**全程離線**:狀態走 `ISEP_COUNTDOWN_STATE_DIR`
時鐘走 `ISEP_COUNTDOWN_NOW`、「那張票掛在哪」走 `ISEP_MAINLINE_FIXTURE`
並把 `TICKET_HOST` 指到一個連不上的位址——**任何一條真的走到網路,會在那裡當場失敗,
不會靜靜地變成假綠**。
**它在守什麼**`inkstone/ISEP#82`leo 2026-08-27):
「**14 個里程碑同時亮著卻不知道該看哪個**」。當天實查 14 個 open milestone
其中「Mira 現代化」這個名字同時活在 5 個 repo,5 個已經逾期
⇒ SOP 說的「只做**該** milestone 的事」**沒有指涉對象,等於不存在**。
**判準**:問「現在的主線是哪一個」,`scripts/mainline` 回一個答案,而且只有一個。
**失敗**
- A 群任一條紅 ⇒ 答案不唯一,或標了新的舊的還在——那就回到 14 個一起亮的狀態
- ⑧ 紅 ⇒ **目標宣告沒有自己出現**(票上驗收第 2 條),leo 又要自己去翻
- ⑨ 紅 ⇒ 主線的名字沒掛在被查核的 ⏱ 那一行上 ⇒ 它到不了 leo 眼前
(本票刻意不另立第二道會擋人的閘,就是靠這一格成立)
- ⑩ 紅 ⇒ 用「期限最近的那個」蓋過了被指定的主線。**猜的不准蓋過指定的**
- ⑬⑭ 紅 ⇒ 派了不在主線上的票沒被問一次,或問了卻沒給補收/跳線兩條路
- ⑯ 紅 ⇒ **鬼打牆**:同一張票被擋第二次。至多擋一次是硬要求
- **⑲–㉔(不該擋)任一條紅 ⇒ 誤攔,這比漏擋嚴重**。特別是 ⑲(沒有主線就一律放行,
票上驗收第 4 條)與 ㉒(問不到那張票掛在哪 ⇒ 放行)——
**「讀不到」不等於「不屬於」,把這兩件事講成同一句是最貴的錯**
📌 這一條線的另一半是**指令**,不是閘:`scripts/mainline``show``list``set`
`clear``refresh``adopt``has`)。要打網路的那幾個(list/set/adopt)不在測試裡跑,
因為它們**只有真的 Gitea 才驗得了**——驗法見 A17。
### A17 — 主線那支指令真的問得到 Gitea(要網路)
```
python3 scripts/mainline list
```
**該看到**:一份 open milestone 清單,**跨多個 repo**`ISEP` / `Arcrun` / `arcrun-rag` / `mira`…),
每一行左邊是可以直接貼給 `set``owner/repo#<milestone id>`
**失敗**
- `一個 open milestone 都沒讀到` ⇒ 這台機器拿不到 Gitea。**那是讀不到,不是沒有**
——不要因此標一個猜的主線
- 只有單一 repo ⇒ 沒有跨 repo,等於沒看到全局
### A15 — 發通知不等於部署:37 條
```
bash hooks/tests/prod-write-guard.test.sh hooks/prod-write-guard.sh
@@ -131,7 +208,68 @@ bash hooks/tests/prod-write-guard.test.sh hooks/prod-write-guard.sh
- 「卡點二」2 條任一紅 ⇒ 「談論它」又被當成「執行它」(同款第八次),
或是剝了內文之後連真的部署都放行了
### A16 — 下游做完時頂層跟著關:49
### A16 — 「總管可以放行」那道門真的打得開:17
```
bash hooks/tests/gate-ok.test.sh
```
**該看到**`通過 17 條,失敗 0 條`
**它在守什麼**inkstone/ISEP#90 ④,2026-08-28 實查):三支閘
`prod-write-guard``main-and-prod-push-guard``stage-before-prod-guard`
都是「擋下來、但**總管看過就能放行**」的設計,而**既有測試只驗了擋得住,
一條都沒驗過放得開**。於是這個 bug 活了很久:`stat -f %m` 在 GNU coreutils 上
是「檔案系統資訊」,它一邊回非零一邊吐一整段文字 ⇒ 秒數被污染 ⇒
**在 Linux(=每一個雲端 session)上戳記永遠不被接受** ⇒ 那三支閘在雲端等於純擋,
而閘不會告訴你門是壞的。
**失敗**
- ②⑤(門打得開)紅 ⇒ 逃生口又焊死了,那三支閘在雲端變回純擋
- ⑥⑦⑧(門沒變寬)任一紅 ⇒ **更嚴重**:綁 repo/有效期/空戳記那三條性質是
08-11、08-12 兩次真的被穿透之後才補上的,不准為了「好放行」而鬆掉
- ⑫ 紅 ⇒ `gate-ok` 在解不出 repo 時留下了一枚註定打不開的空戳記,
那會讓人以為門開了(而空戳記本身就是 08-12 那把萬用鑰匙的形狀)
- ⑰ 紅 ⇒ 判準又變回「行為取決於 cwd 裡有沒有一個叫 `%m` 的檔」。
這條是這個 bug 的最後一層:**GNU 的 `-f` 是布林旗標,`%m` 被當成另一個檔名運算元**
⇒ 有那個檔就 `exit 0``||` 連跑都不跑。所以修法不能只是「把順序反過來」,
**每一步都要驗它是不是純數字**
⚠️ 這支會真的寫 `/tmp` 的戳記檔(那是閘寫死的路徑)。**不要在「已經蓋好戳記正要推東西」
的當下跑它**——它會把那枚戳記洗掉。
### A17 — 未推警察不會對雲端的工作分支亂叫:10 條
```
bash hooks/tests/unpushed-police.test.sh
```
**該看到**`通過 10 條,失敗 0 條`。全離線(用本機 bare repo 當「遠端」)。
**它在守什麼**inkstone/ISEP#90 ①):雲端 session 開出來的工作分支天生沒有 upstream,
內容卻等於遠端 main ⇒ 舊判準「沒 upstream=從沒推過」讓**每個雲端 session、每次收工
都被攔一次**08-27 一個 session 五次全是誤報)。
**失敗**
- A 群(①—⑥,不該報)任一紅 ⇒ **誤攔**,比漏擋嚴重:永遠在響的警報=訓練人忽略它,
下一條真的失蹤的分支會混在雜訊裡
- B 群(⑦—⑩,該報)任一紅 ⇒ 為了不吵而改成放行了,那是把閘關掉不是修好
- 特別看 ④:問不到遠端(離線)**不准當成「你沒推」**
### A18 — 信標會講出雲端會壞掉的那三件事:14 條
```
bash hooks/tests/isep-presence-beacon.test.sh
```
**該看到**`通過 14 條,失敗 0 條`。全離線。
**它在守什麼**inkstone/ISEP#90 ②③):信標原本只證明「有一份 plugin 載入了」,
不證明「載入的是哪一份」——08-27 雲端載 0.3.9、main 0.9.0,差 7 個 release
而它照樣是綠的。
**失敗**
- ⑫⑬⑭(信標永遠在)任一紅 ⇒ **最嚴重**:那一行消失=leo 會判定這個 session 零閘
- ③ 紅 ⇒ 版本落差報不出來,`ISEP#67` 那個病又變回看不見
- ⑤⑦ 紅 ⇒ 舊複本遮蔽正門抓不到(`InkStoneCo/scripts/ticket` 那件)
- ⑥ 紅 ⇒ **誤攔**:同步過的複本被念,人就學會忽略它
- ⑪ 紅 ⇒ 內層迴圈變數撞名的回歸(同一個 `.claude` 下第二個殘骸會被靜靜跳過)
### A19 — 下游做完時頂層跟著關:49 條
```
bash scripts/test-ticket-handoff-writeback.sh
```
@@ -360,15 +498,20 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
| A3 打 tag 閘 | 總管 | ✅ 3/3 |
| **A4 新增 Gitea 東西側門閘** | 總管 | ✅ 24/242026-08-27inkstone/ISEP#72 |
| **A13 戳記證明看過不是跑過** | 總管 | ✅ 17/172026-08-27inkstone/ISEP#72→4873 |
| **A14 PR 一定要有結論** | 總管 | ✅ 52/522026-08-28inkstone/ISEP#81)+真實 Gitea 唯讀重演 |
| **A15 pr-verdict --dry-run** | 總管 | ✅(2026-08-28 |
| **A14 回覆自己說出拖了多久** | 總管 | ✅ 20/202026-08-28inkstone/ISEP#63 |
| **A15 發通知不等於部署** | 總管 | ✅ 37/372026-08-28inkstone/ISEP#63 |
| **A16 下游做完頂層跟著關** | 總管 | ✅ 49/492026-08-28inkstone/ISEP#92 |
| A5 搜尋跨 repo | 總管 | ✅ |
| A6 標籤對齊+冪等 | 總管 | ✅ 14 repo,第二次 0/0 |
| **A9 人閘警察管路** | 總管 | ✅ 14/142026-08-26 |
| **A10 人閘警察準度** | 總管 | ✅ 9/9,連跑三次(2026-08-26),A 群誤攔 0 |
| **A11 派工單只剩票號** | 總管 | ✅ 19/192026-08-27 |
| **A12 留言身份欄(兩道門)** | 總管 | ✅ 11/112026-08-27 |
| **A16 放行的門真的打得開** | 總管 | ✅ 17/172026-08-28inkstone/ISEP#90 |
| **A17 未推警察不誤攔雲端分支** | 總管 | ✅ 10/102026-08-28inkstone/ISEP#90 |
| **A18 信標會報雲端接線缺陷** | 總管 | ✅ 14/142026-08-28inkstone/ISEP#90 |
| **A19 下游做完頂層跟著關** | 總管 | ✅ 49/492026-08-28inkstone/ISEP#92 |
| A7 plugin 裝得起來 | 總管 | ✅ |
| **A8 新 session 閘會觸發** | 總管 | 見本版 release note |
| **B1B5 雲端** | **leo** | 還沒跑(機器碰不到 Cloud environment |
+195
View File
@@ -0,0 +1,195 @@
# 雲端 session 的閘為什麼跟地端不一樣(inkstone/ISEP#90
> leo 的目標一句話:**讓雲端 session 跟地端拿到同一組閘、同一組能用的工具。**
雲端不是「地端少幾支閘」,是**同一批閘在雲端的行為不一樣**。四個實例,
每一個都穩定重現,而且每一個都**不會自己喊痛**——這才是它們活這麼久的原因。
---
## 四個缺陷,兩種病
| | 缺陷 | 病 |
|---|---|---|
| ① | 未推警察每個雲端 session 都誤攔 | 判準在雲端**不成立** |
| ② | `scripts/ticket` 在雲端拿不到 token | 跑到的是**另一份**(舊複本) |
| ③ | 已刪除的機制在雲端重生 | 載到的是**另一版**0.3.9 vs 0.9.0 |
| ④ | 總管的「我確認過了」出口打不開 | 判準在雲端**不成立**(平台差異) |
①④ 是同一種病:**閘的判準寫的是地端才成立的假設**。
②③ 是同一種病:**同一個東西有兩份,而雲端跑到的是舊的那份**。
---
## ① 未推警察:判準是「有沒有 upstream」,而雲端的分支天生沒有
雲端 session 開出來的工作分支沒有 upstream,**內容卻等於遠端 main**
```
薄殼 HEAD = 081c547d90dc2d80692485af083ca1f71f2094f2
GitHub 遠端 main = 081c547d90dc2d80692485af083ca1f71f2094f2 ← 同一顆
```
⇒ 每個雲端 session、每次收工都被攔一次(2026-08-27 一個 session 五次全是誤報)。
**修法**:判準改成「**遠端有沒有這顆 commit**」(`git ls-remote`)。
本機的 remote-tracking ref 只答得準「有」——08-28 實測那份 `origin/main` 落後遠端 4 天
——所以答「沒有」的時候才打網路。三態:有/沒有/**問不到**;問不到一律不報。
**順手補的**`$CLAUDE_PROJECT_DIR` 在雲端是**薄殼根**,真身在 `$TOP/InkStoneCo/`
舊的掃描清單四個路徑在雲端一個都不存在 ⇒ **真身有東西沒推,這支閘一輩子不會知道。**
同一支閘在雲端既亂叫、又看不到該看的地方。
---
## ② `scripts/ticket`:跑到的是舊複本,不是 plugin 那一份
- ISEP 的 `scripts/ticket` **早在 2026-08-20 就修好了**(掃所有 remote 環境變數 fallback
- 但雲端 cwd 是真身,那裡有一份 `InkStoneCo/scripts/ticket` 的**舊複本**
取 token 邏輯還停在「只認名叫 `gitea` 的 remote」,而 `bootstrap.sh` 把 Gitea 設成 `origin`
- ⇒ 雲端一律死在「拿不到 gitea token」
🔴 **後果比「一支腳本壞了」嚴重**`scripts/ticket` 是開票/留言的**正門**
它一壞,人就繞過去直接打 Gitea API——**而那正是 `ticket-api-bypass-guard.sh` 在防的事**。
**一道閘把人逼去走它自己禁止的那條路,那道閘就是在製造違規。**
**ISEP 這半的修法**:信標在 session 開頭就點名「專案裡有 ISEP 腳本的舊複本,
而且**內容不同**」。判準不是檔名一樣,是**檔名一樣而內容不同**——同步過的複本不吵。
**真身那半(把那份複本同步或刪掉)不在 ISEP,要在 `inkstone/InkStoneCo` 修。**
---
## ③ 已刪除的機制在雲端重生
```
ISEP main .claude-plugin/plugin.json → 0.9.0
雲端實際載入 → 0.3.9 ← 差 7 個 release
```
0.3.9 裡還活著兩支在 v0.9.0 已整支刪除的 hook ⇒ `.claude/pending-verification/`
被清掉之後又長回來。**一個看不見的版本落差,會讓已經刪掉的機制在別人的工作區裡復活。**
**傳輸那半是 `inkstone/ISEP#67`**(新版到不到得了手上),本票不重複那件。
**ISEP 這半能做的是讓它不再看不見**:信標匿名讀 ISEP main 的 `plugin.json`
(D20 判準下屬於「讀」,不需開閘),落後就講清楚差幾版、怎麼重拍快照;
同時掃 `.claude/` 底下**這一份 plugin 的 hooks/scripts 一個字都沒提到**的目錄,
點名它們是殘骸。判準是「plugin 現在還認不認得它」,**不是關鍵字黑名單**
leo 2026-08-17 已證明那條路 8 次誤攔、0 次正確攔截)。
---
## ④ 「總管可以放行」的門,在雲端是焊死的
三支閘(`prod-write-guard``main-and-prod-push-guard``stage-before-prod-guard`
都是「擋下來、但**總管看過就能放行**」。它們判斷戳記新不新都用同一行:
```sh
MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
```
macOSBSD stat)上 `-f %m` 就是 mtime,對的。
**GNU coreutils 的 `-f` 是「顯示檔案系統資訊」**,而且它**一邊回非零、一邊往 stdout
吐一整段區塊**
```
$ stat -f %m /tmp/.probe
stat: cannot read file system information for '%m': No such file or directory ← stderr
File: "/tmp/.probe" ← stdout
ID: 0 Namelen: 255 Type: ext2/ext3
```
`2>/dev/null` 吃掉錯誤訊息、`||` 把正確的秒數接在那堆垃圾**後面**
`case "$NOW$MT" in *[!0-9]*) return 1` 必然命中
**在 Linux(=每一個雲端 session)上,那三支閘的戳記永遠不會被接受。**
### 再往下一層:`-f` 根本不吃格式參數,所以連離開碼都不可靠
總管 2026-08-28 自己驗這一格時量到 `exit=0`,而我量到 `exit=1`
**兩個都是真的**,而分歧本身就是這個 bug 最後一塊拼圖:
**GNU 的 `-f` 是 `--file-system`,它是布林旗標、不接格式字串**
`%m` 不是格式,它被當成**另一個檔名運算元** ⇒ 離開碼取決於
「當前目錄裡有沒有一個叫 `%m` 的檔」:
```
$ cd /tmp/statprobe && touch .mt
# A. 沒有名為 %m 的檔(一般情況)
$ stat -f %m .mt ; echo "exit=$?"
stat: cannot read file system information for '%m': No such file or directory
File: ".mt" … ← 檔案系統資訊照樣印到 stdout
exit=1 ← `||` **會**跑 ⇒ 正確的秒數接在那堆垃圾後面
# B. 剛好有一個叫 %m 的檔
$ touch '%m' && stat -f %m .mt ; echo "exit=$?"
exit=0 ← `||` **不會**跑 ⇒ 整包連一個數字都沒有
```
(實測環境:`stat (GNU coreutils) 9.4`
**兩種情況下閘的結果一模一樣**——`MT` 都不是純數字,
`case "$NOW$MT" in *[!0-9]*) return 1` 都必然命中,戳記都作廢:
```
情況 A:非數字 ⇒ return 1 ⇒ 戳記作廢
情況 B:非數字 ⇒ return 1 ⇒ 戳記作廢
```
🔴 **這一層才是真正該記住的教訓**:舊寫法的 `||` fallback 之所以救不了,
**不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定**
——它由「當前目錄裡有沒有一個叫 `%m` 的檔」決定。
一個**行為取決於 cwd 裡有沒有某個檔名**的判斷式,不管跑不跑都是壞的。
⇒ 所以 `lib/mtime.sh` 的修法不是「把順序反過來」而已,是
**每一步都驗它是不是純數字**:這個 bug 的成因正是「命令失敗了卻還是印了東西」,
**只看離開碼會再被騙一次**
📌 這一格的實害(總管 2026-08-28 原話):「我今天為了發一則通知,
用了兩種方式蓋 `prod-write-ok` 都無效,一度以為是權限問題。」
**閘不會告訴你門是壞的**,所以人會往錯的方向查(權限、classifier、設定),
而根因在閘自己身上。
🔴 **後果**:那三支閘在雲端**等於純擋**。總管照著閘自己印的指示做,
做幾次都打不開,**而閘不會告訴他門是壞的**。
**為什麼活這麼久**:既有的三支測試(29/16/10 條)**只驗了「擋得住」,
一條都沒驗過「放得開」**。⇒ 這正是「閘的另外一半從來沒被測過」的代價。
**修法**`hooks/lib/mtime.sh` —— 先 `-c %Y`GNU)再 `-f %m`BSD),
**每一步都驗它是不是純數字**(這個 bug 的成因正是「命令失敗了卻還是印了東西」,
只看離開碼會再被騙一次)。
### 附帶:逃生口收斂成一個入口 `scripts/gate-ok`
原本每支閘的門長得都不一樣,而且藏在被擋下的那則訊息裡:
```
touch /tmp/.prod-write-ok
git rev-parse --show-toplevel > /tmp/.main-push-ok
touch /tmp/.solo-ok-<session_id>
```
兩個後果:**記不住**(抄錯一個字門就打不開),以及**沒有穩定形狀可以事先放行**
——`.claude/settings.json` 的 allow 只能逐條完全比對(現場真的寫著
`Bash(touch /tmp/.prod-write-ok)` 這種一行),多一個 `&&`、換一個 session id
就落在規則之外,然後由權限層自己判斷。
`bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" <閘名> [參數]`**一個名字、一種形狀**
一條前綴規則涵蓋全部,以後新增閘不必再動一次設定。
🔴 **它沒有弱化任何一道閘**:蓋的是同一個檔、同一種語意——單次用完即丟、綁 repo、
綁 session、有效期全部沒動。換掉的只有「怎麼蓋」。
`hooks/tests/gate-ok.test.sh` 的 ⑥⑦⑧ 三條就是在守這件事(那三條性質是 08-11、08-12
兩次真的被穿透之後才補上的)。
---
## 還沒關掉的那兩格(不屬於 ISEP)
| 缺口 | 住在哪 |
|---|---|
| 雲端載到的版本追上 ISEP main | `inkstone/ISEP#67` |
| `InkStoneCo/scripts/ticket` 這份舊複本 | `inkstone/InkStoneCo`(真身) |
ISEP 這一側能做的是**讓它們不再是看不見的**:兩件現在都會在 session 開頭被信標點名。
+29 -6
View File
@@ -1,4 +1,4 @@
# 52 支閘,白話盤點表
# 54 支閘,白話盤點表
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
@@ -7,14 +7,15 @@
## 一句話結論
`hooks/` 底下有 **52 個 `.sh` 檔**`hooks.json` 實際掛上 **67 條註冊**(同一支閘常被多種情境同時掛上);
`hooks/` 底下有 **54 個 `.sh` 檔**`hooks.json` 實際掛上 **71 條註冊**(同一支閘常被多種情境同時掛上);
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
> 現在的寫法是實際數出來的:
> `ls hooks/*.sh | wc -l` 52`grep -c '"command":' hooks/hooks.json` 67
> `ls hooks/*.sh | wc -l` 53`grep -c '"command":' hooks/hooks.json` 68
> `ls hooks/*.sh | wc -l` 53`grep -c '"command":' hooks/hooks.json` 68。
> ⚠️ **冒號不能省**`grep -c '"command"'`(沒冒號)會連 `"type": "command"` 一起數到,回 **120**。
> 本頁 2026-08-27 之前寫的是沒冒號那版——**照著它跑會拿到一個跟本頁不符的數字**。
> **一份會偷偷過期的盤點表,跟沒有盤點表差不多**——見本頁最後「怎麼跟實況對帳」那段。
@@ -32,12 +33,24 @@
> **兩個事件是同一件事的兩半**:只注入 = 又一個會被忽略的提醒;只查核 = 罰它做一件拿不到資料的事。
> 這兩個數字是併之前當場數出來的,不是從上一版加減推出來的。
> 📌 **`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** 條,兩個數字都是加完之後當場數出來的(指令同上)。
>
> 📌 **`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`)——**同一個病,只是換一欄。**
---
@@ -102,6 +115,7 @@
| `micromanage-guard.sh` | 派工單寫得太細(指名檔案函式、編號步驟、要求每做一項回報一次…)就擋下——subagent 該被當成有能力的同事,不是照抄劇本的工具。 | 🛑 擋 |
| `irreversible-dispatch-guard.sh` | 派工單裡出現「刪分支」「drop table」「rm -rf」這類不可逆動作,卻沒寫「先停下來等回覆才執行」就擋下。 | 🛑 擋 |
| `no-ticket-no-dispatch.sh` | 派工單裡沒有寫工單號(`【工單】owner/repo#N`),或那張票已經關閉/根本不存在,就擋下——沒有票號的工作沒有人追得到進度。 | 🛑 擋 |
| `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
@@ -122,6 +136,7 @@
|---|---|---|
| `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 就原封不動——**寧可資料舊,不要把主線弄丟**。跑一次就結束,不輪詢。 | 📝 記錄 |
| `skill-deploy-drift-guard.sh` | 如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
## F. AI 想結束這一輪、要收工的時候(Stop)
@@ -132,7 +147,8 @@
| `worklist-guard.sh` | AI 自己列過的待辦清單裡還有沒做完的步驟,卻想收工寫報告,就擋下,逼它做完剩下的步驟。 | 🛑 擋 |
| `factory-idle-guard.sh` | AI 該去派工卻沒派(工頭停工),就擋下要它交出「已經派工的憑證」,不是隨口說一句「我會催」就算數。**過閘有四條路:現在就派工/把票號寫進那句話/寫一行 `⏸ 等:<在等什麼>`/這一輪收尾在動作上。** 2026-08-23inkstone/ISEP#30)修好「引用被當成主張」——貼原始碼、引用它自己的訊息、否認自己有下一步,都不再被咬。 | 🛑 擋 |
| `mainline-idle-guard.sh` | AI **連續 4 個回合一直在查、一直在讀,卻什麼都沒交出去**(沒派工、沒改檔、沒寫回票)就擋一次。補的是上面兩支中間那一格:「沒動作」空手警察管、「說了下一步卻沒派」稼動率警察管,**而「有動作、但就是不說下一步」以前兩支都放行**。這支**一個字都不讀**,只看有沒有派工/產出/工作區變動。響過一次門檻就加倍(4→8→16),不會變成一直在響的警報。 | 🛑 擋(響過就退讓) |
| `countdown-guard.sh` | **每一則回覆都自己說出「這件事已經花了多久/距今天收工線還剩多久」**,而且不必有人記得。一支閘掛兩個事件:你送出訊息時它先把算好的那一行放到 AI 眼前(`UserPromptSubmit`),AI 想收工時它再看這一回合的回覆裡到底有沒有那一行,沒有就擋一次要它補(`Stop`)。**只做前半 = 又一個會被忽略的提醒**,所以兩半綁在一起。已過時間從**這段對話的第一則訊息**算起(=一個 release 的起點);收工線是台北 16:00,過線後四小時內顯示「已超過」,不會把超時悄悄換算掉。子 session、純工具回合、已經提醒過一次的,全部放行。 | 🛑 擋(至多攔一次 |
| `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、純工具回合、已經提醒過一次的,全部放行。**同一個注入點還帶第二行 `🎯 現在的主線是哪一個`**`inkstone/ISEP#82`)——那一行沒有另外立閘,因為主線的名字本來就掛在被查核的 ⏱ 那一行上。 | 🛑 擋(至多攔一次) |
| `browser-verify-guard.sh` | 這一輪 AI 宣稱「前端驗過了」,卻沒有真的用瀏覽器工具載入過,就擋下——`curl` 抓到 HTML 不算驗過。 | 🛑 擋 |
| `self-drive-police.sh` | AI 想停下來問你「早就決定過的事」(用固定句型判斷,例如「要不要 X」「下一步做什麼」「這交給你」)就擋下,反問它查過 wiki/查過派工表了沒。 | 🛑 擋 |
| `self-drive-judge.sh` | 跟上面同一件事,但改用小模型(haiku)判斷「換句話說」的請示句——防止 AI 只是把「要不要」改寫成「不確定是否符合期待」就閃過上一支閘。 | 🛑 擋 |
@@ -193,11 +209,18 @@ pre-write-guard.sh
pre-write-guard.template.sh
shadow-table-guard.sh
# hooks.json 註冊了、但檔案不存在 → 空(目前沒有這種「指向空氣」的閘)
# 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` 兩列):