#!/usr/bin/env bash # self-drive-judge.sh — 自走警察【第二級:語意判官】(藍圖 v3 §5.5 落地) # # 為什麼要它(病根): # self-drive-police.sh 用 grep 抓請示句,**grep 只認字面**。 # leo 2026-08-04 抓到的病是「停下來問早已決議的事」,但那個病**換句話說就閃過去了**—— # 「要我 X 嗎」擋得下,「不知道這樣是否符合你的期待」「兩條路你偏好哪個」擋不下。 # ⇒ 字面判擋不住換句話說,語意判才治本(藍圖 v3 §5.5「升級 prompt 型 hook」)。 # # 為什麼不用官方 prompt 型 hook(實查,2026-08-05): # 官方 `type: prompt` 的 $ARGUMENTS = **hook 輸入 JSON**(session_id/transcript_path/…), # Stop 事件的輸入**不含我實際說了什麼**,而 prompt hook 單輪無工具、不能自己去讀 transcript。 # ⇒ 直接換成 prompt hook 會變成「拿不到內容的判官」。 # `type: agent`(可帶 Read/Grep 查證)做得到,但官方標 experimental ⇒ 依藍圖 §9.4 成熟度門檻只登記觀察。 # ⇒ 本 hook=command hook 抽文字 + 外呼 haiku 判語意。判準與成本都在我們自己手上。 # # 判準(只有一條,就是四題公式): # 這個問題是否真的命中人閘——① 花錢 ② 不可逆/難回收 ③ 跨專案結構 ④ 品味/方向? # 命中任一 → 該問,放行。四題全否 → 擋回去,逼它自己裁。 # # 成本控制(三道前置閘,正常回合完全不花錢): # ① 沒有問號 → 不判(絕大多數回合走這條,零成本) # ② 已被 self-drive-police 的 grep 擋下的句型 → 不重判(不重複收費) # ③ 明說撞權限閘/四題第 X 命中 → 豁免(與 police 同一套豁免語) # # 失敗一律 fail-open(exit 0):判官掛掉不能變成「永遠停不下來」。 set -u # 遞迴保險:判官自己起的那個 claude 不准再跑判官 if [ "${SELF_DRIVE_JUDGE:-}" = "1" ]; then exit 0; fi input="$(cat)" # stop_hook_active=已經被擋過一輪,別再疊 stop_active="$(printf '%s' "$input" | python3 -c ' import sys, json try: print(json.load(sys.stdin).get("stop_hook_active", False)) except Exception: print(False) ' 2>/dev/null)" [ "$stop_active" = "True" ] && exit 0 transcript="$(printf '%s' "$input" | python3 -c ' import sys, json try: print(json.load(sys.stdin).get("transcript_path", "")) except Exception: print("") ' 2>/dev/null)" [ -z "$transcript" ] || [ ! -f "$transcript" ] && exit 0 # 抽「我這回合最後的 assistant 文字」(與 self-drive-police.sh 同一套抽法) last_text="$(python3 -c ' import sys, json path = sys.argv[1] texts = [] try: with open(path) as f: lines = f.readlines() for line in reversed(lines): try: ev = json.loads(line) except Exception: continue msg = ev.get("message", ev) if msg.get("role") == "assistant": content = msg.get("content", "") if isinstance(content, list): for b in content: if isinstance(b, dict) and b.get("type") == "text": texts.append(b.get("text", "")) elif isinstance(content, str): texts.append(content) break except Exception: pass print("\n".join(texts)) ' "$transcript" 2>/dev/null)" [ -z "$last_text" ] && exit 0 # ── 前置閘①:沒問號就不是在問人 ── printf '%s' "$last_text" | grep -qE '?|\?' || exit 0 # ── 前置閘②:已被 grep 警察涵蓋的句型,交給它,不重複判 ── if printf '%s' "$last_text" | grep -qiE '要我(現在|先|繼續|開始)?[^??]{0,12}嗎|需要我[^??]{0,12}嗎|下一步(要|該)?做什麼|接下來(要)?做什麼|要不要|該不該'; then exit 0 fi # ── 前置閘③:撞真閘的豁免語(與 self-drive-police.sh 同一套)── if printf '%s' "$last_text" | grep -qiE '權限閘|classifier|分類器擋|denied by|四題第|需要你放行|需要 leo 放行'; then exit 0 fi # 只送最後 1200 字給判官(夠判語氣、不吃 context) snippet="$(printf '%s' "$last_text" | tail -c 3600)" judge_prompt="你是「自走警察」的語意判官。下面是一個 AI 助理(總管)準備結束回合時對老闆 leo 說的話。 判斷它是否在**把一個自己該裁的決定推回給 leo**。 判準只有一條——四題人閘公式。它問的事情是否命中下列任一: ① 花錢(付費、開資源、產生帳單、明顯多耗訂閱額度) ② 不可逆/難回收(刪資料、push 到 main、部署上線、對外公開、跨 repo 搬遷) ③ 跨專案結構決策(改全體共用框架的架構、改 repo 歸屬、立新鐵律) ④ 品味/方向(產品要不要某功能、UX 取捨——leo 的主觀偏好) ⑤ 物理人閘(只有人能做:貼憑證、終端機同意、平台上按批准) 命中任一 → decision=allow(它該問)。 四題全否,而它仍在徵詢、確認、討好、或丟空白選擇題 → decision=block。 注意: - 單純**回報**「我做了 X,發現 Y」即使句尾有問號式修辭,也 allow。 - 提供選項但**已明說自己的建議與理由**、只等一個詞確認 → 若命中四題則 allow;四題全否仍是 block。 - 只輸出 JSON,不要任何其他文字。 輸出格式: {\"decision\":\"allow\"或\"block\",\"reason\":\"一句正體中文,若 block 要指出它在問什麼、以及四題為何全否\"} --- 助理說的話 --- $snippet --- 結束 ---" # ⚠️ 判官必須跑在「中性目錄 + --safe-mode」: # 實測(2026-08-05):直接在專案內跑 `claude -p` → **54 秒**,因為內層把整個專案的 # SessionStart recall(數十萬字)、18 支 hook、40 個 arcrun MCP 工具全載進去, # 還會拿專案脈絡去回答判官的問題(實測它跑去讀 self-drive-judge.sh)。 # 換成 `cd /tmp` + `--safe-mode`(關閉全部客製化)→ **8 秒**。 # 判官只需要判一段文字,不需要專案脈絡;載進來只會又慢又汙染判斷。 verdict="$(cd /tmp && SELF_DRIVE_JUDGE=1 printf '%s' "$judge_prompt" \ | (cd /tmp && SELF_DRIVE_JUDGE=1 claude -p --safe-mode --model haiku --allowedTools "" 2>/dev/null))" # 判官掛了/回傳空/不是 JSON → fail-open [ -z "$verdict" ] && exit 0 decision="$(printf '%s' "$verdict" | python3 -c ' import sys, json, re raw = sys.stdin.read() m = re.search(r"\{.*\}", raw, re.S) if not m: print("allow|"); raise SystemExit try: d = json.loads(m.group(0)) print((d.get("decision") or "allow") + "|" + (d.get("reason") or "")) except Exception: print("allow|") ' 2>/dev/null)" case "$decision" in block\|*) reason="${decision#block|}" cat >&2 < 記錄 X,我判斷走 A 因為 Y,回『好』我就繼續」 ——裸問句、空白選擇題=視同沒查。 EOF exit 2 ;; esac exit 0