feat: 收件端核對——A 的結果寫進 B 的票會被擋(inkstone/ISEP#132)

派工有票號、工人有名字(roster)、留言有【身份】三件都在派工/回覆那頭;
缺的是總管改票的那一刻,把「要改的票」跟「這個 session 派出去的那一批票」比一次
(leo 2026-09-05:「明明已經都憑 milestone 和票號了」——subagent 回報的東西被
總管當成另一件事更新)。

- hooks/dispatched-ticket-stamp.sh(PostToolUse Agent|Task):按 session 記
  「哪張票派給了哪位工人」(subagent_type,剝掉 plugin 前綴)。回報(SendMessage)不記。
- hooks/report-ticket-match-guard.sh(PreToolUse Bash):總管 say/close/handback
  (+ Gitea API 側門)一張不在那批派工票裡的票 → 擋一次,印「X 被派的是 #A」。
  判準是票號(唯一識別碼)往放行方向比對,不看措辭(leo 2026-08-17 已證偽文字層封路)。
  沒派過工的 session 整支休眠;動被派出去的那張放行;讀取/別的動詞放行;
  同一張只擋一次(重下就過);逃生口 report-match-ok。
- 測試 scripts/test-report-ticket-match-guard.sh 31/31(離線,戳記走 ISEP_STAMP_DIR)。
- 盤點:hooks 62→64、註冊 88→90、scripts 66→67(在這棵樹上實數);
  docs/hooks-inventory.md、docs/TESTING.md A43、plugin.json 描述一併更新。
  版本號待總管定(未動 plugin.json version,避開 check-version-consistency)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5
This commit is contained in:
2026-09-18 18:27:23 +00:00
parent ebe71e3283
commit 7b51db3d0b
7 changed files with 423 additions and 3 deletions
+24
View File
@@ -1342,6 +1342,29 @@ bash hooks/tests/gitea-arm-to-github-armed.test.sh .
- ④ 前兩條紅 ⇒ **沒拿到核准也寫了保險**——這是 D20 等級的洞,最嚴重
- ④ ★ 紅 ⇒ 擋下之後教的指令又印回相對路徑(站在 InkStoneCo 會叫到舊複本,`inkstone/ISEP#112` 同病)
### A43 — 收件端核對:把 A 的結果寫進 B 的票會被擋:31 條
```
bash scripts/test-report-ticket-match-guard.sh
```
**該看到**`31 通過 / 0 失敗`。**全程離線**:戳記走 `ISEP_STAMP_DIR`(不碰 `/tmp` 正式戳記)、
不打 Gitea、不開任何測試票。派工(餵 `Task` payload 給 `dispatched-ticket-stamp.sh`)與
改票(餵 `Bash` payload 給 `report-ticket-match-guard.sh`)都是手寫 payload。
**它在守什麼**`inkstone/ISEP#132`leo 2026-09-05:「明明已經都憑 milestone 和票號了」):
subagent 回報的東西被總管當成**另一件事**更新(把 A 的結果寫進 B 的票)。派工有票號、
工人有名字、留言有【身份】三件都在派工/回覆那頭;缺的是總管改票那一刻的核對——
**要改的票,在不在「這個 session 派出去的那一批票」裡**。判準是票號比對,不看措辭
leo 2026-08-17 已證偽文字層封路)。
**失敗**
- 「該擋」五條任一紅 ⇒ 派了 #A 卻去動 #Bsayclosehandback 兩條 API 側門)沒被擋,
漏掉的正是這張票要修的病;側門兩條特別看——只封 `scripts/ticket` 正門等於沒封
- **「不該擋」任一紅 ⇒ 誤攔,比漏擋更糟**:動被派出去的那張、兩條線並行各落各票(驗收條件第 2 條)、
沒派過工的 session(休眠)、只擋一次後重下、逃生口、GET 讀取、別的動詞(wherenewtriage
decideminesubtask)——這幾條是這支閘不會「天天誤擋被學會忽略」的保證
- 戳記那組紅 ⇒ 沒記到工人+票、plugin 前綴沒剝掉、去重壞了,或 `SendMessage`(回報)被誤記成派工
- 訊息那四條任一紅 ⇒ 擋下來說不出「X 被派的是 #A」,或沒給那條走得通的出路(重下就過/`report-match-ok`
---
## B. 只有 leo 能跑的(雲端)
@@ -1472,6 +1495,7 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
| **A24 分身住 repo 裡面+收工被收(G/H 群)** | isep-hand | ✅ 110/1102026-09-07inkstone/ISEP#147)+舊閘實跑 20 條紅(訊息+G/H,B/D/E/F 全綠)+本線自己就是 dogfood(分身開在 `ISEP/.worktrees/ISEP-147`,主目錄 `git status` 乾淨) |
| **A40 sweep 現場整理(七個分身的 fixture)** | isep-hand | ✅ 42/422026-09-07inkstone/ISEP#147);**leo 的 Mac 現場**要他自己跑 `sweep ~/tech_projects`(先 dry-run)——這台機器碰不到 |
| **A42 票上回碼的保險寫在出貨線讀得到的地方** | isep-hand | ✅ 18/182026-09-17inkstone/ISEP#156);修之前的腳本 11 條紅 |
| **A43 收件端核對:A 的結果寫進 B 的票會被擋** | isep-hand | ✅ 31/312026-09-18inkstone/ISEP#132);全程離線(戳記走 ISEP_STAMP_DIR |
| **A26 雲端憑證清單這台拿得到** | isep-hand | ✅ 11/112026-09-01inkstone/ISEP#115→5526554155425577)+兩條「該紅」方向實跑 |
| **A16 放行的門真的打得開** | 總管 | ✅ 17/172026-08-28inkstone/ISEP#90 |
| **A17 未推警察不誤攔雲端分支** | 總管 | ✅ 10/102026-08-28inkstone/ISEP#90 |
+13 -2
View File
@@ -1,4 +1,4 @@
# 62 支閘,白話盤點表
# 64 支閘,白話盤點表
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
@@ -7,10 +7,19 @@
## 一句話結論
`hooks/` 底下有 **62 個 `.sh` 檔**`hooks.json` 實際掛上 **88 條註冊**(同一支閘常被多種情境同時掛上);
`hooks/` 底下有 **64 個 `.sh` 檔**`hooks.json` 實際掛上 **90 條註冊**(同一支閘常被多種情境同時掛上);
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
> 📌 **`inkstone/ISEP#132`2026-09-18,待總管定版)+2 支、+2 條、腳本 66→67**:收件端核對。
> `report-ticket-match-guard.sh`A 組,PreToolUse `Bash`)+`dispatched-ticket-stamp.sh`H 組,PostToolUse `Agent|Task`)。
> 派工有票號、工人有名字、留言有【身份】三件都在派工/回覆那頭;缺的是總管改票的那一刻,把「要改的票」跟
> 「這個 session 派出去的那一批票」比一次——不在那批裡就擋一次(leo 2026-09-05:「明明已經都憑 milestone 和票號了」)。
> 判準是唯一識別碼(票號)往放行方向比對,不看措辭;沒派過工的 session 整支休眠、同一張只擋一次、讀取/別的動詞全放行。
> `scripts/` 1`test-report-ticket-match-guard.sh`31/31)。
> **64 支、90 條、67 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`64、
> `grep -c '"command":' hooks/hooks.json`90、`ls -p scripts | grep -v / | wc -l`67)。
> 📌 **`inkstone/ISEP#113`2026-09-18,待總管定版)+0 支、+0 條、腳本不變**:沒加閘、沒加腳本。
> `dispatch-format-guard.sh`(出路 `scripts/ticket say`)與 `factory-idle-guard.sh`(主閘+副閘的 `scripts/ticket where`
> 印出來的 ticket 指令改成**絕對路徑**`${CLAUDE_PLUGIN_ROOT}/scripts/ticket`)——收工方 cwd 常是 InkStoneCo
@@ -206,6 +215,7 @@
| `search-is-not-proof-guard.sh` | AI 用知識庫的**搜尋**找到一筆東西,然後把那筆東西的原始編號直接貼進票裡或派工單裡,當成「某個功能壞了」的證據——而它從沒用正規的檢索路徑去查過那筆——擋一次。你關心的是:不會再有人拿錯的方法量出一個錯的結論,然後退回一張其實已經做完的票。 | 🛑 擋(同一輪只擋一次) |
| `line-needs-own-worktree.sh` | 一條派工線想在**大家共用的那份工作目錄**裡切分支(`git checkout``git switch`)就擋下,叫它先開一份自己的 worktree——**開在 repo 裡面的 `.worktrees/`,不是旁邊**`scripts/worktree open``inkstone/ISEP#147`leo 在 Finder 看到一排 `ISEP-wt-117``InkStoneCo-wt-112` 就是舊版教出來的)。同一支閘也掛在線收工那一刻(PostToolUse `Agent|Task`):從派工單的【工單】認出分身,分支推了且乾淨就收掉,沒推/有東西沒交代就點名分支與路徑——不靜默、不 force、不刪沒推的 commit。你關心的是:一個 repo 在 Finder 只有一個資料夾,而且不會再有人交回工作時把某個 repo 留在自己的分支上。**總管在自己的目錄裡切分支不管**(那是他的目錄);已經在自己 worktree 裡的切換全部放行;還原檔案(`git checkout -- 檔`)不管。擋下來的同時會順手把「指向空氣」的 worktree 登記清掉。切分支的出路是閘訊息印的那一行 `WORKTREE_OK=1 git -C … checkout …`——認的是**指令字串裡**的這個前綴,原樣貼回去就過(`inkstone/ISEP#109`→6587)。 | 🛑 擋/收工 🚨 點名 |
| `diagnosis-evidence-guard.sh` | 你想把一段有份量的東西寫上票,而這個 session **沒派人去查過那張票**、那段話裡也**沒有任何走得過去的出處**(檔案:行號/commit/comment 號/票號/網址/貼出來的實測輸出)就擋一次——未經調查的診斷寫在票上會長得像事實,工人會照著它去驗證而不是去查。派過人查、或把出處帶上,都放行;一句短回覆不管。 | 🛑 擋 |
| `report-ticket-match-guard.sh` | 這個 session 派過工(有工人被指到某張票),總管卻對「不在那批派工票裡」的某張票做 `ticket say``close``handback`(或直接打 API 改它)時,擋一次,並印出「哪位工人被派的是哪張票」。守的是 2026-09-05 leo「明明已經都憑 milestone 和票號了」那件事——subagent 回報的東西被總管當成**另一件事**更新(把 A 的結果寫進 B 的票)。判準是**票號比對**(要改的票在不在這個 session 派出去的那一批裡),一個字的措辭都不看。**沒派過工的 session 整支休眠、一律放行**;動的就是被派出去的那張 → 放行;純讀取(GET)、`where``new``triage``decide``mine``subtask` 等別的動詞全放行。**同一張沒派出去的票只擋一次**(真的要動它就重下同一個指令,不必多打字);真有例外在指令裡加 `report-match-ok` 留痕(`inkstone/ISEP#132`)。 | 🛑 擋(同一張至多一次) |
## B. AI 想寫檔案/改程式碼的當下(PreToolUse / Write·Edit·MultiEdit
| 閘名 | 對你意味著什麼 | 動作 |
@@ -325,6 +335,7 @@
| `baton-handback-guard.sh` | 一條派工線做完了,就去看它那張票有沒有**指派給人/標籤有沒有說它卡在哪/有沒有寫下一步**,缺哪一格就當場說出來。你關心的是:不會再有票做完了卻沒人接手,躺在那裡沒人發現。**只管前景派工**:背景派工送出的那一刻工人才剛出發,這裡不出聲,等它交件時由 F 組的 Stop 那半查(`inkstone/ISEP#153`)。 | 📝 記錄(提醒,不擋) |
| `kbdb-evidence-stamp.sh` | AI 每次去知識庫拿資料,就記一筆「這是**搜尋**搜到的,還是**走正規檢索路徑**拿到的」。你關心的是:它等一下要拿這筆資料下結論時,有東西可以對照它走的是哪條路。 | 📝 記錄 |
| `investigate-first-stamp.sh` | 你真的**派人去查某張票**之後,按「session + 票號」留一個時間戳——給 `diagnosis-evidence-guard.sh` 判斷「這件事有沒有人查過」用。按票分而不是按 session 分:早上派人查了 A 票,不該等於解鎖了 B 票的診斷。 | 📝 記錄 |
| `dispatched-ticket-stamp.sh` | 你一派工出去(`Task``Agent`,派工單有【工單】),就按 session 記下「哪張票派給了哪位工人」(`subagent_type` 那個名字)——給 `report-ticket-match-guard.sh` 在總管改票時核對用。按 session 存成一份清單(要一次看全部派工),跟 `investigate-first-stamp` 按票各存剛好相反,因為兩者問的問題不同。回報(`SendMessage`)不記,那條路上沒有工人名字。 | 📝 記錄 |
## I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit
| 閘名 | 對你意味著什麼 | 動作 |