3bc7f3c5f5
leo 2026-08-27(inkstone/ISEP#30 comment 4322/4325/4327): 「這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?」 「你用一個 output parser 把你給 subagent 的指令規範,分作幾點,每一點規定格式, 照這種散文寫法根本無法迭代」「警察也不能抓」 「交件方式不需要寫,定義在原則裡⋯⋯每次都一樣提取出來變成共通規定」 「(那些 session 事實)這些為什麼不寫到票裡?」「subagent 回覆時要表明身份」 病根:no-ticket-no-dispatch.sh 驗的是「有沒有一行【工單】owner/repo#N」, 而規則的原文是「派工單只寫票號」。⇒ 把 40 行任務全寫在 prompt 裡、票號補一行, 閘照樣放行。2026-08-27 一天內這樣做了 5 次,每次票上都沒有那份任務。 規則存在,閘只驗了它的殼——同款第 N 次(history-first/KBDB-first/stage-first)。 新增 hooks/dispatch-format-guard.sh(PreToolUse Task|Agent),兩件事: - 擋:【工單】以外還有實質內容就 exit 2,並指出那些內容該搬去哪 (每次都一樣 → 共通規定;這次才知道 → 寫進那張票。 判準「這句話換一張票還成立嗎?」) - 注入:合規的派工自動把共通規定送給收工方(交件方式、不准 push main、 org 是 inkstone…)——這是「派工單只剩票號」能成立的前提, leo 的驗收條件之一就是「收工方沒讀派工單也知道要貼回原票」 判準是結構不是文字(leo 2026-08-17 那條檢驗):問的是「這一行是不是【工單】欄位」 ——在不在,不是寫什麼。hooks/lib/dispatch_parse.py 全檔零個「命中某個詞就違規」的比對。 ⇒ 也因此不需要語意判官:免費、瞬間、每次結果一樣。 新增 hooks/reply-identity-guard.sh(PreToolUse Bash)+ scripts/ticket 內建檢查: 票上每一則留言第一行要有【身份】(總管/subagent/leo)。貼留言有兩條路,兩條都封 ——ticket-api-bypass-guard 是刻意放行「對既有票留言」的,只封正門等於沒封。 實害:多條線並行時總管寫的診斷被當成 subagent 的結論,而其中一則是錯的。 規約寫成文件:docs/governance/dispatch-and-reply-format.md (§2 那段就是被注入的那份共通規定本體——只有一份,改那裡等於改所有派工) 實測(離線、不打網路、不花錢): hooks/tests/dispatch-format-guard.test.sh 19/19 hooks/tests/reply-identity.test.sh 11/11 測資裡的 B⑨ 是真跡:產生 ISEP#30 這條線的那一次派工,一字未改。 另 4 份 leo 點名的違規派工拿不回來了——它們住在 prompt 裡,agent 一停就沒了, 這件事本身就是這條規則的證據(見 hooks/tests/fixtures/README.md,不用想像的例子替補)。 升版 0.4.0 → 0.5.0(產物按版本號分資料夾,不升版新閘不會被載入)。 tag 照慣例打在 merge commit 上,所以這條分支上 check-version-consistency.sh 是紅的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176 lines
7.2 KiB
Bash
Executable File
176 lines
7.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# dispatch-format-guard.sh 的迴歸測試(inkstone/ISEP#30 comment 4322/4325/4327)。
|
||
#
|
||
# **離線、不打網路、不花錢、每次結果一樣**——這支閘是純結構判斷,沒有語意判官,
|
||
# 所以它不像 ask-user-question-guard 那樣需要一支 live 測試量準度。
|
||
#
|
||
# 三群,兩個方向都要有證據:
|
||
# A 群「該放行」——合規的派工(只有票號)、不是派工的動作、沒有【工單】(別人的地盤)、
|
||
# 豁免戳記;**而且合規時要真的把共通規定注入出去**
|
||
# B 群「該擋」 ——真跡一份(那 5 次違規裡唯一還拿得回來的),
|
||
# 加上一條規則一個最小案例
|
||
# C 群「訊息本身」——講得出出路、沒有被 shell 展開
|
||
#
|
||
# 用法:hooks/tests/dispatch-format-guard.test.sh [hook 路徑]
|
||
|
||
set -u
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
HOOK="${1:-$(cd "$HERE/.." && pwd)/dispatch-format-guard.sh}"
|
||
FIX="$HERE/fixtures"
|
||
|
||
PASS=0; FAIL=0; N=0
|
||
|
||
# payload <prompt 檔或字串> [tool_name]
|
||
payload() {
|
||
python3 - "$1" "${2:-Task}" <<'PY'
|
||
import json, os, sys
|
||
src, tool = sys.argv[1], sys.argv[2]
|
||
prompt = open(src, encoding="utf-8").read() if os.path.isfile(src) else src
|
||
print(json.dumps({"session_id": "S-TEST", "hook_event_name": "PreToolUse",
|
||
"tool_name": tool, "tool_input": {"prompt": prompt}},
|
||
ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
# t <期望 exit> <說明> <payload JSON>
|
||
t() {
|
||
want="$1"; desc="$2"; body="$3"
|
||
N=$((N+1))
|
||
err=$(printf '%s' "$body" | bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||
if [ "$rc" -eq "$want" ]; then
|
||
printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc"
|
||
printf '%s\n' "$err" | sed -n '1,6p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
}
|
||
|
||
clean() { rm -f /tmp/.dispatch-ok-S-TEST 2>/dev/null || true; }
|
||
|
||
echo "── A 群:該放行 ────────────────────────────────────────────────"
|
||
clean
|
||
|
||
t 0 "① 合規:整份派工單只有一行票號" \
|
||
"$(payload '【工單】inkstone/ISEP#30 → comment 4322')"
|
||
|
||
t 0 "② 合規:兩張票就兩行【工單】" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
【工單】inkstone/InkStoneCo#55')"
|
||
|
||
t 0 "③ 合規:用 #issuecomment- 定址也認得" \
|
||
"$(payload '【工單】inkstone/ISEP#30#issuecomment-4327')"
|
||
|
||
t 0 "④ 不是派工的動作(Bash)→ 一律不管" \
|
||
'{"tool_name":"Bash","tool_input":{"command":"ls"}}'
|
||
|
||
t 0 "⑤ 整包不是合法 JSON → fail-open" 'this is not json at all'
|
||
|
||
t 0 "⑥ 沒有【工單】→ 閉嘴,那是 no-ticket-no-dispatch 的地盤(不准兩支閘同時開口)" \
|
||
"$(payload '去把 arcrun 那個 worker 修一修,順便跑一下測試')"
|
||
|
||
# 注入:合規時要真的把共通規定送出去(leo 的驗收條件之一——
|
||
# 「收工方**沒讀派工單**也知道要貼回原票」)
|
||
N=$((N+1))
|
||
inj=$(printf '%s' "$(payload '【工單】inkstone/ISEP#30')" | bash "$HOOK" 2>/dev/null)
|
||
if printf '%s' "$inj" | python3 -c '
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
c = d["hookSpecificOutput"]["additionalContext"]
|
||
assert "貼回那張票" in c, "沒有交件方式"
|
||
assert "【身份】" in c, "沒有身份欄規定"
|
||
assert "不准 push" in c, "沒有 main 紅線"
|
||
' 2>/dev/null; then
|
||
printf ' ✅ ⑦ 合規的派工會被注入共通規定(交件方式+身份欄+不准 push main)\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ⑦ 共通規定沒有被注入,或缺了其中一項\n'
|
||
printf '%s\n' "$inj" | sed -n '1,4p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
clean
|
||
touch /tmp/.dispatch-ok-S-TEST
|
||
t 0 "⑧ 明示豁免戳記在 → 放行一次" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
這一次真的有例外')"
|
||
N=$((N+1))
|
||
if [ -f /tmp/.dispatch-ok-S-TEST ]; then
|
||
printf ' ❌ ⑧b 豁免戳記用完沒被消掉(會變成永久開關)\n'; FAIL=$((FAIL+1))
|
||
else
|
||
printf ' ✅ ⑧b 豁免戳記用完就消失,不是永久開關\n'; PASS=$((PASS+1))
|
||
fi
|
||
clean
|
||
|
||
echo
|
||
echo "── B 群:該擋 ──────────────────────────────────────────────────"
|
||
|
||
t 2 "⑨ **真跡**:產生 ISEP#30 這條線的那一次派工(散文開場+指令圍欄+三點就地+交件)" \
|
||
"$(payload "$FIX/real-violation-isep30.txt")"
|
||
|
||
t 2 "⑩ 只多一行散文——沒有「一句而已」這種豁免" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
記得先讀 CLAUDE.md')"
|
||
|
||
t 2 "⑪ 帶【交件】——那是「每次都一樣」,該進共通規定" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
【交件】貼回 inkstone/ISEP#30,回報分支名')"
|
||
|
||
t 2 "⑫ 帶【就地】——那是「這次才知道」,該寫進票" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
【就地】main 現在是 8e7e265')"
|
||
|
||
t 2 "⑬ 帶【人格】——已收回的欄位" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
【人格】ISEP plugin 維護者')"
|
||
|
||
t 2 "⑭ 票號形狀不對(裸號,跨 repo 會撞號)" \
|
||
"$(payload '【工單】#30')"
|
||
|
||
t 2 "⑮ 內容躲在圍欄裡也算數(圍欄只讓裡面的【】不被當欄位,不讓內容變成不存在)" \
|
||
"$(payload '【工單】inkstone/ISEP#30
|
||
```
|
||
【就地】main 現在是 8e7e265
|
||
```')"
|
||
|
||
echo
|
||
echo "── C 群:訊息本身 ──────────────────────────────────────────────"
|
||
clean
|
||
msg=$(printf '%s' "$(payload "$FIX/real-violation-isep30.txt")" | bash "$HOOK" 2>&1 >/dev/null)
|
||
|
||
N=$((N+1))
|
||
if printf '%s' "$msg" | grep -q '這句話換一張票還成立嗎' \
|
||
&& printf '%s' "$msg" | grep -q '共通規定' \
|
||
&& printf '%s' "$msg" | grep -q '寫進那張票'; then
|
||
printf ' ✅ ⑯ 訊息講得出判準與兩條出路(共通規定/寫進票)\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ⑯ 訊息缺了判準或其中一條出路\n'
|
||
printf '%s\n' "$msg" | sed -n '1,10p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
# 迴歸:訊息裡的反引號一度在別支閘被 shell 當成命令替換執行掉
|
||
# (2026-08-26 ask-user-question-guard:閘照擋,但**它教人怎麼解的那兩行變成空白**)
|
||
N=$((N+1))
|
||
if printf '%s' "$msg" | grep -q 'scripts/ticket say' \
|
||
&& printf '%s' "$msg" | grep -q 'touch /tmp/.dispatch-ok-S-TEST' \
|
||
&& ! printf '%s' "$msg" | grep -qi 'is a directory\|command not found'; then
|
||
printf ' ✅ ⑰ 訊息原文照印:反引號沒被當命令執行,出路那兩行沒有變空白\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ⑰ 訊息被 shell 展開了(指令消失,或冒出 shell 錯誤)\n'
|
||
printf '%s\n' "$msg" | sed -n '1,20p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
# 訊息要點名是哪一格違規(可迭代的最小單位——leo 4325:「照這種散文寫法根本無法迭代」)
|
||
N=$((N+1))
|
||
if printf '%s' "$msg" | grep -q '派工單不只有票號'; then
|
||
printf ' ✅ ⑱ 訊息點得出違反的是哪一條規則,不是一句籠統的「格式不對」\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ⑱ 訊息沒有點名違反哪一條\n'; FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
clean
|
||
echo
|
||
echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══"
|
||
[ "$FAIL" -eq 0 ]
|