Files
ISEP/hooks/comment-carries-task-guard.sh
Leo b46bc5e44c dogfood 抓到自己的漏:交棒標記只認第一行,不認「內文提過」
實跑自己那則交件報告時,閘沒有擋——因為報告裡**引用了一段 ticket mine 的輸出**
(那段有 🏃),整則就被豁免掉了,而它裡面真的有「等雲端那半出貨」。
⇒ 同 inkstone/InkStoneCo#23 那個病:複述關鍵字被當成本人。
ticket handback 寫的交棒留言 🏃 一定在第一行,引用別人的輸出則不會 ⇒ 錨在第一行。

回歸測試 +2(20/20 → 22/22)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:13:27 +08:00

133 lines
7.9 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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-27Gitea 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(收工那一刻)補第二層。
#
# 已知極限(誠實標記):這支**認不出「我已經開好子票了」**——它只看得到指令文字。
# 所以交件報告裡引用自己剛開的子票時仍會被擋一次;那一次用 `no-subtask:` 過,
# 或就當它是一次確認。刻意不去打 API 查「這張票有沒有相依」:那會讓每一個
# 命中關鍵詞的 Bash 呼叫都多一次網路往返,代價落在錯的地方。
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 newticket-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 那條路,不重複擋。
# 🔴 只認**第一行**,不認「內文任何地方出現過」——這是 dogfood 當場抓到的漏(2026-08-27):
# 交件報告裡**引用了一段 `ticket mine` 的輸出**(那段有 🏃),
# 整則報告就被豁免掉了,而它裡面真的有「等雲端那半出貨」這種待辦。
# ⇒ 這正是 `inkstone/InkStoneCo#23` 那個病的同款:**複述關鍵字被當成本人**。
# `ticket handback` 寫的交棒留言,🏃 一定在第一行;引用別人的輸出則不會。
case "$(printf '%s' "$BODY" | head -1)" in "🏃"*) exit 0 ;; esac
# ③ 「未完成的未來式」——刻意取窄。這一組全部來自 08-26 那則真的掉了的留言的形狀。
#
# 🔴 比對用 python3 不用 grep——這是**實測逼出來的**,不是偏好(2026-08-27 本閘第一版):
# 第一版寫 `等[^。]{0,20}(出貨|…)`,跑真的 hook 呼叫路徑時對
# 「這件事等雲端那半出貨才驗得了」**不匹配**,閘靜默失效(測試 19/20,那一條紅)。
# 同一條 pattern 在別的 shell 手打卻會過 ⇒ **它的行為依賴 localegrep 實作**
# 而 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