e474b4cb6f
leo 2026-08-27:「子票相依是 gitea 原有機制,改 tag 和指定也是,這些全部都要」 08-26 掉的那一次(arcrun-rag#136 comment 4267「等雲端那半出貨才驗得了」躺了 14 小時)三格全缺:沒有子票、tag 沒動、沒有指派給任何人。根因不是誰忘了, 是那件事沒有落在任何一個「撈一次就看得到」的欄位上。 實測(Gitea 1.26.4):子票還開著時關母票 → HTTP 412 "cannot close this issue or pull request because it still has open dependencies" ⇒ 相依是平台保證的硬擋,不是提醒。跨 repo 也成立(201)。 - scripts/ticket 新增三個動詞:subtask(長子票+掛相依)/ handback(指派+改 tag+寫下一步,一個動作)/mine(撈一次看棒子在誰手上) - hooks/comment-carries-task-guard.sh:留言帶未完成的未來式卻沒開子票 → 擋一次 - hooks/baton-handback-guard.sh:一條線收工,三格缺哪一格當場說出來(提醒不擋) - docs/governance §16:三個維度/粒度(傾向多開票)/既有票只從今天起適用/ 與 s/* 的關係/實測輸出 - 測試 20+10 全綠,用 fixture 跑不打網路、不在票池留測試票 📌 踩到並記進閘的檔頭:hook 裡比對中文一律用 python3 的 re,不要用 grep 的字元類 (等[^。]{0,20}出貨 在真實 hook 呼叫路徑下對「等雲端那半出貨」不匹配,閘靜默失效) 票:inkstone/ISEP#30(comment 4334/4335/4346/4347) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
6.7 KiB
Bash
Executable File
137 lines
6.7 KiB
Bash
Executable File
#!/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 <<EOF
|
||
🏃 交棒警察:$OWNER/$REPO#$NUM 這條線收工了,但**棒子沒有交回來**。
|
||
|
||
現在票上:指派=[${NOW_A:-(空)}] 標籤=[${NOW_L:-(空)}]
|
||
|
||
$BODY
|
||
|
||
【leo 2026-08-27】「**每個任務結束必須要指派回總管**,要說明下一步怎麼做,
|
||
直到最後交出正確 deliverable 並且驗證」
|
||
|
||
🔴 **關鍵在「指派」是欄位,不是文字。** 撈一次就看得到,不必讀 comment——
|
||
leo 自己就是用 Gitea 原生的「指派給您的」在看。
|
||
|
||
【一個動作把三格補齊】
|
||
scripts/ticket handback $OWNER/$REPO#$NUM \\
|
||
--to claude-code \\
|
||
--next "<收下的人第一件事要做什麼,一句話>" \\
|
||
--label <s/review|s/stage|s/pending|s/todo> \\
|
||
--evidence "<實測輸出或 PR 連結>"
|
||
|
||
要 leo 親手做的 → --to Leo(會自動加 Human 標籤)
|
||
已經真的做完且驗過了 → scripts/ticket close $OWNER/$REPO#$NUM --deliverable <URL>
|
||
|
||
【撈一次看棒子在誰手上】 scripts/ticket mine
|
||
EOF
|
||
exit 2
|