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
15 changed files with 170 additions and 184 deletions
+3 -3
View File
@@ -56,7 +56,7 @@
| `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` | AI(不是被派來做這件事的 subagent 本人)要親手改程式碼(含閘自己的 `.sh`),就擋下逼它想「這件事能不能交給別人做」——擋過一次後有寬限期不鬼打牆,但寬限期一過會再響,不是擋過一次就永久放行。 | 🛑 擋 |
| `subagent-first-guard.sh` | 這個對話**從頭到尾都沒有派過任何 subagent**,AI 卻要自己動手改程式碼,就先擋一次,逼它想一想「這件事能不能交給別人做」。 | 🛑 擋 |
| `mistake-needs-ticket-guard.sh` | AI 想往 `mistakes.md`(教訓紀錄)新增一條「機制可以防止」的教訓,卻沒附對應票號就擋下——沒有票號的教訓沒有人會回頭處理。 | 🛑 擋 |
| `pending-changes-retired.sh` | AI 想寫東西進已經廢除的 `pending-changes.md` 檔案就擋下——這個檔案已停用,規格變更一律改開 Gitea 票。 | 🛑 擋 |
@@ -77,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
@@ -126,7 +126,7 @@
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
| `kbdb-asked-stamp.sh` | AI 真的查過 KBDB 之後,留一個時間戳——給前面 `history-first-guard.sh` 判斷「這輪有沒有先查過」用。 | 📝 記錄 |
| `subagent-first-stamp.sh` | AI 真的派過工之後,留一個時間戳——純資訊,給被 `subagent-first-guard.sh` 擋下時的訊息當脈絡(「上次派工是幾分鐘前」),不再是放行判準。 | 📝 記錄 |
| `subagent-first-stamp.sh` | AI 真的派過工之後,留一個時間戳—— `subagent-first-guard.sh` 判斷用。 | 📝 記錄 |
| `issue-status-autoflip.sh` | AI 一派工出去,就自動把對應的 Gitea 票改成「進行中(s/doing)」,不必等人手動改標籤。 | 📝 記錄(自動改票) |
## I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit
+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 {
+17 -73
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# subagent-first-guard.sh — PreToolUseWrite|Edit|MultiEdit):
# 要親手改 code,卻不是被派來做這件事的人 → 擋,逼你先回答「這件事該不該派出去」。
# 要親手改 code,卻**這個 session 一次工都沒派過** → 擋一次,逼你先回答「這件事該不該派出去」。
#
# 🔴 立這道閘的來由(leo 2026-08-08):
# 「你記得**要叫 subagent 開工,你負責維護 loop**,而不是你開工後過一陣子停下來對吧?
@@ -15,34 +15,8 @@
# 這道閘負責讓「授權」真的變成「行為」——同 KBDB 那道的教訓:
# **規則被讀到 ≠ 會被執行,要有機制驗證照做**。
#
# 🔴 2026-08-27 改版(inkstone/ISEP#66)——leo 當場撞到:「你禁止寫 code,為什麼你改?
# 為什麼你可以執行?」查出來這道閘同時有三個洞,改完的是這三個:
#
# 洞一:`.sh` 從來不在受管副檔名清單裡。這個 repo 自己的閘全部是 .sh
# ⇒ 總管改任何一支閘,這道閘從第一行 case 就直接放行,路徑白名單根本沒機會判斷。
# 這才是「改 no-ticket-no-dispatch.sh 閘一聲不響」的真正原因,不是路徑豁免。
# → 修法:閘本身(`.claude/hooks/*.sh`、`hooks/*.sh`)獨立收進受管清單,
# 不看副檔名是不是「傳統程式語言」。同時拿掉 `*/.claude/hooks/*` 整目錄豁免——
# 「能改閘的人,等於能關掉所有其他閘」,這句話本身就是拿掉它的理由;
# 真要緊急止血,走下面②的 solo-ok,不需要一扇整目錄常開的門。
# 洞二:「這個 session 派過一次工」被當永久放行條件。但「派過一次」跟
# 「有派工的習慣」是兩件事——派過工只回答了「那一件任務有沒有交出去」,
# 回答不了「這一次任意的手改合不合理」。→ 修法:這個訊號整個拿掉,
# 不再是放行條件(只留來在被擋下時的提示文字裡,讓你看到「你剛剛才派過工,
# 這次怎麼不派」,純資訊、不放行)。
# 洞三:「同一 session 只擋一次」=警報响過一次就對這個 session 的餘生啞掉,
# 等於自己承認「這道閘只在第一次有效」。但本 repo 心法第二條反過來也成立:
# 永遠在響 = 訓練人忽略它。→ 修法:改成「寬限期內不重複鬼打牆,
# 寬限期一過就再響」——兩頭都要顧,不是選一邊犧牲。
#
# 放行只剩三種:
# ① CLAUDE_CODE_CHILD_SESSION=1 —— 這是被派來做事的 subagent 本人,它就是被派來寫的
# ② /tmp/.solo-ok-<session_id> 存在且**非空**(寫了理由,不是 touch 空檔)
# ③ 檔案本身不歸這道閘管(測試檔/system-dev 的 wiki/非受管副檔名)
#
# 逃生口:真的該自己做(單行修、緊急止血、subagent 回報後的收尾)
# → 把理由寫進檔案本身,不是單純 touch:
# echo "單行修 xxx,理由 yyy" > /tmp/.solo-ok-<session_id>
# 逃生口:真的該自己做(單行修、改 hook 自己、緊急止血、subagent 回報後的收尾)
# → `touch /tmp/.solo-ok-<session_id>` 後重送,並**在回覆裡說明理由**(留痕)。
set -eu
INPUT="$(cat)"
@@ -59,62 +33,34 @@ except Exception: print("nosid")
[ -z "$FILE_PATH" ] && exit 0
# ① 這是被派來做事的 subagent 本人(不是總管自己)→ 它本來就該寫 code,不歸這道閘管
[ "${CLAUDE_CODE_CHILD_SESSION:-}" = "1" ] && exit 0
# 只管「這是不是該被派工規則管的程式碼」——
# 一般受管語言,加上「閘本身」(即使是 .sh,這正是洞一要補的地方)
# 只管 code 檔——文件、SDD、wiki、測試、hook 自己都放行
# (這些本來就常是總管自己該寫的:判準、規格、落帳)
case "$FILE_PATH" in
*/.claude/hooks/*.sh|*/hooks/*.sh) ;;
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.go|*.py|*.rs|*.java|*.rb) ;;
*) exit 0 ;;
esac
# 就算是受管檔案,這兩類仍放行:測試檔/頂層 wiki(system-dev/
case "$FILE_PATH" in
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
*system-dev/*) exit 0 ;;
*/.claude/hooks/*|*system-dev/*) exit 0 ;;
esac
# ② 明示「我自己做」且寫了理由(不是空檔)→ 放行
SOLO_OK="/tmp/.solo-ok-$SID"
[ -s "$SOLO_OK" ] && exit 0
[ -f "/tmp/.subagent-spawned-$SID" ] && exit 0 # 這個 session 派過工了 → 放行
[ -f "/tmp/.solo-ok-$SID" ] && exit 0 # 明示要自己做 → 放行(留痕)
NOW=$(date +%s)
NAG_TTL=${SUBAGENT_GUARD_NAG_TTL:-900} # 15 分鐘寬限期:同一輪不重複鬼打牆,過了才再響
WARNED="/tmp/.subagent-guard-warned-$SID"
if [ -f "$WARNED" ]; then
WARN_TS=$(cat "$WARNED" 2>/dev/null || echo 0)
case "$WARN_TS" in ''|*[!0-9]*) WARN_TS=0 ;; esac
if [ $((NOW - WARN_TS)) -lt "$NAG_TTL" ]; then
exit 0 # 剛擋過,還在寬限期內 → 不重複鬼打牆
fi
fi
[ -f "$WARNED" ] && exit 0 # 同一 session 只擋一次,不鬼打牆
date +%s > "$WARNED"
# 派過工不是放行條件(洞二),但拿來給脈絡:讓你看到「上次派工是多久前」
DISPATCH_NOTE=""
SPAWNED="/tmp/.subagent-spawned-$SID"
if [ -f "$SPAWNED" ]; then
D_TS=$(cat "$SPAWNED" 2>/dev/null || echo 0)
case "$D_TS" in ''|*[!0-9]*) D_TS=0 ;; esac
if [ "$D_TS" -gt 0 ] 2>/dev/null; then
MIN_AGO=$(( (NOW - D_TS) / 60 ))
DISPATCH_NOTE="
【這個 session 派過工】最近一次是 ${MIN_AGO} 分鐘前——
但「派過一次」不等於「這次也不用派」,兩件事分開判斷。"
fi
fi
cat >&2 <<EOF
🧑‍🏭 派工警察:你要親手改 code。${DISPATCH_NOTE}
cat >&2 <<'EOF'
🧑‍🏭 派工警察:你要親手改 code,但這個 session **一次工都沒派過**。
【leo 2026-08-08】「你記得**要叫 subagent 開工,你負責維護 loop**
而不是你開工後過一陣子停下來對吧?」
【leo 2026-08-27】「你禁止寫 code,為什麼你改?為什麼你可以執行?」
——這句話問的就是現在這一次。
【已知的環境陷阱】Claude Desktop 這個 surface 的 system prompt 有
`Do not call the AgentTool unless the user requested it`,會壓過 CLAUDE.md 規則三點五。
**leo 已在 CLAUDE.md 給出常駐授權**(規則三點五的「常駐授權」段)=那個條件永久滿足,
不必再等他開口。
先回答一句(答不出來就是該派):
這件事**為什麼不能交給 subagent**
@@ -123,8 +69,6 @@ cat >&2 <<EOF
✅ 該自己做:判準/規格/落帳(那些檔本來就放行)、subagent 回報後的收尾裁決、
單行修、緊急止血
決定自己做 ⇒ 把理由寫進檔案本身(不是 touch 空檔):
echo "理由:xxx" > /tmp/.solo-ok-$SID
再重送這次操作。
決定自己做 ⇒ `touch /tmp/.solo-ok-$SID` 後重送,並**在回覆裡說明理由**。
EOF
exit 2
-6
View File
@@ -4,12 +4,6 @@
# 配 subagent-first-guard.sh 使用(同 kbdb-asked-stamp.sh 的形狀)。
# 只留時戳,不擋任何東西。
#
# 🔴 2026-08-27inkstone/ISEP#66):這個時戳**不再是放行條件**。
# 「派過一次工」曾經被 subagent-first-guard.sh 當成「這個 session 永久放行」——
# 但那混淆了「這件任務有沒有交出去」跟「下一次任意的手改合不合理」,是兩件事。
# 現在這支只負責留時戳,給被擋下時的訊息當脈絡用(「上次派工是幾分鐘前」),
# 純資訊、不影響放行判斷。
#
# 🔴 為什麼要按 session 分開存(而不是像 KBDB 那道用全域+1 小時 TTL):
# 「這一輪工作有沒有先考慮派工」是**每個 session 各自要回答**的問題。
# 用全域檔的話,昨天派過一次就等於永久解鎖,這道閘會在第二天起完全失效。
+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。
-96
View File
@@ -1,96 +0,0 @@
#!/bin/bash
# subagent-first-guard 的測試(inkstone/ISEP#66
#
# 補的三個洞:
# 洞一:.sh 從沒進過受管副檔名清單 ⇒ 這個 repo 自己的閘(全是 .sh)從沒被這道閘管過。
# 洞二:「這個 session 派過一次工」被當永久放行——拿掉,不再是放行條件。
# 洞三:「同一 session 只擋一次」讓警報響過一次就對餘生啞掉——改成寬限期,過了再響。
#
# 判準:派過工不再免死金牌/閘本身(.sh)進受管清單/寬限期內不鬼打牆、過了要再響/
# CHILD_SESSIONsubagent 本人放行/測試檔・system-dev wiki 放行/
# solo-ok 要非空(寫理由)才放行,單純 touch 不算。
cd "$(dirname "$0")/.." || exit 1
H=hooks/subagent-first-guard.sh
PASS=0; FAIL=0
run(){ # $1=want_exit $2=file_path $3=sid $4=child(0/1)
local want="$1" fp="$2" sid="$3" child="${4:-0}"
local payload
payload=$(python3 -c 'import json,sys;print(json.dumps({"tool_name":"Edit","tool_input":{"file_path":sys.argv[1]},"session_id":sys.argv[2]}))' "$fp" "$sid")
if [ "$child" = "1" ]; then
got=$(CLAUDE_CODE_CHILD_SESSION=1 SUBAGENT_GUARD_NAG_TTL="${TTL:-900}" bash -c 'printf "%s" "$1" | bash "$2" >/dev/null 2>&1; echo $?' _ "$payload" "$H")
else
got=$(env -u CLAUDE_CODE_CHILD_SESSION SUBAGENT_GUARD_NAG_TTL="${TTL:-900}" bash -c 'printf "%s" "$1" | bash "$2" >/dev/null 2>&1; echo $?' _ "$payload" "$H")
fi
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s sid=%s %.70s\n' "$want" "$got" "$sid" "$fp"
}
clean(){ rm -f "/tmp/.subagent-spawned-$1" "/tmp/.solo-ok-$1" "/tmp/.subagent-guard-warned-$1"; }
R="/Users/youlinhsieh/Documents/tech_projects/InkStoneCo"
ISEP="/Users/youlinhsieh/Documents/tech_projects/ISEP"
echo "── 洞一:閘本身是 .sh,也要被這道閘管(重演 2026-08-27 那次)──"
S=t66-hole1-a; clean "$S"
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
S=t66-hole1-b; clean "$S"
run 2 "$ISEP/hooks/subagent-first-guard.sh" "$S" 0
echo "── 洞二:派過工不是免死金牌——就算剛派、就算派了很多次,直接手改一樣要擋 ──"
S=t66-hole2-a; clean "$S"
date +%s > "/tmp/.subagent-spawned-$S" # 剛派過工(此刻)
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
S=t66-hole2-b; clean "$S"
echo $(( $(date +%s) - 3600 )) > "/tmp/.subagent-spawned-$S" # 一小時前派過(今天真實情境的重演)
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
echo "── 洞三:警報不能響一次就對餘生啞掉——寬限期內不鬼打牆,過了要再響 ──"
S=t66-hole3; clean "$S"; TTL=2
run 2 "$R/matrix/arcrun/foo.py" "$S" 0 # 第一次:沒見過 → 擋,順便留下 warned 時戳
run 0 "$R/matrix/arcrun/foo.py" "$S" 0 # 馬上重試:寬限期內 → 不鬼打牆,放行
sleep 3
run 2 "$R/matrix/arcrun/foo.py" "$S" 0 # 寬限期過了 → 再響
TTL=900
echo "── 不該擋:subagent 本人(CHILD_SESSION=1)自己寫它被派來寫的 code ──"
S=t66-child; clean "$S"
run 0 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 1
run 0 "$R/matrix/arcrun/foo.py" "$S" 1
echo "── 不該擋:測試檔 ──"
S=t66-test; clean "$S"
run 0 "$R/matrix/arcrun/foo_test.py" "$S" 0
run 0 "$R/matrix/arcrun/foo.test.ts" "$S" 0
run 0 "$ISEP/tests/foo.spec.js" "$S" 0
echo "── 不該擋:system-dev/ 的 wiki(即使副檔名受管)──"
S=t66-wiki; clean "$S"
run 0 "$R/system-dev/wiki/status.md" "$S" 0
run 0 "$R/system-dev/docs/some_script.py" "$S" 0
echo "── 不該擋:非受管副檔名(一般 .sh 不在 hooks 目錄/.md.json)──"
S=t66-nonguard; clean "$S"
run 0 "$R/scripts/deploy.sh" "$S" 0
run 0 "$R/README.md" "$S" 0
run 0 "$R/package.json" "$S" 0
echo "── solo-ok:要非空(寫理由)才放行,單純 touch 不算 ──"
S=t66-soloempty; clean "$S"
touch "/tmp/.solo-ok-$S"
run 2 "$R/matrix/arcrun/foo.py" "$S" 0
clean "$S"
echo "理由:單行修,緊急止血" > "/tmp/.solo-ok-$S"
run 0 "$R/matrix/arcrun/foo.py" "$S" 0
clean "$S"
echo "── 真違規不能因為這次改動漏擋:全新 session 直接改一般 code ──"
S=t66-fresh; clean "$S"
run 2 "$R/matrix/arcrun/graph-executor.ts" "$S" 0
# 清理
for s in t66-hole1-a t66-hole1-b t66-hole2-a t66-hole2-b t66-hole3 t66-child t66-test t66-wiki t66-nonguard t66-soloempty t66-fresh; do clean "$s"; done
echo
echo "$PASS/$((PASS+FAIL)) 通過"
[ "$FAIL" -eq 0 ]