Compare commits

..

1 Commits

Author SHA1 Message Date
Leo b1830053e5 dispatch-format-guard 修兩處:規則自己的合格範例會被自己擋,禮貌收尾不該算違規
inkstone/ISEP#65:leo 發現「寫完 ISEP#30 那條規則之後,同一天總管又犯了 8 次」,
派來查「現行閘為什麼抓不到」。查證結果分兩層:

━━ 真正在跑的閘其實是 dispatch-format-guard.sh,不是 no-ticket-no-dispatch.sh ━━
no-ticket-no-dispatch.sh 的殼驗證早就是已知行為(只驗有沒有一行【工單】),
但 ISEP#30 已經為此新增了 dispatch-format-guard.sh 做內容判定,測試 19/19 通過。
問題是它有兩個沒被那 19 條測資蓋到的洞:

1. **regex 洞(本體 bug)**:頂層 CLAUDE.md 規定的合格格式帶全形括號——
   「【工單】owner/repo#N(→ comment M)」。_COMMENT_RE 只吃「→ comment M」
   本體,兩側括號沒被算進去,殘留括號讓 _REF_RE 比不過,於是**規則自己定義
   的合格範例會被自己的閘擋下**(半形括號 `(...)` 同樣會中)。用今天派我這張
   票的那份派工單原句實測,改之前 exit=2「票號形狀不對」。
2. **零容忍過頭**:純禮貌收尾(「謝謝」)跟「記得先讀 CLAUDE.md」這種真內容
   一樣被當「派工單不只有票號」擋下。ISEP#65 test 4 明講這四種不該擋
   (只有票號/空行/「謝謝」/括號包住的 comment 格式),優先做成低誤鎖。

判斷:不改 no-ticket-no-dispatch.sh、不另立第三支閘——dispatch-format-guard.sh
已經是「另立一支」的正確位置,這兩個洞在它自己的地盤上補。

修法:
- _COMMENT_RE 兩側括號(全形/半形)都設可選
- 新增 _COURTESY_CLOSERS 白名單(謝謝/多謝/感謝/辛苦了…)+ _is_courtesy_closer(),
  只在圍欄外生效,判準仍是「整行清乾淨標點後完全相等」不是「包含」——
  白名單不是黑名單,猜漏頂多誤鎖一次,不會反過來放走真內容(③g 測資證明)

測試:hooks/tests/dispatch-format-guard.test.sh 19→33 條,全過。新增:
- ③b/③c 全形/半形括號格式(規則自己的例句)
- ③d/③e/③f ISEP#65 test 4 的三種不該擋
- ③g 白名單邊界(禮貌詞混真內容裡照樣算數,防止白名單被誤用成漏洞)
- ⑮b–⑮i:leo 點名的「寫完規則後又犯的 8 次」當回歸樣本,內容是從
  ISEP#60/#61/#64、Arcrun#142/#144/#165、arcrun-rag#104、InkStoneCo#102
  的真實票內文摘錄(見 hooks/tests/fixtures/README.md 記載來歷),
  不是想像出來的例子;8 種形狀全部驗證會被擋
reply-identity.test.sh 11/11 仍全過(共用 dispatch_parse.py 沒有回歸)

升版 0.5.0 → 0.5.1(改完不升版沒人吃得到;check-version-consistency.sh
在本分支照慣例是紅的,tag 於 merge 時打)。

另查:.shell-payload/ 整個被 gitignore(scripts/vendor-to-shell.py 產物),
不是 git 分發的一部分——「這支閘會不會被吃到」取決於消費端有沒有重跑
plugin update/vendor-to-shell.py,不是這個 repo 委交的內容缺漏,故不在
本票改動範圍內,僅記錄供總管排查用。

未動 no-ticket-no-dispatch.sh(判斷見上,職責保持不重疊,兩支閘互斥見
dispatch_parse.py 的 has_ticket_marker 分岔)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:25:17 +08:00
16 changed files with 156 additions and 299 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "isep",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:49 支機械閘(60 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、27 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:48 支機械閘(59 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、27 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"version": "0.5.1",
"keywords": [
"inkstone",
+5 -10
View File
@@ -1,4 +1,4 @@
# 47 支閘,白話盤點表
# 46 支閘,白話盤點表
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
@@ -7,18 +7,14 @@
## 一句話結論
`hooks/` 底下有 **47 個 `.sh` 檔**`hooks.json` 實際掛上 **57 條註冊**(同一支閘常被多種情境同時掛上);
`hooks/` 底下有 **46 個 `.sh` 檔**`hooks.json` 實際掛上 **56 條註冊**(同一支閘常被多種情境同時掛上);
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
> +1+1 是本次(ISEP#69)新增的 `leo21c-mcp-guard.sh`。
>
> ⚠️ **這份表本身可能又已經漂移**`ls hooks/*.sh | wc -l` 在本分支實測是 49
> (比這裡寫的 47 多 2),初步比對疑似是 `isep-presence-beacon.sh`
> `milestone-due-guard.sh` 兩支已經掛進 `hooks.json` 卻沒被收進這張表——
> 但那兩支不是本票(ISEP#69)動的閘,沒有一併查證+改動,留給總管或後續票核實。
> 現在的寫法是實際數出來的:
> `ls hooks/*.sh | wc -l` 46`hooks.json` 展開後的 command 條目 56。
> **一份會偷偷過期的盤點表,跟沒有盤點表差不多**——見本頁最後「怎麼跟實況對帳」那段。
---
@@ -46,7 +42,6 @@
| `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 的實驗一律走別的測試帳號。 | 🛑 擋 |
| `leo21c-mcp-guard.sh` | AI 想用 ArcrunKBDB 的 MCP 工具查或寫**你的真庫(leo21c)**就擋下(不管讀寫)——測試場是另一條 MCP 連線,沒指定就該用那條。`arcrun_whoami` 本身不擋(用來確認身份);真的要碰真庫要明說(`touch /tmp/.leo21c-mcp-ok`),會留痕。連線的 server id 若不在已知清單裡也擋(清單可能過期,寧可先擋不要靜默放行)。 | 🛑 擋 |
| `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 票(會漏掉「開票前先搜過」這道檢查)就擋下。 | 🛑 擋 |
| `reply-identity-guard.sh` | 直接打 Gitea API 貼留言到票上時,內文第一行沒有 `【身份】``總管``subagent``leo`)就擋下——票上多條線並行時,看不出哪一則是誰寫的。純讀取、走 `scripts/ticket` 正門、指令裡加 `reply-identity-ok` 都放行。 | 🛑 擋 |
@@ -82,7 +77,7 @@
| `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`…),所以你不寫也不會漏。判準是「這一行是不是【工單】欄位」——**在不在**,不是寫了什麼,所以講得再謹慎也不會被多罰。 | 🛑 擋(+注入) |
| `dispatch-format-guard.sh` | **派工單 票號,就這樣。** 票號以外還寫了別的東西(背景、驗收條件、紅線、交件方式、「這個 session 才知道的事」)就擋下,並告訴你那些內容該搬去哪:每次都一樣的 → 共通規定;這次才知道的 → **寫進那張票**。反過來,合規的派工它會**自動把共通規定注入**給收工方(交件方式、不准 push main、org 是 `inkstone`…),所以你不寫也不會漏。判準是「這一行是不是【工單】欄位」——**在不在**,不是寫了什麼,所以講得再謹慎也不會被多罰。純禮貌收尾(「謝謝」)不算違規;`(→ comment M` 不論全形或半形括號都認得(ISEP#65 修過一次:CLAUDE.md 自己規定的合格寫法曾經被這支閘自己擋下)。 | 🛑 擋(+注入) |
## D2. AI 想開口問你問題的當下(PreToolUse / AskUserQuestion
-9
View File
@@ -60,15 +60,6 @@
}
]
},
{
"matcher": ".*mcp__[0-9a-fA-F-]+__(kbdb_|arcrun_).*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/leo21c-mcp-guard.sh"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
-202
View File
@@ -1,202 +0,0 @@
#!/bin/sh
# leo21c-mcp-guard.sh — PreToolUse:擋下打向 leo21c 真庫的 Arcrun/KBDB MCP 呼叫
# leo 2026-08-27 立,ISEP#69
#
# leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
# 為什麼打算用我的真庫測試?」
#
# 🔴 立這道閘的來由(2026-08-27 總管實查):
# 這台機器同時掛著兩條 Arcrun MCP 連線,工具名是兩串亂碼 UUID:
# mcp__00402d88-5e2f-4125-b971-8bdac50d7456__* → logged_in_as: leo21c 🔴 leo 的真庫
# mcp__d0eaf65b-583e-464e-aacc-00527ac05c4b__* → logged_in_as: youlin ✅ 測試場
# 總管一整天用的是第一條,所有測試資料卻寫在 youlin。範圍講清楚:
# 打在 leo21c 上的**全是唯讀查詢**kbdb_searchkbdb_get_mapwhoami),
# 沒有寫入、沒有 ingest。但唯讀也不該發生,而且它造成了三次假結論:
# ①「wiki 沒送上去」——其實上去了,在 youlin
# ②「檔案排在 1691 筆佇列後面」——查錯庫
# ③「ingest 回 200 但一個半小時還沒寫進去」——換 youlin 那條查,四塊內容全在
# 最難看的一格:總管每一份派工單都親手寫著「測試一律用 youlin,絕對不要碰 leo21c」
# ——規則寫給別人,自己一次都沒套用。
#
# 判準(封動作,不封措辭 —— 同 empty-handed-stop-guardleo21c-write-guard 的哲學):
# 任何打向 leo21c 座標的 Arcrun/KBDB MCP 呼叫 ⇒ 擋,除非明說要打它(見下方戳記)。
# 讀寫都擋,但訊息不同:唯讀提示「你可能查錯地方」,寫入提示「這會汙染真資料」。
# `arcrun_whoami` 本身永遠放行——那是唯一能確認身份的工具,擋它會造成死結。
#
# 🔴 已知限制(紅線要求「查不到就明講」,這裡就是查不到的那部分):
# MCP server 的 UUID 是這台機器這次連線分配的,**不是穩定不變的識別碼**
# ——查過 ~/.claude.json/本機常見設定檔,這兩條 Arcrun 連線都不是本地
# mcpServers 設定出來的(找不到來源設定檔),判斷是雲端 connector 這層
# 動態配的,UUID 會在重裝/換 session 時换掉。沒有找到官方文件證實這點,
# 這裡寫的是「查過但查不到穩定識別法」,不是猜測的答案。
# ⇒ 下面兩個 UUID 是寫死的(2026-08-27 由 leo 實測 arcrun_whoami 得到),
# **這份對照表會過期**。因此設計上不是「表格以外的一律放行」,
# 而是「不在表格裡的 UUID 一樣擋,且訊息明講『清單可能過期』」——
# 大聲失敗,不要靜默放行到一個身份不明的目標(fail-closed)。
set -eu
LEO21C_UUID="00402d88-5e2f-4125-b971-8bdac50d7456"
YOULIN_UUID="d0eaf65b-583e-464e-aacc-00527ac05c4b"
STAMP="/tmp/.leo21c-mcp-ok"
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
LOGDIR="$PROJ/.claude/hooks"
trace() {
# 留痕:擋下與放行都記,"明說放行" 也要留下痕跡讓 leo 事後查得到
# inkstone/InkStoneCo#48 同一個理由:閘只記「擋下」,分母未知答不出「它有沒有在動」)
T=$(date "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "?")
if [ -d "$LOGDIR" ]; then
printf '| %s | %s | %s | %s |\n' "$T" "$1" "${UUID:-?}" "${TOOL:-?}" \
>> "$LOGDIR/leo21c-mcp-guard.log" 2>/dev/null || true
fi
}
stamp_ok() {
# 單次、15 分鐘失效 —— 同 prod-write-guard.sh 的戳記設計,用完即丟
# (多條 subagent 並行時「時間窗」本身就是漏洞,單次消耗把窗口壓到最小)
[ -f "$STAMP" ] || return 1
NOW=$(date +%s 2>/dev/null || echo 0)
MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
case "$NOW$MT" in *[!0-9]*) return 1 ;; esac
[ "$NOW" -gt 0 ] && [ "$MT" -gt 0 ] || return 1
[ $((NOW - MT)) -lt 900 ] || return 1
rm -f "$STAMP" 2>/dev/null || true
return 0
}
INPUT="$(cat)"
TOOL=$(printf '%s' "$INPUT" | python3 -c '
import json,sys
try: print(json.load(sys.stdin).get("tool_name","") or "")
except Exception: print("__PYFAIL__")
' 2>/dev/null || printf '__PYFAIL__')
# ── fail-closed:解析壞了不准變成靜默放行 ────────────────────────────
# 這裡拿不到 tool_name,就退回對原始 payload 做字串比對;
# 命中真庫座標就照「未知/不確定」等級擋,而不是直接 exit 0。
if [ "$TOOL" = "__PYFAIL__" ] || [ -z "$TOOL" ]; then
if printf '%s' "$INPUT" | grep -q "$LEO21C_UUID"; then
UUID="$LEO21C_UUID"; TOOL="(解析失敗,字串比對命中真庫座標)"
stamp_ok && { trace "✅ 明說放行(解析失敗)"; exit 0; }
trace "⛔ 擋下(解析失敗仍命中真庫)"
cat >&2 <<MSG
🚫 leo21c-mcp-guard 解析這次呼叫的 tool_name 失敗,
但原始內容裡看得到 leo21c 的座標 —— fail-closed,先擋。
要放行這一次:touch /tmp/.leo21c-mcp-ok
MSG
exit 2
fi
exit 0
fi
# 只管「mcp__<uuid>__(kbdb_|arcrun_)*」這個形狀,其餘一律放行
case "$TOOL" in
mcp__*__kbdb_*|mcp__*__arcrun_*) ;;
*) exit 0 ;;
esac
UUID=$(printf '%s' "$TOOL" | sed -E 's/^mcp__([0-9a-fA-F-]+)__.*/\1/')
SUFFIX=$(printf '%s' "$TOOL" | sed -E 's/^mcp__[0-9a-fA-F-]+__//')
# 🔴 紅線:arcrun_whoami 本身永遠不擋——它是唯一能確認身份的工具,
# 擋它會造成死結(想查是誰、卻連查都查不了)。
case "$SUFFIX" in
arcrun_whoami) exit 0 ;;
esac
# youlin 測試場:預設放行,不留痕(每次都記會把 log 灌爆,而這是預期的正常路徑)
[ "$UUID" = "$YOULIN_UUID" ] && exit 0
# 明說要打(leo 有時真的需要查自己的庫):單次戳記放行,留痕
if stamp_ok; then
trace "✅ 明說放行"
exit 0
fi
# 判斷讀寫,訊息不同(兩者都擋,但傷害的說法不一樣)
case "$SUFFIX" in
*create_record*|*create_template*|*push_workflow*|*delete_workflow*|\
*recipe_push*|*recipe_delete*|*create_tag*|*delete_tag*|\
*tag_resource*|*untag_resource*|*run_workflow*|*report_feedback*|*recipe_submit*)
KIND="write" ;;
*)
KIND="read" ;;
esac
if [ "$UUID" = "$LEO21C_UUID" ]; then
trace "⛔ 擋下(真庫/$KIND)"
if [ "$KIND" = "write" ]; then
cat >&2 <<MSG
🚫 這個寫入打向 leo21c —— leo 本人的真庫,不是測試場(leo 2026-08-27 立,ISEP#69
leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
為什麼打算用我的真庫測試?」
你剛剛呼叫的是:
mcp__${LEO21C_UUID}__${SUFFIX}
→ logged_in_as: leo21c 🔴 真庫
這不是「查錯地方」——這是**把測試資料寫進 leo 自己在用的知識庫**,會汙染他的真實資料。
你要的測試場是另一條線(同樣的動作,換一個 server 前綴):
mcp__${YOULIN_UUID}__${SUFFIX}
→ logged_in_as: youlin.hsieh.dev ✅ 測試場,隨便寫
改法:換那條線再打一次。
⚠️ 若這次真的是 leo 要你動他的真庫(而不是測試):
touch /tmp/.leo21c-mcp-ok # 只放行「下一個」呼叫,15 分鐘內有效、用完即消失
會被記錄到 .claude/hooks/leo21c-mcp-guard.log。
MSG
else
cat >&2 <<MSG
🚫 這個查詢打向 leo21c —— leo 本人的真庫,不是測試場(leo 2026-08-27 立,ISEP#69
leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
為什麼打算用我的真庫測試?」
你剛剛呼叫的是:
mcp__${LEO21C_UUID}__${SUFFIX}
→ logged_in_as: leo21c 🔴 真庫
即使只是唯讀查詢,打錯庫依然有害——**你可能正在查錯地方**:
真庫的資料跟測試場的資料是兩份,用真庫的查詢結果去判斷測試進度,
會做出「wiki 沒送上去」「排在 1691 筆後面」這類看起來像結論、其實是查錯庫的假結論
2026-08-27 總管本人今天就連中三次)。
你要的測試場是另一條線(同樣的查詢,換一個 server 前綴):
mcp__${YOULIN_UUID}__${SUFFIX}
→ logged_in_as: youlin.hsieh.dev ✅ 測試場
改法:換那條線再查一次。
⚠️ 若這次真的是要查 leo 自己的庫(他本人有時真的需要):
touch /tmp/.leo21c-mcp-ok # 只放行「下一個」呼叫,15 分鐘內有效、用完即消失
會被記錄到 .claude/hooks/leo21c-mcp-guard.log。
MSG
fi
exit 2
fi
# ── 走到這裡:UUID 兩個都不是 —— 對照表可能過期,不要靜默放行 ─────────
# 這正是檔頭說的「大聲失敗」:某條連線被重裝、UUID 換了,我們認不出它是誰,
# 與其猜測放行、不如逼一次 arcrun_whoami 或人工更新這份表。
trace "⛔ 擋下(未知UUID,清單可能過期)"
cat >&2 <<MSG
🚫 這個 MCP server 的 id 不在 leo21c-mcp-guard.sh 的已知清單裡
已知清單(2026-08-27 由 arcrun_whoami 實測寫死,見本檔檔頭):
${LEO21C_UUID} → leo21c(真庫,預設擋)
${YOULIN_UUID} → youlin(測試場,預設放)
這次打的是:${UUID}
兩個都不是 —— 代表某條 MCP 連線被重裝過、UUID 換了,這份對照表可能已經過期。
寧可先擋、不要靜默放行到一個身份不明的目標。怎麼辦:
1. 呼叫 arcrun_whoami(這支工具本身不會被擋)確認這個 server 真正的身份
2. 如果它是新的 youlin id → 回報 isep-guard 更新 hooks/leo21c-mcp-guard.sh 的對照表
3. 現在就是要放行這一次 → touch /tmp/.leo21c-mcp-ok15 分鐘內有效、用完即消失)
MSG
exit 2
+37 -6
View File
@@ -37,12 +37,16 @@
當日實證:文字層的閘 8 次誤攔、0 次正確攔截,**紅線寫得越細,命中關鍵字的機率越高
⇒ 那些閘在懲罰謹慎**。
本檔**全檔零個「命中某個詞就違規」的比對**。判準是「這一行是不是【工單】欄位」——
**在不在**,不是**寫什麼**。用到的正則只有兩種,都在認形狀:
本檔**沒有「命中某個詞就違規」的比對**。判準是「這一行是不是【工單】欄位」——
**在不在**,不是**寫什麼**。用到的正則有三種,全部在認形狀:
- `【某某】` 欄位標記本身
- `owner/repo#N` 票號形狀
措辭多謹慎、有沒有提到「紅線」「部署」「main」,本檔一律不看。
⇒ 也因此它不會像文字層的閘那樣「寫得越小心越容易被罰」。
- `owner/repo#N`(可選外掛全形/半形括號的 `→ comment M`票號形狀
- 唯一例外(ISEP#65):`_COURTESY_CLOSERS` 是一份**白名單**,只用來認出
「這一行是不是零資訊的禮貌收尾」,作用方向與黑名單相反——認出的是
「不算違規」,不是「算違規」;且要求整行清乾淨標點後完全相等,不是
「包含」,所以不會反過來放走任何真正的內容。
措辭多謹慎、有沒有提到「紅線」「部署」「main」,本檔一律不看,仍然不會被
「寫得越小心越容易被罰」的病拖下水。
━━ 派工單格式 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【工單】inkstone/ISEP#30 → comment 4322
@@ -83,9 +87,32 @@ IDENTITY_ROLES = ("總管", "subagent", "leo")
_MARKER_RE = re.compile(r"^【([^】]{1,12})】\s*(.*)$")
_REF_RE = re.compile(r"^([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)\s*$")
_COMMENT_RE = re.compile(r"(?:#issuecomment-|→\s*comment\s*)(\d+)")
# 🔴 inkstone/ISEP#65:頂層 CLAUDE.md 規定的合格格式帶著全形括號——
# 「【工單】owner/repo#N(→ comment M)」。舊版正則只吃「→ comment M」本體,
# 兩側的括號(全形/半形都要吃)沒被算進去,殘留的括號讓 _REF_RE 比不過,
# 於是**規則自己定義的合格範例會被自己的閘擋下**。這裡把左右括號都收進來,
# 兩種都設成可選(沒有括號的舊寫法、純 #issuecomment- 連結都還要能過)。
_COMMENT_RE = re.compile(r"[(]?\s*(?:#issuecomment-|→\s*comment\s*)(\d+)\s*[)]?")
_FENCE_RE = re.compile(r"^\s*(```|~~~)")
# 🔴 inkstone/ISEP#65:票號後面只掛一句純禮貌收尾(例:「謝謝」),不該被當成
# 「派工單不只有票號」的違規——它不含任何要搬去票上或共通規定的實質內容。
# 這是**白名單,不是黑名單**:判準仍是結構(整行清乾淨標點後「等於」清單裡的
# 固定字串,不是「包含」),漏收某個禮貌講法頂多誤鎖一次(代價小,見 ISEP#65
# 的「優先做成低誤鎖」),但不會反過來放走任何真正的內容——真內容不會剛好
# 打出一個字不差的空話。全檔仍然沒有「命中某個詞就違規」的比對,這裡是唯一
# 一處字面比對,且作用方向相反:認出「不是內容」,不是認出「是違規」。
_COURTESY_CLOSERS = frozenset({
"謝謝", "謝謝你", "謝謝您", "謝謝囉", "多謝", "感謝", "感恩", "辛苦了", "收到",
"thanks", "thank you", "thx",
})
_COURTESY_STRIP_RE = re.compile(r"[\s。!!.,~,、]+$")
def _is_courtesy_closer(line):
s = _COURTESY_STRIP_RE.sub("", line.strip())
return s.lower() in _COURTESY_CLOSERS
def _split_lines(text):
"""切成 (是否在圍欄內, 原始行)。
@@ -146,6 +173,10 @@ def parse_dispatch(prompt):
continue
if raw.strip():
# 【工單】欄位本身只准佔它自己那一行;它下面的續行也是多的。
# 白名單豁免只在圍欄外生效——圍欄裡的東西本來就該一律算數(test ⑮),
# 這裡不額外放寬,避免有人把真內容包在圍欄裡再貼一句「謝謝」蒙混。
if not in_fence and _is_courtesy_closer(raw):
continue
extra.append((cur, raw))
return {
+53
View File
@@ -61,6 +61,32 @@ t 0 "② 合規:兩張票就兩行【工單】" \
t 0 "③ 合規:用 #issuecomment- 定址也認得" \
"$(payload '【工單】inkstone/ISEP#30#issuecomment-4327')"
# ── inkstone/ISEP#65CLAUDE.md 規定的合格格式(全形/半形括號包住 → comment M)
# 曾經被本閘自己擋下——這裡把它釘成迴歸測試,不再走回頭路。
t 0 "③b 合規:全形括號包住 → comment M(CLAUDE.md 標準寫法,曾被自己的閘擋下)" \
"$(payload '【工單】inkstone/ISEP#65(→ comment 4515')"
t 0 "③c 合規:半形括號變體" \
"$(payload '【工單】inkstone/ISEP#65(→ comment 4515)')"
# ── inkstone/ISEP#65 test 4:不該擋的三種雜訊 ────────────────────────
t 0 "③d 不該擋:票號後面只有空行" \
"$(payload '【工單】inkstone/ISEP#65
')"
t 0 "③e 不該擋:票號後面只有一句「謝謝」(零資訊禮貌收尾,白名單豁免)" \
"$(payload '【工單】inkstone/ISEP#65
謝謝')"
t 0 "③f 不該擋:禮貌收尾帶標點也認得(「謝謝!」)" \
"$(payload '【工單】inkstone/ISEP#65
謝謝!')"
t 2 "③g 白名單不是漏洞:禮貌詞混在真內容裡照樣算數" \
"$(payload '【工單】inkstone/ISEP#65
辛苦了,順便告訴你 main 現在是 8e28041')"
t 0 "④ 不是派工的動作(Bash)→ 一律不管" \
'{"tool_name":"Bash","tool_input":{"command":"ls"}}'
@@ -132,6 +158,33 @@ t 2 "⑮ 內容躲在圍欄裡也算數(圍欄只讓裡面的【】不被當
【就地】main 現在是 8e7e265
```')"
# ── inkstone/ISEP#65 test 5:拿「寫完規則之後總管又犯的 8 次」當回歸樣本 ──
# 內容是從各自票的真實內文摘錄(hooks/tests/fixtures/README.md 記著來歷),
# 不是想像出來的例子。8 種形狀都要被擋,證明這不是只補了一個特例。
t 2 "⑮b 回歸:ISEP#60 那次的內容(自造第四套機制的診斷)" \
"$(payload "$FIX/real-content-isep60.txt")"
t 2 "⑮c 回歸:ISEP#61 那次的內容(push-guard 戳記機制失敗史)" \
"$(payload "$FIX/real-content-isep61.txt")"
t 2 "⑮d 回歸:ISEP#64 那次的內容(PR 分診現況數字)" \
"$(payload "$FIX/real-content-isep64.txt")"
t 2 "⑮e 回歸:Arcrun#142 那次的內容(庫索引從沒被產生過)" \
"$(payload "$FIX/real-content-arcrun142.txt")"
t 2 "⑮f 回歸:Arcrun#144 那次的內容(樹狀圖驗收記錄)" \
"$(payload "$FIX/real-content-arcrun144.txt")"
t 2 "⑮g 回歸:Arcrun#165 那次的內容(MCP 徹查現場)" \
"$(payload "$FIX/real-content-arcrun165.txt")"
t 2 "⑮h 回歸:arcrun-rag#104 那次的內容(收檔實測數字)" \
"$(payload "$FIX/real-content-arcrunrag104.txt")"
t 2 "⑮i 回歸:InkStoneCo#102 那次的內容(milestone 打假背景)" \
"$(payload "$FIX/real-content-inkstoneco102.txt")"
echo
echo "── C 群:訊息本身 ──────────────────────────────────────────────"
clean
+18
View File
@@ -12,3 +12,21 @@ leo 2026-08-27 要的測資是「今天實際發生的 5 次違規派工」
其餘的覆蓋率改用「一條規則一個最小案例」來補(見 dispatch-format-guard.test.sh B 群)。
**不要用想像出來的例子替補那 4 份**,那會讓測試看起來比實際更綠。
---
## 第二批:`real-content-*.txt`inkstone/ISEP#65
leo 2026-08-27 又在同一天抓到總管**寫完規則之後又犯了 8 次**(八條線的派工單,
每一條都帶了「這個 session 才知道的事」):`ISEP#60``#61``#64`
`Arcrun#142``#144``#165``arcrun-rag#104``InkStoneCo#102`
🔴 **原始的違規派工 prompt 本身一樣拿不回來**(同一個道理:agent 一停就蒸發),
但這 8 次的**內容已經被總管補寫回各自的票**(ISEP#65 body 明講)。
這裡的 8 份 `real-content-*.txt` 是**從那些票的真實內文摘錄**(一字未改地複製,
只是截取一段,並在最前面加回 `【工單】owner/repo#N` 讓它符合派工單的殼)——
不是想像出來的例子,是真的存在於 Gitea 上、總管真的寫過的句子,只是原本的位置
錯了(在 prompt 裡,不在票上)。
用途:驗證 dispatch-format-guard.sh 修完 ISEP#65 之後,**這 8 種「票號 + 這個
session 才知道的事」的形狀全部會被擋下**——不是只驗一個最小案例。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/Arcrun#142
實查結論:這兩樣東西(庫索引與全庫摘要)從沒被產生過,不是壞掉,是根本沒做。
於是「開場就知道有哪些子庫、各裝了什麼」這件事永遠靠現查,查不到就當作沒有。
相關:inkstone/Arcrun#81、#87、#100(總圖說 0 條關聯,實際 1854 條)。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/Arcrun#144
leo 明講「不要現在執行,記下來」,本票只是把它接住,開工前要先確認優先序。
測試 OK,出現了樹狀圖,但不符合需求——達成的是 mindmap 向右向上分佈,
上下間距很大、展開字很小,用戶要擁有觸控板否則很難閱讀。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/Arcrun#165
不要先修,先查清楚再說——現在連斷在哪都還沒人講得出來。
已知的現場(總管 2026-08-26 用 MCP 實查,不是讀 code 推的):同一個知識庫
youlinyuga3bse),同一套 MCP 工具,四次查詢分數與筆數都對不上。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/arcrun-rag#104
現況(總管 2026-08-14 實測,非推測):leo 把 5 個資料夾接上重裝後的 leo21c,
App 顯示你的檔案 12022 共幾份、819 已送上去、11183 排隊中、20 送不上去。
逐資料夾實數(find 計 .md/.pdf/.txt,已排除 node_modules.git)。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/InkStoneCo#102
背景(同一天稍早,leo 對同一件事說過的重話):昨天要完成 51,你現在還有沒派的,
去看看 milestone 一大堆沒有一個完成,現在整理,不要詐騙,去看清楚哪些完成的。
Gitea 的百分比只數票的開關,它不知道票裡的東西通了沒。
+6
View File
@@ -0,0 +1,6 @@
【工單】inkstone/ISEP#60
`subagent-claim-worksheet.sh``SubagentStop` 產待驗單)與 `claim-verify-police.sh`
(`Stop` 擋收工)這一套自造的第四套機制整個移除,它守的東西改由 Gitea 原生的三格承接
(子票相依/tag/指派——見 `inkstone/ISEP#59` 與 PR `#58`)。
`#issuecomment-4289` 量到同一份待驗單重生三次、同內容兩個檔名。
+5
View File
@@ -0,0 +1,5 @@
【工單】inkstone/ISEP#61
`main-and-prod-push-guard.sh` 的戳記機制已經走進死路:原設計用
`CLAUDE_CODE_CHILD_SESSION=1` 判斷 subagent,實測結果總管主 session 也是 1,
那個變數不是身分標記,誰都擋。改成戳記(`/tmp/.main-push-ok`,總管手動寫)也失敗。
+6
View File
@@ -0,0 +1,6 @@
【工單】inkstone/ISEP#64
現況(總管實查,2026-08-27 12:40):open PR 總數 23,總管已關掉 8 個「內容早就在
main」的空 PR,剩 15 個要分診。已關的 8 個實測 git merge-base --is-ancestor 證明
HEAD 已是 main 的祖先,diff +0/-0Arcrun#166#161#158#155#153、
arcrun-rag#142#141#139。
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
# leo21c-mcp-guard.sh 的迴歸測試(ISEP#69)。
HOOK="$1"
rm -f /tmp/.leo21c-mcp-ok 2>/dev/null # 起手清乾淨,避免上一輪殘留戳記污染這輪
mk(){ python3 -c "import json,sys;print(json.dumps({'tool_name':sys.argv[1],'tool_input':{}}))" "$1"; }
PASS=0; FAIL=0
t(){ # t <期望 block|pass> <說明> <tool_name> —— 除了明說放行那幾格,其餘每格開跑前先確保沒有殘留戳記
mk "$3" | "$HOOK" >/dev/null 2>&1; rc=$?
got=$([ $rc -eq 2 ] && echo block || echo pass)
if [ "$got" = "$1" ]; then echo "$2"; PASS=$((PASS+1))
else echo "$2 —— 期望 $1,實得 $got"; FAIL=$((FAIL+1)); fi
}
LEO="00402d88-5e2f-4125-b971-8bdac50d7456"
YOULIN="d0eaf65b-583e-464e-aacc-00527ac05c4b"
UNKNOWN="11111111-2222-3333-4444-555555555555"
echo "── 該擋(打向 leo21c 真庫)──"
t block "唯讀查詢打 leo21c" "mcp__${LEO}__kbdb_search"
t block "get_map 打 leo21c" "mcp__${LEO}__kbdb_get_map"
t block "寫入打 leo21c" "mcp__${LEO}__kbdb_create_record"
t block "推工作流打 leo21c" "mcp__${LEO}__arcrun_push_workflow"
echo "── 該放行(打向 youlin 測試場)──"
t pass "唯讀查詢打 youlin" "mcp__${YOULIN}__kbdb_search"
t pass "寫入打 youlin" "mcp__${YOULIN}__kbdb_create_record"
t pass "查 template 打 youlin" "mcp__${YOULIN}__kbdb_list_templates"
t pass "arcrun 讀清單打 youlin" "mcp__${YOULIN}__arcrun_list_workflows"
echo "── 不該擋:arcrun_whoami 本身(不管打哪條線)──"
t pass "whoami 打 leo21c" "mcp__${LEO}__arcrun_whoami"
t pass "whoami 打 youlin" "mcp__${YOULIN}__arcrun_whoami"
t pass "whoami 打未知 UUID" "mcp__${UNKNOWN}__arcrun_whoami"
echo "── 不該擋:非 kbdb/arcrun 的 MCP 工具、非 MCP 工具 ──"
t pass "無關的 MCP 工具" "mcp__${LEO}__some_other_tool"
t pass "一般工具名" "Bash"
t pass "另一個一般工具" "kbdb_search"
echo "── 該擋(清單以外的未知 UUID —— 對照表可能過期,大聲失敗)──"
t block "未知 UUID 的 kbdb_search" "mcp__${UNKNOWN}__kbdb_search"
t block "未知 UUID 的寫入" "mcp__${UNKNOWN}__kbdb_create_record"
echo "── 明說要打真庫:戳記放行(單次)──"
rm -f /tmp/.leo21c-mcp-ok
touch /tmp/.leo21c-mcp-ok
t pass "戳記在,打 leo21c 放行一次" "mcp__${LEO}__kbdb_search"
# 上面 t() 已經在下一次呼叫前 rm -f 戳記,這裡驗證戳記確實是單次
touch /tmp/.leo21c-mcp-ok
mk "mcp__${LEO}__kbdb_search" | "$HOOK" >/dev/null 2>&1
mk "mcp__${LEO}__kbdb_search" | "$HOOK" >/dev/null 2>&1; rc2=$?
if [ $rc2 -eq 2 ]; then echo " ✅ 戳記用完即消失(第二次仍被擋)"; PASS=$((PASS+1))
else echo " ❌ 戳記用完即消失 —— 期望第二次被擋,實得 pass"; FAIL=$((FAIL+1)); fi
rm -f /tmp/.leo21c-mcp-ok 2>/dev/null
echo "── 明說放行要留痕 ──"
LOGDIR="${CLAUDE_PROJECT_DIR:-$(pwd)}/.claude/hooks"
mkdir -p "$LOGDIR"
rm -f "$LOGDIR/leo21c-mcp-guard.log"
touch /tmp/.leo21c-mcp-ok
mk "mcp__${LEO}__kbdb_search" | "$HOOK" >/dev/null 2>&1
if [ -f "$LOGDIR/leo21c-mcp-guard.log" ] && grep -q "明說放行" "$LOGDIR/leo21c-mcp-guard.log"; then
echo " ✅ 明說放行有寫進 log"; PASS=$((PASS+1))
else
echo " ❌ 明說放行沒有留痕"; FAIL=$((FAIL+1))
fi
rm -f "$LOGDIR/leo21c-mcp-guard.log" /tmp/.leo21c-mcp-ok 2>/dev/null
echo
echo "結果:通過 $PASS 失敗 $FAIL"
[ $FAIL -eq 0 ] || exit 1