把主線接進派工與注入(inkstone/ISEP#82)
- hooks.json mainline-focus-guard 掛 Agent+Task(派工的兩個入口都要守);
SessionStart 多一條 `scripts/mainline refresh`(0.8s,不輪詢)
- hooks-inventory 54 支/71 條(當場數的)+補人話一列;順手修掉上一版重複的那一行,
並把「指向空氣」那格的假警報寫清楚(countdown-milestone-refresh 在 scripts/)
- TESTING A16(24 條離線)+ A17(要網路的那半怎麼驗)
- plugin.json 描述數字對齊;版本不動,待總管定版
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "isep",
|
||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:53 支機械閘(68 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、39 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:54 支機械閘(71 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、37 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||
"version": "0.11.0",
|
||||
"keywords": [
|
||||
"inkstone",
|
||||
|
||||
@@ -154,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
|
||||
|
||||
+21
-6
@@ -1,4 +1,4 @@
|
||||
# 53 支閘,白話盤點表
|
||||
# 54 支閘,白話盤點表
|
||||
|
||||
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
|
||||
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
## 一句話結論
|
||||
|
||||
`hooks/` 底下有 **53 個 `.sh` 檔**,`hooks.json` 實際掛上 **68 條註冊**(同一支閘常被多種情境同時掛上);
|
||||
`hooks/` 底下有 **53 個 `.sh` 檔**,`hooks.json` 實際掛上 **68 條註冊**(同一支閘常被多種情境同時掛上);
|
||||
`hooks/` 底下有 **54 個 `.sh` 檔**,`hooks.json` 實際掛上 **71 條註冊**(同一支閘常被多種情境同時掛上);
|
||||
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
|
||||
下面按「你會在什麼時候撞到它」分組,一支一行。
|
||||
|
||||
@@ -34,6 +33,13 @@
|
||||
> **兩個事件是同一件事的兩半**:只注入 = 又一個會被忽略的提醒;只查核 = 罰它做一件拿不到資料的事。
|
||||
> 這兩個數字是併之前當場數出來的,不是從上一版加減推出來的。
|
||||
|
||||
> 📌 **`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`。
|
||||
@@ -109,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)
|
||||
@@ -129,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)
|
||||
@@ -140,7 +148,7 @@
|
||||
| `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、純工具回合、已經提醒過一次的,全部放行。 | 🛑 擋(至多攔一次) |
|
||||
| `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 只是把「要不要」改寫成「不確定是否符合期待」就閃過上一支閘。 | 🛑 擋 |
|
||||
@@ -201,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` 兩列):
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
# ① UserPromptSubmit ── 注入:把算好的那一行送到眼前(模型不必自己算、也算不準)
|
||||
# ② Stop ── 查核:這一回合的回覆裡到底有沒有那一行;沒有就擋一次
|
||||
#
|
||||
# ── 注入點的第二行:🎯 現在的主線是哪一個(inkstone/ISEP#82)────────────
|
||||
# 同一個注入點,兩行,來源不同:⏱ 算時間,🎯 讀 `hooks/lib/mainline.py` 裡被標定的主線。
|
||||
# 🔴 **Stop 那一半只查 ⏱,沒有跟著擴大成兩個標記**,兩個理由:
|
||||
# ① ⏱ 那一行本身**已經帶著主線的名字**(`countdown.py` 的第三段),
|
||||
# 所以主線是跟著一道**已經驗過的閘**到 leo 眼前的,不是靠新增一道會擋人的閘。
|
||||
# ② 一個回合最多該被擋一次。多要求一個標記 = 多一種被擋的方式,
|
||||
# 而本 repo 心法第 2 條寫死了:永遠在響的警報等於訓練人忽略警報。
|
||||
# 主線這條線真正會擋人的地方在派工那一端(`hooks/mainline-focus-guard.sh`),
|
||||
# 那裡擋的是「派了一張不在主線上的票」——**一個動作**,不是一段文字。
|
||||
#
|
||||
# 只有 ① = 又一個會被忽略的提醒(本 repo 心法第 2 條明令不要做這種東西)。
|
||||
# 只有 ② = 罰它做一件它拿不到資料的事。兩段合起來才是一條走得通的路。
|
||||
#
|
||||
|
||||
@@ -149,6 +149,10 @@
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/dispatch-format-guard.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/mainline-focus-guard.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/search-is-not-proof-guard.sh"
|
||||
@@ -192,6 +196,10 @@
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/dispatch-format-guard.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/mainline-focus-guard.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/search-is-not-proof-guard.sh"
|
||||
@@ -227,6 +235,10 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/countdown-milestone-refresh.sh"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/mainline refresh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+11
-1
@@ -147,7 +147,10 @@ def cmd_show(argv):
|
||||
print("🎯 現在的主線:%s「%s」" % (ML.ref_of(ms), ms.get("title")))
|
||||
desc = (ms.get("description") or "").strip()
|
||||
if desc:
|
||||
print(" 目標:" + desc.splitlines()[0].strip())
|
||||
first = desc.splitlines()[0].strip()
|
||||
# 只印第一行、最多 150 字:這是**一眼掃**的答案,不是 milestone 的全文。
|
||||
# leo 一天看兩眼(白話鐵律),整段貼過來他就不會看。
|
||||
print(" 目標:" + (first[:150] + "…" if len(first) > 150 else first))
|
||||
print(" 期限:%s" % ML.due_phrase(ms))
|
||||
print(" 進度:%d/%d 張已關(%d%%)" % (c, total, pct))
|
||||
mem = sorted(ML.members(ms))
|
||||
@@ -310,6 +313,13 @@ CMDS = {"show": cmd_show, "list": cmd_list, "set": cmd_set, "clear": cmd_clear,
|
||||
"refresh": cmd_refresh, "adopt": cmd_adopt, "has": cmd_has}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# `mainline | head` 是很自然的用法,別讓它吐一段 BrokenPipe 的 traceback
|
||||
# ——那會讓人以為這支壞了。
|
||||
try:
|
||||
import signal
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except Exception:
|
||||
pass
|
||||
if len(sys.argv) < 2:
|
||||
cmd_show([])
|
||||
sys.exit(0)
|
||||
|
||||
Reference in New Issue
Block a user