#!/bin/sh # no-ticket-no-dispatch.sh — PreToolUse(Agent|Task):**無票不發包。** # # 🔴 為什麼是硬擋而不是提醒(leo 2026-08-16): # 「**提醒可以忽略,deny 不能**——被 deny 的動作沒有發生,不存在「忽略」這個選項。 # 所以問題化簡為:找出『起任務』在物理上必經的瓶頸,把閘設在那裡。」 # 「因為我發現**你不會去更新票**,所以定義 tags 完全是裝飾⋯⋯ # 唯一辦法是,你不准寫 code,要 subagent 寫**一定要有票號**,它自己去拿任務, # 回覆在票裡⋯⋯這樣你就會去更新狀態,否則永遠會『我忘了』, # **我就無法知道你做到哪裡**。」 # # 瓶頸就是這個工具呼叫:總管要讓 subagent 動起來,唯一的機制是它,繞不過去。 # ⇒ 「先開票」不是 SOP 第一條,是**發包動作的前置條件**, # 跟「不插鑰匙車不會發動」同一性質。 # # 🔴 同日實錯(本閘的來由):總管說了三次「下一步要查 X」都沒有派人; # 真的派人時又**沒搜就開新票**(arcrun-rag#110),而那條線早有 hub(InkStoneCo#44)。 # ⇒ 兩個病:說了不派、派了亂開票。前者這道閘擋不到,後者由 `scripts/ticket` 擋。 # # 認什麼:派工單裡要有一行 `【工單】owner/repo#N`,可再帶 comment 定址 # (`#issuecomment-2749` 或 `→ comment 2749`)。 # leo:「你可以更細的寫票號+對話的號,因為它不一定拿整張票, # 可能你把指令寫在某個對話裡。」 # # 刻意的取捨(fail-closed vs fail-open 的界線畫在哪): # · **票號缺席 → 硬擋。** 這是純本地檢查,永遠可靠,沒有藉口。 # · **票號在、但 Gitea 連不上 → 放行並警告。** 網路抖動不該讓工作停擺; # 而且「他寫了票號」這件事本身已經是可追溯的痕跡。 # · **票號在、Gitea 說那張票不存在或已關 → 硬擋。** 那是指向空氣。 set -eu PAYLOAD=$(cat 2>/dev/null || echo '{}') REF=$(printf '%s' "$PAYLOAD" | python3 -c ' import sys, json, re try: d = json.load(sys.stdin) except Exception: sys.exit(0) 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) if [ -z "$REF" ]; then cat >&2 <<'EOF' 🚫 無票不發包(leo 2026-08-16 立) leo 原話:「**你不會去更新票,所以定義 tags 完全是裝飾**⋯⋯ 要 subagent 寫一定要有票號,它自己去拿任務,回覆在票裡, 這樣你就會去更新狀態,否則永遠會『我忘了』,**我就無法知道你做到哪裡**。」 派工單裡缺一行工單號。加上這一行(放最前面): 【工單】inkstone/# 【工單】inkstone/InkStoneCo#44 → comment 2749 ← 指令寫在某則對話裡時這樣寫 ── 還沒有票?照這個順序,不要直接開新的 ──────────────────── 1. 先搜「這件事該放哪」 scripts/ticket where <關鍵字...> 2. 命中同一條線 scripts/ticket say -F <內文檔> ← 預設 3. 真的是新的一條線 scripts/ticket new -F <內文檔> --title <標題> 🔴 **預設是貼進既有的票,不是開新的。** leo:「不要每個開新票,現有的票開在它下面的對話裡」 「我希望你把 gitea 變成**可以追蹤**,不是變成一個池子」 為什麼連「只是查個東西」也要票:研究任務沒有票就是逃逸艙口。 票上宣告 `deliverable 類型: research` 即可,交付物是貼在票上的結論,不是 PR。 EOF exit 2 fi set -- $REF OWNER="$1"; REPO="$2"; NUM="$3" # 拿 token(拿不到就放行——這是本地環境問題,不是派工者的錯) URL=$(git -C "${CLAUDE_PROJECT_DIR:-.}" remote get-url gitea 2>/dev/null || true) case "${URL:-}" in *@*) ;; *) exit 0 ;; esac TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') HOST=$(printf '%s' "$URL" | sed -E 's|.*@([^/]+)/.*|\1|') RESP=$(curl -s --max-time 15 -H "Authorization: token $TOKEN" \ "https://$HOST/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null || true) [ -n "$RESP" ] || exit 0 # 連不上 → 放行(票號在,已有痕跡) STATE=$(printf '%s' "$RESP" | python3 -c ' import sys, json try: d = json.load(sys.stdin) except Exception: print("unknown"); sys.exit(0) print(d.get("state") or ("missing" if d.get("message") else "unknown")) ' 2>/dev/null || echo unknown) case "$STATE" in open|unknown) exit 0 ;; closed) cat >&2 < · 這確實是新的一條線 → scripts/ticket new -F <檔> --title <標題> EOF exit 2 ;; missing) cat >&2 < 找出真正該掛的那張。 EOF exit 2 ;; esac exit 0