#!/usr/bin/env bash # 管什麼: 一條派工線收工時,檢查它的工單票有沒有把棒子交回來——指派(誰該動)+ tag(卡在哪)+ 下一步(做什麼)三格。 # 為什麼: 2026-08-26 那次交回寫在 comment 裡,票沒指派、tag 沒動 ⇒ 沒有任何欄位在說「這張票在等你」,棒子躺了 14 小時。撈一次看得到的只有欄位,不是留言。 # 誤觸時怎麼關: 這條線不對應任何票 → 派工單本來就不該少了【工單】那行(no-ticket-no-dispatch.sh 管那個);真要放行在指令裡留 `baton-ok: <理由>`(本閘只提醒不阻擋,不會擋住任何動作)。 # # baton-handback-guard.sh — 收工=把棒子交回去(PostToolUse: Agent|Task) # # ── leo 2026-08-27 的原話(本閘的規格)──────────────────────────────── # 「它把事情做完後如果要檢查後續,就要把任務指定給你並改 tag, # 你收到指定用 tag 查就知道要去做,所以**每個任務結束必須要指派回總管**, # 要說明下一步怎麼做,**直到最後交出正確 deliverable 並且驗證**」 # # ── 三格各自回答一個問題(leo:「這些全部都要」)───────────────────── # 指派 assignee 現在誰該動? 缺了它 → 大家都以為是別人的事 # tag s/* 它卡在哪一段? 缺了它 → 撈得到票,但要重讀整串 # 下一步(留言) 收下的人要做什麼? 缺了它 → 撿起棒子的人得自己重建脈絡 # # ── 為什麼掛在 PostToolUse(Agent) 而不是 SubagentStop ────────────── # 工單票號寫在**派工單**裡(`【工單】owner/repo#N`),而派工單就是 Agent 的 tool_input。 # SubagentStop 拿到的 transcript 是主對話,票號要繞一圈才找得回來 # (見 subagent-claim-worksheet.sh 檔頭那段實撞紀錄)。 # 同一個位置 issue-status-autoflip.sh 已經在用,形狀是驗過的。 # # ── 只提醒,不阻擋 ──────────────────────────────────────────────── # 工作已經做完了,擋在這裡沒有任何東西可以被挽救;能做的是**讓總管在當下就看到缺哪一格**。 # 同 unpushed-police.sh 的定位:exit 2 讓訊息進到模型眼前,不是 blocker。 set -uo pipefail INPUT=$(cat) SPEC=$(printf '%s' "$INPUT" | python3 -c ' import sys, json, re try: d = json.load(sys.stdin) except Exception: raise SystemExit p = (d.get("tool_input") or {}).get("prompt") or "" for line in p.splitlines(): if "【工單】" not in line: continue m = re.search(r"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)", line) if m: print(f"{m.group(1)} {m.group(2)} {m.group(3)}") break ' 2>/dev/null || true) [ -n "${SPEC:-}" ] || exit 0 # shellcheck disable=SC2086 set -- $SPEC OWNER="$1"; REPO="$2"; NUM="$3" # ── 測試縫(唯一目的:讓判斷邏輯測得起來,不必每跑一次測試就在票池留一張票)── # BATON_GUARD_FIXTURE=<目錄> ⇒ 讀 issue.json / comments.json,不打網路。 # 正式執行時這個變數不存在,一行都不會走到。 if [ -n "${BATON_GUARD_FIXTURE:-}" ]; then J=$(cat "$BATON_GUARD_FIXTURE/issue.json") C=$(cat "$BATON_GUARD_FIXTURE/comments.json") else # token:掃已知的 checkout 找第一個帶憑證、指向本站的 remote(同 scripts/ticket 的做法) ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" URL="" for d in "$ROOT" "$ROOT/products/$REPO" "$ROOT/matrix/$REPO" "$ROOT/polaris/$REPO"; do [ -e "$d/.git" ] || continue U=$(git -C "$d" remote -v 2>/dev/null | grep -m1 'git\.uncle6\.me' | grep '@' | awk '{print $2}') || true [ -n "${U:-}" ] && { URL="$U"; break; } done [ -n "$URL" ] || exit 0 TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') [ "$TOKEN" != "$URL" ] || exit 0 J=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \ "https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null) || exit 0 C=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \ "https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM/comments?limit=100" 2>/dev/null) || C='[]' fi REPORT=$(python3 - "$J" "$C" <<'PY' 2>/dev/null import json, sys try: it = json.loads(sys.argv[1]); cs = json.loads(sys.argv[2]) except Exception: raise SystemExit if it.get("state") == "closed": raise SystemExit # 已經關了 = 棒子到終點了,沒有下一手 miss = [] if not (it.get("assignees") or []): miss.append("指派:沒有人被指到這張票 ⇒ 撈 assignee 撈不到它,等於棒子躺在地上") labs = [l["name"] for l in (it.get("labels") or [])] st = [n for n in labs if n.startswith("s/")] if not st: miss.append("tag:沒有任何 s/* ⇒ 看不出它卡在哪一段") elif st == ["s/doing"]: miss.append("tag:還停在 s/doing,但這條線已經收工了 ⇒ 它到底做完沒有?" "(等總管複驗=s/review/已上 stage=s/stage/等別的東西=s/pending)") if not any("🏃" in (c.get("body") or "") for c in cs): miss.append("下一步:票上沒有交棒留言 ⇒ 收下棒子的人得自己重讀整串才知道要做什麼") if not miss: raise SystemExit print("\n".join(" ❌ " + m for m in miss)) print("__ASSIGNEES__" + ",".join(a["login"] for a in (it.get("assignees") or []))) print("__LABELS__" + ",".join(labs)) PY ) || exit 0 [ -n "${REPORT:-}" ] || exit 0 BODY=$(printf '%s' "$REPORT" | grep -v '^__') NOW_A=$(printf '%s' "$REPORT" | sed -n 's/^__ASSIGNEES__//p') NOW_L=$(printf '%s' "$REPORT" | sed -n 's/^__LABELS__//p') cat >&2 <" \\ --label \\ --evidence "<實測輸出或 PR 連結>" 要 leo 親手做的 → --to Leo(會自動加 Human 標籤) 已經真的做完且驗過了 → scripts/ticket close $OWNER/$REPO#$NUM --deliverable 【撈一次看棒子在誰手上】 scripts/ticket mine EOF exit 2