5ac06abc95
三張票同一族(誰在派、派給誰、派的內容住哪裡),做在同一條分支: #86 工人名單:agents/ 七位有名字的工人+scripts/roster+hooks/roster-guard.sh 派工用 Task 的 subagent_type 指名,派工單格式一個字都沒改; 指對名字就把那位的檔案原文注入(你是誰/先讀什麼/你的紅線)。 【身份】欄同時吃得下工人名字(原本三個角色照舊)。 #87 未經調查不寫診斷:hooks/diagnosis-evidence-guard.sh + investigate-first-stamp.sh 三個結構訊號(派過人查沒/有沒有走得過去的出處/有沒有份量), 一個關鍵字比對都沒有;轉述有出處不會被誤擋。 #88 回覆也是派工:不另造閘,把攔截點加掛上去。 hooks/lib/dispatch_parse.py 的 tool_channel() 一次列全所有通往 subagent 的路 (SendMessage/雲端 session・trigger/claude -p);擋下來時把那段內容原文印出來。 subagent 往上回報(to: "main")=交件不是派工,刻意不管。 順手修掉一個真的會咬人的 flake:dispatch-format-guard 原本開四支 python 各讀一個欄位, 機器忙的時候某個欄位會靜靜變空字串(實測連跑 10 次有 1 次「豁免了卻還是被擋」)。 四個欄位改成一次讀完。 版本號待總管定(plugin.json 只更新了描述裡的數字,版本沒動)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
172 lines
9.8 KiB
Bash
Executable File
172 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# diagnosis-evidence-guard.sh + investigate-first-stamp.sh 的迴歸測試(inkstone/ISEP#87)
|
||
#
|
||
# 票上的四條驗收條件,一條一組:
|
||
# 1 開一張還沒有人查過的票,在裡面寫「根因是 X」 → 要被擋一次
|
||
# 2 派人查過、結論貼回票上了,再寫診斷 → 放行
|
||
# 3 擋的訊息要講得出「你還沒派人查這件事」,不是「格式不對」
|
||
# 4 只是轉述別人查到的東西(有出處) → **不能被誤擋**
|
||
#
|
||
# 🔴 全程離線:不打 Gitea、不開任何測試票、不碰 /tmp 的正式戳記
|
||
# (戳記走 `ISEP_STAMP_DIR`)。每次結果一樣——純結構判斷,沒有語意判官。
|
||
#
|
||
# 🔴 這支閘**沒有任何關鍵字比對**(票上明令:「根因」「因為」「應該是」不准當判準)。
|
||
# 所以下面「該擋」的案例裡有沒有出現那些詞完全不影響結果——
|
||
# ⑬ 就是拿一段**一個那種詞都沒有**的內容,證明判準不是措辭。
|
||
#
|
||
# 用法:hooks/tests/diagnosis-evidence-guard.test.sh [hook 路徑]
|
||
set -u
|
||
unset CLAUDE_CODE_CHILD_SESSION
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||
HOOK="${1:-$ROOT/hooks/diagnosis-evidence-guard.sh}"
|
||
STAMP="$ROOT/hooks/investigate-first-stamp.sh"
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
export ISEP_STAMP_DIR="$TMP"
|
||
|
||
PASS=0; FAIL=0; N=0
|
||
|
||
bash_payload() {
|
||
python3 - "$1" "$2" <<'PY'
|
||
import json, sys
|
||
print(json.dumps({"session_id": sys.argv[1], "hook_event_name": "PreToolUse",
|
||
"tool_name": "Bash", "tool_input": {"command": sys.argv[2]}},
|
||
ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
# t <期望 exit> <說明> <session> <指令> [必須出現的字串]
|
||
t() {
|
||
want="$1"; desc="$2"; sid="$3"; cmd="$4"; must="${5:-}"
|
||
N=$((N+1))
|
||
err=$(bash_payload "$sid" "$cmd" | bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||
ok=1
|
||
[ "$rc" -eq "$want" ] || ok=0
|
||
if [ -n "$must" ] && ! printf '%s' "$err" | grep -qF "$must"; then ok=0; fi
|
||
if [ "$ok" -eq 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ %s —— 期望 exit=%s%s,實得 exit=%s\n' "$desc" "$want" \
|
||
"${must:+ 且訊息含「$must」}" "$rc"
|
||
printf '%s\n' "$err" | sed -n '1,8p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
}
|
||
|
||
# 派一次工去查某張票(=讓戳記出現)
|
||
dispatch() {
|
||
python3 - "$1" "$2" <<'PY' | bash "$STAMP"
|
||
import json, sys
|
||
print(json.dumps({"session_id": sys.argv[1], "tool_name": "Task",
|
||
"tool_input": {"prompt": "【工單】%s" % sys.argv[2]}},
|
||
ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
# ── 測資:三份內文 ────────────────────────────────────────────────
|
||
# ① 沒查過的診斷(0 個出處)
|
||
cat > "$TMP/diagnosis.md" <<'EOF'
|
||
【身份】總管/inkstone/ISEP/-
|
||
根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存,
|
||
這樣就不會再撞到額度上限了。這件事拖了很久一直沒解,先照這個方向做下去。
|
||
EOF
|
||
# ② 同一段話,但帶著出處(轉述別人查到的)
|
||
cat > "$TMP/relayed.md" <<'EOF'
|
||
【身份】總管/inkstone/ISEP/-
|
||
scout 查回來的:寫入那段在 hooks/lib/evidence.py:42,額度是在那裡爆的,
|
||
出處 inkstone/Arcrun#142 → comment 4327。我只是把它的結論搬過來,沒有自己加判斷。
|
||
EOF
|
||
# ③ 一段**一個「根因/因為/應該是」都沒有**的長內容,同樣沒有出處
|
||
cat > "$TMP/no-keyword.md" <<'EOF'
|
||
【身份】總管/inkstone/ISEP/-
|
||
那個匯入流程跑不動是設定檔的問題,換掉那個欄位就會通了,我看過一次有印象。
|
||
先照這個方向改,改完應該就不會再出現同樣的狀況了,不用另外再查。
|
||
EOF
|
||
# ④ 短回覆
|
||
SHORT="收到,我來看"
|
||
|
||
echo "── 驗收 1+3:沒人查過、也沒出處 ⇒ 擋,且說得出「還沒派人查」──"
|
||
|
||
t 2 "① 貼一段沒人查過的診斷到票上 ⇒ 擋" \
|
||
S-A "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md"
|
||
t 2 "② 訊息講得出「這個 session 沒有派人去查這張票」(不是籠統的「格式不對」)" \
|
||
S-B "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" \
|
||
'沒有派人去查這張票'
|
||
t 2 "③ 訊息要點名是哪一張票(人才知道該派誰去查什麼)" \
|
||
S-C "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" 'inkstone/ISEP#87'
|
||
t 2 "④ 訊息要給兩條出路:先派人查/把出處帶上" \
|
||
S-D "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" '先派人去查'
|
||
t 2 "⑤ 開新票也一樣(新票天生沒人查過,`ticket new`)" \
|
||
S-E "python3 scripts/ticket new ISEP -F $TMP/diagnosis.md --title 身為X我要Y我才Z"
|
||
t 2 "⑥ 側門:直接打 Gitea API POST 留言 ⇒ 同樣擋(認動作的方式不准漏一條路)" \
|
||
S-F "curl -X POST -H 'Authorization: token T' -d '{\"body\":\"根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存,這樣就不會再撞到額度上限了,這件事拖了很久一直沒解,先照這個方向做下去不要再查了\"}' https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/87/comments"
|
||
t 2 "⑦ 側門:隱式 POST(requests.post)也要認得" \
|
||
S-G "python3 -c \"requests.post(url + '/repos/inkstone/ISEP/issues/87/comments', json={'body': '根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存,這樣就不會再撞到額度上限了,這件事拖了很久一直沒解,先照這個方向做下去不要再查了'})\""
|
||
|
||
echo "── 驗收 2:派人查過 ⇒ 放行 ─────────────────────────────────"
|
||
|
||
dispatch S-H inkstone/ISEP#87
|
||
t 0 "⑧ 同一 session 派過人去查 #87 ⇒ 同一段診斷放行" \
|
||
S-H "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md"
|
||
|
||
dispatch S-I inkstone/ISEP#86
|
||
t 2 "⑨ 派的是**別張票**(#86)⇒ #87 仍然沒人查過 ⇒ 照擋(戳記按票分,不是按 session)" \
|
||
S-I "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md"
|
||
|
||
echo "── 驗收 4:轉述有出處 ⇒ 不能被誤擋 ─────────────────────────"
|
||
|
||
t 0 "⑩ 帶檔案:行號+票號+comment 號的轉述 ⇒ 放行" \
|
||
S-J "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/relayed.md"
|
||
|
||
for pair in "網址:https://git.uncle6.me/inkstone/ISEP/pulls/97" \
|
||
"commit:8e28041" \
|
||
"檔案路徑:hooks/lib/dispatch_parse.py"; do
|
||
kind="${pair%%:*}"; token="${pair#*:}"
|
||
printf '【身份】總管/inkstone/ISEP/-\n那段行為我核對過了,寫在 %s,跟票上寫的不一樣,所以下一步要照那邊的實況走,先不要動別的地方。\n' \
|
||
"$token" > "$TMP/one-source.md"
|
||
t 0 "⑪ 出處只有一個「$kind」也算數 ⇒ 放行" \
|
||
"S-K-$kind" "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/one-source.md"
|
||
done
|
||
|
||
printf '【身份】總管/inkstone/ISEP/-\n實測輸出如下,這是它自己跑出來的,不是我推的:\n```\n$ bash hooks/tests/x.sh\n通過 20 條,失敗 0 條\n```\n' > "$TMP/fenced.md"
|
||
t 0 "⑫ 貼實測輸出(圍欄)也算出處 ⇒ 放行" \
|
||
S-L "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/fenced.md"
|
||
|
||
echo "── 判準不是措辭(本閘沒有關鍵字比對)────────────────────────"
|
||
|
||
t 2 "⑬ 一個「根因/因為/應該是」都沒有的內容,照樣擋(判準是結構不是詞)" \
|
||
S-M "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/no-keyword.md"
|
||
|
||
echo "── 不該擋(誤攔比漏擋嚴重)──────────────────────────────────"
|
||
|
||
t 0 "⑭ 一句短回覆(「$SHORT」)⇒ 不可能是診斷,放行" \
|
||
S-N "python3 scripts/ticket say inkstone/ISEP#87 --body '$SHORT'"
|
||
t 0 "⑮ GET 撈票(唯讀)⇒ 放行" \
|
||
S-O "curl -s https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/87"
|
||
t 0 "⑯ ticket where(搜尋)⇒ 不是寫票,放行" \
|
||
S-P "python3 scripts/ticket where 診斷 調查 順序"
|
||
t 0 "⑰ ticket close(關票)⇒ 不是寫內文,放行" \
|
||
S-Q "python3 scripts/ticket close inkstone/ISEP#87 --deliverable https://git.uncle6.me/x"
|
||
t 0 "⑱ 一般指令(ls)⇒ 一律不管" S-R "ls -la"
|
||
t 0 "⑲ 逃生口 evidence-ok 在指令裡 ⇒ 放行(留痕)" \
|
||
S-S "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md # evidence-ok"
|
||
|
||
echo "── 只擋一次,不鬼打牆 ──────────────────────────────────────"
|
||
|
||
t 2 "⑳ 第一次 ⇒ 擋" S-T "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md"
|
||
t 0 "㉑ 同一 session 同一張票第二次 ⇒ 放行(閘是絆一下,不是把人關起來)" \
|
||
S-T "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md"
|
||
t 2 "㉒ 同一 session **換一張票** ⇒ 仍然擋(不是一次就永久解鎖)" \
|
||
S-T "python3 scripts/ticket say inkstone/Arcrun#142 -F $TMP/diagnosis.md"
|
||
|
||
echo "── 內部壞掉不准把人鎖死 ────────────────────────────────────"
|
||
|
||
t 0 "㉓ 整包不是合法 JSON ⇒ fail-open" S-U ''
|
||
N=$((N+1))
|
||
err=$(printf 'not json at all' | bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||
if [ "$rc" -eq 0 ]; then printf ' ✅ ㉔ payload 不是 JSON ⇒ fail-open\n'; PASS=$((PASS+1))
|
||
else printf ' ❌ ㉔ payload 壞掉時擋住了(exit=%s)\n' "$rc"; FAIL=$((FAIL+1)); fi
|
||
|
||
echo
|
||
printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N"
|
||
[ "$FAIL" -eq 0 ] || exit 1
|