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>
190 lines
10 KiB
Bash
Executable File
190 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 管什麼: 派工單裡除了【工單】票號以外還有別的內容就擋;合規的派工則自動注入共通規定。
|
||
# 為什麼: 任務寫在 prompt 裡 => 票上看不到、agent 一停就蒸發;散文沒有欄位 => 警察抓不到。
|
||
# 誤觸時怎麼關: 把內容歸位(每次都一樣的→共通規定;這次才知道的→寫進票),或 touch /tmp/.dispatch-ok-<session_id> 放行一次。
|
||
#
|
||
# dispatch-format-guard.sh — PreToolUse(Task|Agent):**派工單 = 票號。就這樣。**
|
||
#
|
||
# ━━ 來由(leo 2026-08-27,inkstone/ISEP#30 comment 4322 / 4325 / 4327)━━━━
|
||
#
|
||
# 他貼回總管派給 Arcrun#142 那條線的 prompt,問:
|
||
# 「**這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?**」
|
||
# 「執行的不是你,你去派工,它才知道問題,**你寫診斷意義是什麼?不就是假的?**」
|
||
#
|
||
# 實查:`no-ticket-no-dispatch.sh` 驗的是「派工單裡有沒有一行【工單】owner/repo#N」。
|
||
# ⇒ 所以可以**把 40 行任務全寫在 prompt 裡、票號補一行**,閘照樣放行。
|
||
# ⇒ 2026-08-27 一天之內這樣做了 5 次(arcrun-rag#104/Arcrun#142/Arcrun#127/
|
||
# Arcrun#144/InkStoneCo#55),每一次票上都沒有那份任務。
|
||
# ⇒ **規則存在,閘只驗了它的殼。**(同款第 N 次:history-first/KBDB-first/
|
||
# stage-first/AskUserQuestion 裸奔,全是這個形狀。)
|
||
#
|
||
# 4325 追加:「你用一個 **output parser** 把你給 subagent 的指令規範,**分作幾點,
|
||
# 每一點規定格式**,**照這種散文寫法根本無法迭代**」「**警察也不能抓**」。
|
||
# 4327 收斂到最後一步:「**交件方式不需要寫,定義在原則裡**⋯⋯每次都一樣提取出來
|
||
# 變成共通規定」+(對「這是 session 才知道的事」的回應)「**這些為什麼不寫到票裡?**」
|
||
#
|
||
# ⇒ 派工單裡想寫的東西只有兩種,兩種都不該留在派工單:
|
||
# 每次都一樣 → 共通規定(本閘自動注入,見下)
|
||
# 這次才知道 → **寫進那張票**
|
||
# 判準:**「這句話換一張票還成立嗎?」**
|
||
#
|
||
# ━━ 為什麼是結構閘,不是關鍵字閘(leo 2026-08-17 的檢驗)━━━━━━━━━━━━
|
||
# 「你在**文字層**封路⋯⋯『回「做」我就啓動』沒有問號,下次它會寫『準備就緒』⋯⋯
|
||
# **自然語言的變體是無限的,blacklist 永遠追不完。**
|
||
# 封路哲學之所以有效,是因為它封的是**動作**——動作有限且可枚舉,文字不是。」
|
||
# 當日實證:文字層的閘 8 次誤攔、0 次正確攔截,且方向穩定——
|
||
# **紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。**
|
||
#
|
||
# 本閘怎麼過這個檢驗:
|
||
# · 判準是**「這一行是不是【工單】欄位」**——在不在,不是寫什麼。
|
||
# 整條路徑(hooks/lib/dispatch_parse.py)**零個「命中某個詞就違規」的比對**。
|
||
# · ⇒ 措辭再謹慎也不會被多罰;改寫成別的講法也閃不過去。文字層閘的兩個病都沒有。
|
||
# · 也**不需要語意判官**:這條規則本來就是結構性的,用不著 LLM,
|
||
# 於是它免費、瞬間、每次結果一樣(比 ask-user-question-guard 更硬的一種閘)。
|
||
#
|
||
# ━━ 這支同時做兩件事 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
# ① **擋**:【工單】以外有實質內容 → exit 2,並指出那些內容該搬去哪一格
|
||
# ② **注入**:合規的派工 → exit 0 並用 hookSpecificOutput 把共通規定送給收工方
|
||
# (慣例同 subagent-wiki-guard.sh)。
|
||
# ⇒ 這是「派工單只剩票號」能成立的前提:交件方式、不准 push main、org 是 inkstone…
|
||
# **不必有人記得寫**,機器每次都補。
|
||
# leo 的驗收條件之一就是「收工方**沒讀派工單**也知道要貼回原票」。
|
||
#
|
||
# ━━ 邊界(刻意不管的事)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
# · 派工單裡**沒有**【工單】 → 本閘閉嘴,那是 no-ticket-no-dispatch.sh 的地盤。
|
||
# 兩支閘同時開口,收工方會拿到兩份互相打架的教學。
|
||
# · 票存不存在、關了沒 → 也是那一支在驗,本閘不重複打 Gitea(本閘不連網)。
|
||
#
|
||
# 逃生口(留痕、用完即丟,慣例同 micromanage-guard.sh/ask-user-question-guard.sh):
|
||
# touch /tmp/.dispatch-ok-<session_id>
|
||
# 閘是絆一下逼你重想,不是把人關起來。
|
||
#
|
||
# 測試:hooks/tests/dispatch-format-guard.test.sh(離線、不打網路、不花錢)
|
||
set -u
|
||
|
||
INPUT="$(cat)"
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||
|
||
FACTS="$(printf '%s' "$INPUT" | python3 "$HERE/lib/dispatch_parse.py" dispatch 2>/dev/null)"
|
||
[ -n "$FACTS" ] || exit 0 # parser 掛了 → fail-open(回到「沒有這支閘」的狀態)
|
||
|
||
STATUS="$(printf '%s' "$FACTS" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("status", "skip"))
|
||
except Exception: print("skip")
|
||
' 2>/dev/null)"
|
||
[ "$STATUS" = "ok" ] || exit 0
|
||
|
||
SESSION="$(printf '%s' "$FACTS" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("session_id", ""))
|
||
except Exception: print("")
|
||
' 2>/dev/null)"
|
||
|
||
NVIOL="$(printf '%s' "$FACTS" | python3 -c '
|
||
import sys, json
|
||
try: print(len(json.load(sys.stdin).get("violations", [])))
|
||
except Exception: print(0)
|
||
' 2>/dev/null)"
|
||
|
||
# ── 明示豁免(留痕用,用掉就消失,不是永久開關)────────────────────────
|
||
if [ "$NVIOL" != "0" ] && [ -n "$SESSION" ] && [ -f "/tmp/.dispatch-ok-$SESSION" ]; then
|
||
rm -f "/tmp/.dispatch-ok-$SESSION"
|
||
NVIOL=0
|
||
fi
|
||
|
||
if [ "$NVIOL" != "0" ]; then
|
||
# 訊息用 quoted heredoc(裡面有反引號,不能讓 shell 當命令替換執行掉),
|
||
# 佔位符交給 python 換——sed 對正體中文與特殊字元的跳脫太脆。
|
||
# (2026-08-26 ask-user-question-guard 真的犯過:訊息裡的反引號被當命令跑,
|
||
# 閘照擋,但**它教人怎麼解的那兩行變成空白**,等於沒有出路。)
|
||
MSG="$(cat <<'EOF'
|
||
🚫 派工單 = 票號。就這樣。(leo 2026-08-27,inkstone/ISEP#30 comment 4327)
|
||
|
||
__VIOLATIONS__
|
||
|
||
━━ 這些內容該去哪:問一句「**這句話換一張票還成立嗎?**」━━━━━━━━━━
|
||
|
||
還成立 ⇒ **共通規定**(交件方式、不准 push main、org 是 inkstone、先讀 CLAUDE.md…)
|
||
改 `docs/governance/dispatch-and-reply-format.md` 的 §2,
|
||
**本閘每次派工都會自動注入給收工方**,你不必寫、也不會漏。
|
||
|
||
只有這次 ⇒ **寫進那張票**(main 是哪顆、今天撞過什麼、另一條線在動什麼)
|
||
`scripts/ticket say <owner/repo#N> -F <檔>`
|
||
然後派工單寫 `【工單】<owner/repo#N> → comment <剛拿到的號碼>`
|
||
|
||
🔴 **「票上還沒有」不是把它寫進 prompt 的理由——它就是「去把它寫上票」的指令。**
|
||
leo 2026-08-27:「**這些為什麼不寫到票裡?**」
|
||
寫進 prompt 的後果:agent 被停掉或換人接手,那段事實隨 prompt 一起蒸發。
|
||
當天實害:總管停掉重派 3 次,前兩次的任務與 session 事實**全部消失**。
|
||
|
||
━━ 合格的派工單長這樣(整份,沒有省略)━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
【工單】inkstone/ISEP#30 → comment 4322
|
||
|
||
━━ 為什麼連「一句叮嚀」都不行 ━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
leo 2026-08-27:「**交件方式不需要寫,定義在原則裡,每張票都要做這件事⋯⋯
|
||
每次都一樣提取出來變成共通規定**」
|
||
你多寫的每一句,都是在造第二份會漂的真相;而收工方讀到的是**你的轉述**,
|
||
不是票上的原始證據。
|
||
|
||
真的有例外 → `touch /tmp/.dispatch-ok-__SESSION__` 後重送(用掉就消失,會留在指令歷史上)。
|
||
EOF
|
||
)"
|
||
VIOL_TEXT="$(printf '%s' "$FACTS" | python3 -c '
|
||
import sys, json
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
d = {}
|
||
lines = []
|
||
for v in d.get("violations", []):
|
||
lines.append(" ❌ %s:%s" % (v.get("code", ""), v.get("detail", "")))
|
||
for name in d.get("retired_seen", []):
|
||
lines.append(" ↳ 【%s】這個欄位已經收回了" % name)
|
||
print("\n".join(lines))
|
||
' 2>/dev/null)"
|
||
|
||
DFG_MSG="$MSG" DFG_VIOL="$VIOL_TEXT" DFG_SESSION="$SESSION" python3 -c '
|
||
import os, sys
|
||
sys.stderr.write(
|
||
os.environ["DFG_MSG"]
|
||
.replace("__VIOLATIONS__", os.environ.get("DFG_VIOL", ""))
|
||
.replace("__SESSION__", os.environ.get("DFG_SESSION", "") or "<session_id>")
|
||
+ "\n"
|
||
)
|
||
'
|
||
exit 2
|
||
fi
|
||
|
||
# ── 合規 → 注入共通規定 ──────────────────────────────────────────────
|
||
# 這是「派工單只剩票號」的另一半:那些每次都一樣的規定不必有人記得寫,機器每次都補。
|
||
# 來源是 docs/governance/dispatch-and-reply-format.md 的 INJECT 區段——
|
||
# **只有一份**,改那裡等於改所有派工(不會有第二份漂走的副本)。
|
||
DOC="${CLAUDE_PLUGIN_ROOT:-$(cd "$HERE/.." && pwd)}/docs/governance/dispatch-and-reply-format.md"
|
||
[ -f "$DOC" ] || exit 0
|
||
|
||
DFG_DOC="$DOC" python3 -c '
|
||
import json, os, re, sys
|
||
try:
|
||
text = open(os.environ["DFG_DOC"], encoding="utf-8", errors="ignore").read()
|
||
except Exception:
|
||
sys.exit(0)
|
||
m = re.search(r"<!-- INJECT:BEGIN.*?-->\n(.*?)<!-- INJECT:END -->", text, re.S)
|
||
if not m:
|
||
sys.exit(0)
|
||
body = m.group(1).strip()
|
||
print(json.dumps({
|
||
"hookSpecificOutput": {
|
||
"hookEventName": "PreToolUse",
|
||
"additionalContext":
|
||
"【自動注入:派工共通規定】\n"
|
||
"(來源 docs/governance/dispatch-and-reply-format.md §2——"
|
||
"派工單只給票號是刻意的,這些每次都一樣的東西由機器補,不靠人記得寫)\n\n"
|
||
+ body,
|
||
}
|
||
}, ensure_ascii=False))
|
||
' 2>/dev/null
|
||
|
||
exit 0
|