交棒警察不再對剛出發的背景工人說「收工了」(inkstone/ISEP#153)

- PostToolUse:Agent 看到 launch ack(isAsync/async_launched/"Async agent launched")就不查票
- 多掛一條 Stop:主 transcript 出現 <task-notification> completed 時,用 tool-use-id
  對回原本那則 Agent tool_use、取【工單】再查三格;同一條線只點名一次(狀態檔)
- 出路改印絕對路徑:InkStoneCo/scripts/ticket 是舊複本,grep -c handback → 0
- 測試 10 → 24 條;新 14 條拿 main 的舊 hook 跑是 7 條紅
- 盤點:62 支、88 條(+1)、65 支腳本,當場數的

待總管定版(plugin.json version 未動)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 17:30:45 +08:00
parent 916a6890ff
commit 436e30b9d1
7 changed files with 309 additions and 79 deletions
+103 -8
View File
@@ -1,11 +1,14 @@
#!/bin/bash
# 交棒警察的測試——三格(指派/tag/下一步)各自缺漏時要被抓到
# 判準:5 種該報、5 種不該報
# 判準:前景 5 種該報、5 種不該報;背景派工(inkstone/ISEP#153)送出時不報、交件時才報、只報一次;
# 訊息印的出路照著貼真的跑得完。
# 用 fixture 跑,不打網路、不在票池留下任何一張測試票。
cd "$(dirname "$0")/.." || exit 1
ROOT=$(pwd)
H=hooks/baton-handback-guard.sh
PASS=0; FAIL=0
FX=$(mktemp -d); trap 'rm -rf "$FX"' EXIT
FX=$(mktemp -d); ST=$(mktemp -d); trap 'rm -rf "$FX" "$ST"' EXIT
export CLAUDE_PLUGIN_ROOT="$ROOT" ISEP_BATON_STATE_DIR="$ST"
mkfx(){ # $1=state $2=assignees(csv) $3=labels(csv) $4=有沒有交棒留言(y/n)
python3 - "$FX" "$1" "$2" "$3" "$4" <<'PY'
@@ -22,12 +25,15 @@ json.dump(comments, open(os.path.join(d, "comments.json"), "w"))
PY
}
judge(){ # $1=want $2=got $3=說明
if [ "$2" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s %s\n' "$1" "$2" "$3"
}
run(){ # $1=want $2=說明 fixture 已備好)
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\n做點事"}}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
got=$?
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s %s\n' "$1" "$got" "$2"
judge "$1" "$?" "$2"
}
echo "── 該報:三格有缺 ──"
@@ -46,9 +52,98 @@ mkfx closed "" "" n ; run 0 "票已關=棒子到
echo "── 派工單裡沒有【工單】那行 ⇒ 不歸這支管 ──"
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"隨便做點事"}}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
got=$?
if [ "$got" = 0 ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=0 got=%s 沒有票號的派工單\n' "$got"
judge 0 "$?" "沒有票號的派工單"
# ═══ inkstone/ISEP#153:背景派工 ═══════════════════════════════════════
post(){ # $1=tool_response JSON → 離開碼
printf '{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\\n做點事","run_in_background":true},"tool_response":%s}' "$1" \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
}
echo "── 驗收 1:背景派工送出的那一刻不報(票上三格全缺也一樣)──"
mkfx open "" "s/doing" n
post '{"isAsync":true,"status":"async_launched","agentId":"a93f7dc98c50fc671","description":"x"}'
judge 0 "$?" "launch acktoolUseResult 真跡的形狀:isAsyncasync_launched"
post '[{"type":"text","text":"Async agent launched successfully. (This tool result is internal metadata)\nagentId: a1"}]'
judge 0 "$?" "launch ack 只剩文字 content 陣列時也認得"
post '{"status":"completed","content":[{"type":"text","text":"工作做完了"}]}'
judge 2 "$?" "驗收 3 對照:前景派工回來(不是 launch ack)照舊要報"
# 假 transcript:派工 tool_use launch ack +(可選)完工通知
TP="$ST/main.jsonl"
mktx(){ # $1=通知對到哪個 tool-use-id(空=還沒交件) $2=通知 status $3=派工單第一行
python3 - "$TP" "$1" "$2" "$3" <<'PY'
import json, sys
tp, note_id, status, first = sys.argv[1:5]
L = []
def add(o): L.append(json.dumps(o, ensure_ascii=False))
for tid, line in (("toolu_AAA", first), ("toolu_BBB", "【工單】inkstone/ISEP#999")):
add({"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "tool_use", "id": tid, "name": "Agent",
"input": {"prompt": line + "\n做點事", "run_in_background": True}}]}})
add({"type": "user", "message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tid,
"content": [{"type": "text", "text": "Async agent launched successfully."}]}]},
"toolUseResult": {"isAsync": True, "status": "async_launched"}})
if note_id:
n = ("<task-notification>\n<task-id>a1</task-id>\n<tool-use-id>%s</tool-use-id>\n"
"<status>%s</status>\n<summary>Agent finished</summary>\n<result>好了</result>\n"
"</task-notification>") % (note_id, status)
# 真跡裡同一則通知出現三次:enqueueattachmentremove
add({"type": "queue-operation", "operation": "enqueue", "content": n})
add({"type": "attachment", "attachment": {"type": "queued_command", "prompt": n}})
add({"type": "queue-operation", "operation": "remove", "content": n})
open(tp, "w").write("\n".join(L) + "\n")
PY
}
stop(){ # $1=session id → 離開碼;stderr 存到 $ST/err
printf '{"hook_event_name":"Stop","session_id":"%s","transcript_path":"%s","stop_hook_active":false}' "$1" "$TP" \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>"$ST/err"
}
echo "── 驗收 2:背景工人真的交件時才報 ──"
mkfx open "" "s/doing" n
mktx "" "" "【工單】inkstone/arcrun-rag#136"
stop s1; judge 0 "$?" "工人還在跑(沒有完工通知)⇒ 不報"
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
stop s1; got=$?; judge 2 "$got" "完工通知 completed 票上三格全缺 ⇒ 報"
if grep -q 'inkstone/arcrun-rag#136' "$ST/err" && ! grep -q 'ISEP#999' "$ST/err"; then got=ok; else got=bad; fi
judge ok "$got" "只點名那一則通知對到的票(tool-use-id 比對;另一條還在跑的 #999 不點)"
cp "$ST/err" "$ST/msg"
stop s1; judge 0 "$?" "同一則通知第二次 Stop ⇒ 不鬼打牆(只點名一次)"
mktx toolu_AAA killed "【工單】inkstone/arcrun-rag#136"
stop s2; judge 0 "$?" "通知不是 completedkilled)⇒ 不當成交件"
mkfx open "claude-code" "s/review" y
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
stop s3; judge 0 "$?" "交件時三格已經補齊 ⇒ 安靜(出路:補完就不再出現)"
mkfx open "" "s/doing" n
mktx toolu_AAA completed "隨便做點事,沒有工單"
stop s4; judge 0 "$?" "交件的那條派工單沒有【工單】⇒ 不歸這支管"
printf '{"hook_event_name":"Stop","session_id":"s5","transcript_path":"/nonexistent"}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
judge 0 "$?" "讀不到 transcript ⇒ 放行"
echo "── 出路:訊息印的那塊指令照著貼真的跑得完 ──"
BLK=$(python3 - "$ST/msg" <<'PY'
import sys, re
t = open(sys.argv[1], encoding="utf-8").read()
m = re.search(r'^\s*("[^"\n]*/scripts/ticket" handback .*?--evidence "[^"\n]*")', t, re.S | re.M)
if not m:
raise SystemExit
b = m.group(1)
b = b.replace("<收下的人第一件事要做什麼,一句話>", "去複驗")
b = re.sub(r"<s/review[^>]*>", "s/review", b)
b = b.replace("<實測輸出或 PR 連結>", "測試")
print(b)
PY
)
case "$BLK" in /*|\"/*) got=abs ;; *) got="${BLK:0:30}" ;; esac
judge abs "$got" "出路印的是絕對路徑(InkStoneCo/scripts/ticket 是舊複本,沒有 handback"
OFF="TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN_CLAUDE_CODE=fake ISEP_API_RETRIES=0"
env $OFF bash -c "$BLK" >/dev/null 2>&1
judge 1 "$?" "★ 原樣貼上只填三格 ⇒ 參數全過、走到網路才停(離開碼 1;2=被自己的用法擋下)"
env $OFF bash -c "${BLK/--next/--nope}" >/dev/null 2>&1
judge 2 "$?" "對照組:拿掉 --next ⇒ 確實跑不完(上一條才有鑑別力)"
echo
echo "$PASS/$((PASS+FAIL)) 通過"