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>
123 lines
7.0 KiB
Bash
Executable File
123 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 管什麼: 往 Gitea 票裡貼「留言裡藏著一件還沒做完的事」(等 X/驗不了/還沒…)卻沒有把它長成子票時,擋一次。
|
||
# 為什麼: 2026-08-26 arcrun-rag#136 comment 4267 就是這樣掉的——「等雲端那半出貨才驗得了」只活在討論串,收端後來真的上線了,卻沒有任何東西讓人回頭把它撿起來,躺了 14 小時。
|
||
# 誤觸時怎麼關: 這則留言真的不含待辦 → 指令裡加 `no-subtask: <一句理由>` 放行(留痕);同一 session 只擋一次。
|
||
#
|
||
# comment-carries-task-guard.sh — 討論串裡的任務要長成看得見的子票(PreToolUse: Bash)
|
||
#
|
||
# ── leo 2026-08-27 的原話(本閘的規格)────────────────────────────────
|
||
# 「可以在討論串延伸子票,它完成了子票就完工,不然這筆任務就是沒完工,
|
||
# 如果你沒辦法察覺,那不要把任務加在討論串,而是每個任務長出子票做為這張票的相依,
|
||
# **票沒完工可以察覺嗎?**」
|
||
#
|
||
# ── 為什麼子票比「讓等待會響」更根本 ─────────────────────────────────
|
||
# 任務寫在討論串 → 要有人回頭讀 comment 才知道它存在 → 沒人回頭讀 → 掉
|
||
# 任務長成子票 → open/closed 是**狀態** → 撈一次就看得到 → 掉不了
|
||
# ⇒ 不是「讓它會響」,是**讓它本來就在視線裡**。
|
||
#
|
||
# 而且相依是硬的:實測(2026-08-27,Gitea 1.26.4)子票還開著時關母票 →
|
||
# HTTP 412 `cannot close this issue or pull request because it still has open dependencies`。
|
||
#
|
||
# ── 粒度:傾向多開票,不是少開票(leo 2026-08-27 當場更正總管)────────
|
||
# 「**小任務做完就關閉,不會讓池子爆掉,每個事情沒有歷史記錄才是大問題**」
|
||
# ⇒ 判準是「這件事需要被記得嗎」,不是「會不會太多」。
|
||
# 池子爆掉的前提是「開了不關」,那由「做完就關」解決,不該由「不開票」解決。
|
||
#
|
||
# ── 誠實邊界 ──────────────────────────────────────────────────────
|
||
# PreToolUse 只看得到指令文字,這是這一層的天花板(同 InkStoneCo#36)。
|
||
# 所以關鍵詞刻意取**窄**的一組「未完成的未來式」,寧可漏抓也不要天天誤擋——
|
||
# `.claude/branch-holds.md` 檔頭那句:**永遠在響的警報,等於訓練人忽略這個警報。**
|
||
# 漏抓的那些,由 baton-handback-guard.sh(收工那一刻)補第二層。
|
||
set -uo pipefail
|
||
|
||
INPUT=$(cat)
|
||
CMD=$(printf '%s' "$INPUT" | python3 -c "
|
||
import sys,json
|
||
try: print(json.load(sys.stdin).get('tool_input',{}).get('command',''))
|
||
except Exception: print('')
|
||
" 2>/dev/null)
|
||
SID=$(printf '%s' "$INPUT" | python3 -c "
|
||
import sys,json
|
||
try: print(json.load(sys.stdin).get('session_id','') or 'nosid')
|
||
except Exception: print('nosid')
|
||
" 2>/dev/null || echo nosid)
|
||
[ -n "$CMD" ] || exit 0
|
||
|
||
# 逃生口(留痕,會留在指令歷史上)
|
||
case "$CMD" in *no-subtask:*) exit 0 ;; esac
|
||
|
||
# 這些動詞本來就是在處理「棒子」,不該被自己擋
|
||
case "$CMD" in
|
||
*"ticket subtask"*|*"ticket handback"*|*"ticket mine"*|*"ticket close"*|*"ticket where"*) exit 0 ;;
|
||
esac
|
||
|
||
# ① 這是不是在對「既有票」貼留言?(開新票走 ticket new/ticket-api-bypass-guard)
|
||
IS_COMMENT=no
|
||
case "$CMD" in *"ticket say"*) IS_COMMENT=yes ;; esac
|
||
if printf '%s' "$CMD" | grep -qE 'issues/[0-9]+/comments' && printf '%s' "$CMD" | grep -qw 'POST'; then
|
||
IS_COMMENT=yes
|
||
fi
|
||
[ "$IS_COMMENT" = "yes" ] || exit 0
|
||
|
||
# ② 把「要貼的內文」拿到手:-F <檔> 就讀那個檔,否則就看指令文字本身
|
||
BODY="$CMD"
|
||
FARG=$(printf '%s' "$CMD" | sed -nE 's/.*[[:space:]](-F|--file)[[:space:]]+([^[:space:]]+).*/\2/p' | head -1)
|
||
if [ -n "${FARG:-}" ] && [ -f "$FARG" ]; then
|
||
BODY=$(cat "$FARG" 2>/dev/null || printf '%s' "$CMD")
|
||
fi
|
||
|
||
# 已經帶著交棒標記的留言 = 走的是 handback 那條路,不重複擋
|
||
case "$BODY" in *"🏃"*) exit 0 ;; esac
|
||
|
||
# ③ 「未完成的未來式」——刻意取窄。這一組全部來自 08-26 那則真的掉了的留言的形狀。
|
||
#
|
||
# 🔴 比對用 python3 不用 grep——這是**實測逼出來的**,不是偏好(2026-08-27 本閘第一版):
|
||
# 第一版寫 `等[^。]{0,20}(出貨|…)`,跑真的 hook 呼叫路徑時對
|
||
# 「這件事等雲端那半出貨才驗得了」**不匹配**,閘靜默失效(測試 19/20,那一條紅)。
|
||
# 同一條 pattern 在別的 shell 手打卻會過 ⇒ **它的行為依賴 locale/grep 實作**,
|
||
# 而 hook 的執行環境不由我們決定(本機 `LC_CTYPE=C`,雲端又是另一套)。
|
||
# unpushed-police.sh 檔頭記過同一族的坑:C locale 下處理全形標點會咬掉位元組。
|
||
# ⇒ 規約:**hook 裡凡是要比對中文,一律交給 python3 的 re**(它一定是字元語意),
|
||
# 不要賭 grep 在那台機器上是字元模式還是位元組模式。
|
||
HIT=$(BODY="$BODY" python3 -c '
|
||
import os, re, sys
|
||
PAT = ("等.{0,20}?(出貨|上線|部署|發佈|合併|修好|回覆|做完|接上)"
|
||
"|驗不了|測不了|還沒(驗|測|做|上線|部署|接|補)|之前(驗不|無法|不能)"
|
||
"|要等|尚未完成|待辦|\\[blocker\\]|TODO|FIXME")
|
||
ms = [m.group(0) for m in re.finditer(PAT, os.environ.get("BODY", ""))]
|
||
print(" ".join(ms[:3]))
|
||
' 2>/dev/null)
|
||
[ -n "${HIT:-}" ] || exit 0
|
||
|
||
WARNED="/tmp/.comment-task-guard-warned-$SID"
|
||
[ -f "$WARNED" ] && exit 0 # 同一 session 只擋一次,不鬼打牆
|
||
date +%s > "$WARNED"
|
||
|
||
cat >&2 <<EOF
|
||
🎫 這則留言裡藏著一件**還沒做完的事**,而它現在只是文字。
|
||
|
||
命中的詞:$HIT
|
||
|
||
【leo 2026-08-27】「**每個任務長出子票做為這張票的相依⋯⋯票沒完工可以察覺嗎?**」
|
||
|
||
【它掉過一次,時序如下】
|
||
08-26 13:00 arcrun-rag#136 comment 4267:「等雲端那半出貨才驗得了」 ← 只是一段文字
|
||
08-26 18-23 總管真的出貨了 ← 條件滿足,沒人知道
|
||
08-27 11:xx leo 問「這 14 小時前的,你派了還是沒派?」 ← 才被撿起來
|
||
|
||
【怎麼做】把它長成子票,Gitea 原生相依會**硬擋**母票關閉(實測 412):
|
||
|
||
scripts/ticket subtask <母票 owner/repo#N> \\
|
||
--title "身為<誰>,我要<什麼>,我才<為什麼>" \\
|
||
-F <內文檔> --label s/todo --assign <誰做>
|
||
|
||
⇒ 子票沒關,母票關不掉。**不是提醒,是平台保證。**
|
||
|
||
【粒度】leo 2026-08-27:「**小任務做完就關閉,不會讓池子爆掉,
|
||
每個事情沒有歷史記錄才是大問題**」⇒ 判準是「這件事需要被記得嗎」,
|
||
傾向**多開票**。不要用「會不會太多」當理由不開。
|
||
|
||
【真的不是待辦】在指令裡加 \`no-subtask: <一句理由>\` 重送(留痕)。
|
||
EOF
|
||
exit 2
|