#!/usr/bin/env bash # self-drive-police.sh — 自走警察(Stop hook) # # 病根(leo 2026-06-29 一個 session 點破三次):我「做一條停一條」「把該自己做的推給 leo」 # ——不是缺 loop,是每個決策點退回「推給 leo」的省事預設。CLAUDE.md 寫規則沒用(我會不讀)。 # leo 的解法:在我「要停下來」那一刻攔截 + 反問,逼我先窮盡查證才准停。 # 靈感=leo 之前做的「Haiku 警察 Anna」(檢查回話、犯規就抓)。先用輕的(純 bash grep),不夠再升級。 # # 機制(官方 Stop hook,核實 code.claude.com/docs/en/hooks): # Claude 結束回合想停 → 本 hook 觸發 → 讀 transcript 我這回合輸出 → grep 三類推卸句式: # ① 憑記憶問「要不要/該選/哪個」 → 反問「查 wiki/internet 官方了嗎」 # ② 問「下一步/接下來做什麼」 → 反問「查 SDD/派工表/北極星了嗎」 # ③ 推卸「交給你/這是你的/你去開」 → 反問「規劃做完了嗎、能 spawn subagent 嗎、為何停」 # 命中 → exit 2 + stderr(reason) → Claude 不停、收到反問繼續做。 # 沒命中 → exit 0 放行(真的該停,不擾)。 # # 防無限迴圈(官方):stop_hook_active=true(已 block 過一輪)→ 放行。 # + Claude Code 內建 block cap(連續 8 次自動放行)。 # # 誠實限制:靠句式 grep,擋得住「明顯推卸句」,擋不住「換句話說的推卸」。是底線不是萬能。 # 不夠時升級成 type:prompt(Haiku 判語意) 或 type:agent(查 wiki/SDD 客觀驗),見 hook 末註。 input="$(cat)" # 防無限迴圈:已經 block 過一輪(Claude 正在回應上一次 block)→ 放行 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)" if [ "$stop_active" = "True" ]; then exit 0 fi # 取 transcript 路徑 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 文字輸出」(transcript 是 JSONL,每行一事件) # 取最後一個 assistant 訊息的 text,避免掃到歷史 last_text="$(python3 -c ' import sys, json path = sys.argv[1] texts = [] try: with open(path) as f: lines = f.readlines() # 從尾往前找最後一個 assistant 訊息的 text 內容 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 # ── 豁免:撞真閘就別逼它做不可能的事(2026-07-22 補)── # 病例:雲端總管有 token、備好 patch,但 harness classifier 擋掉 mutating 寫入 → # 舊版警察會抓「Which way do you want it?」逼它繼續,它撞牆到 block cap 才放行=空轉。 # 明說「四題第X命中」也放行(本 hook 註解原本就要求這格式,但沒實作豁免)。 if printf '%s' "$last_text" | grep -qiE '權限閘|classifier|分類器擋|denied by|四題第|需要你放行|需要 leo 放行'; then exit 0 fi # ── 三類推卸句式偵測 ────────────────────────────────────────────── # ③ 推卸給 leo(最該抓)——「交給你/這是你的/你去/請你去開/leo 去做」 if printf '%s' "$last_text" | grep -qiE '交給(你|leo)|這是你的|是你的工作|你(去|來)(開|做|跑|查|執行)|請你去|leo (去|來)(開|做)|由你(去|來)|該你'; then cat >&2 << 'EOF' 🚓 自走警察:偵測到「推卸給 leo」。停下前先答這三題: 1. 所有規劃工作做完了嗎?(查 autonomy-dispatch/tasks.md 派工表 + 北極星) 2. 這件事我能不能 spawn 一個 subagent 去做?(子 repo 寫 code 走 guard 白名單 CHILD_SESSION;偵察/文書/診斷都能 spawn) 3. 為什麼要停下來?——只有四題命中(花錢/不可逆/品味方向/物理人閘如 terminal consent、merge 批准)才輪到 leo。 「做 vs 問」是假二選一,真三選一:自己做 / spawn subagent 做 / 才輪到 leo。叫 leo 當人肉中繼=親手裝回剛拔掉的瓶頸。 若確認真四題命中才需 leo,下一回合明說「四題第X命中:<理由>」再停。 EOF exit 2 fi # ② 問「下一步做什麼」——答案在規劃文件裡 if printf '%s' "$last_text" | grep -qiE '下一步(要|該)?做什麼|接下來(要)?做什麼|接下來呢|要做什麼好|現在(要|該)做什麼'; then cat >&2 << 'EOF' 🚓 自走警察:偵測到「問下一步做什麼」。答案多半在規劃文件裡,先查再說: 1. 查 SDD 了嗎?(system-dev/docs/3-specs/*/tasks.md — 唯一進度來源) 2. 查派工表了嗎?(system-dev/docs/3-specs/autonomy-dispatch/tasks.md — issue 開哪、誰做、狀態) 3. 查北極星了嗎?(system-dev/docs/6-user/20260627-northstar.md §8.4 順序) 查完下一條自然浮現,自己往前推,別把判斷推給 leo。若真查過且確認無下一條/撞人類閘,下一回合明說再停。 EOF exit 2 fi # ⓪ 🔴 請示句:「要我 X 嗎/我可以開始嗎/需要我 X 嗎」——**已交代過的事不必再請示** # 2026-08-04 leo 兩度點破:「不要停下來問要不要繼續,**這是前面明說要做的事**」 # 「都列表了你還要問這個問題?」「**就是個自問自答功能,你問的問題明明有答案**」 # ⇒ 舊 ① 只認「要不要/該選哪個」,**漏掉最常見的請示句型「要我…嗎」**。 # 判準不是句型漂亮與否,而是:**這個問題的答案,是不是已經存在於既有資料裡?** if printf '%s' "$last_text" | grep -qiE '要我(現在|先|繼續|開始)?[^??]{0,12}嗎|需要我[^??]{0,12}嗎|我(現在|可以|該)(開始|繼續|動手)[^??]{0,8}嗎|(繼續|開工|動手)嗎'; then cat >&2 << 'EOF' 🚓 自走警察(請示句):你在問「要我 X 嗎」——**先自問自答:這個問題有答案嗎?** 【leo 2026-08-04】「不要停下來問要不要繼續,**這是前面明說要做的事**」 「都列表了你還要問這個問題?」 「**就是個自問自答功能,你問的問題明明有答案,應該就可以找到答案了**」 ━━━ 送出前先在這三處找答案(找到就別問,直接做)━━━ A. **leo 這個 session 說過的話**——他是不是已經給了優先序/清單/「開工」? (已交代過的事再請示=把他當按鈕按,正是自走規則要拔掉的瓶頸) B. **你自己剛列的 todo/任務表**——下一項就是答案,照著做 C. **wiki/tasks.md**——`system-dev/wiki/status.md`、對應 tasks.md 有沒有定案 ━━━ 只有這種情況才可以問(且要明說已查過)━━━ 真命中四題人閘:花錢/不可逆(push main、刪資料、部署上線)/ 跨專案結構決策/品味方向。**其餘一律自己裁、往前推,錯了被擋回再修。** 📌 想收尾時不要問「要我繼續嗎」,直接**做下一項並回報做了什麼**。 EOF exit 2 fi # ① 憑記憶問「要不要/該選哪個」——先查證再決定 if printf '%s' "$last_text" | grep -qiE '要不要|該不該|該選(哪|什麼)|哪個(比較)?好|選哪(個|條)|是不是要|可不可以'; then # 但若這回合已有查證跡象(提到查了 wiki/官方/grep/WebFetch/出處),放行——別擾正常提問 if printf '%s' "$last_text" | grep -qiE '查(了|過)|官方(確認|核實|出處)|核實|grep|WebFetch|出處|wiki 寫|記憶(卡|寫)'; then exit 0 fi cat >&2 << 'EOF' 🚓 自走警察:偵測到「憑判斷問要不要/該選哪個」,但沒看到查證跡象。停下問 leo 前先窮盡: 1. 查 wiki/記憶了嗎?(system-dev/wiki/ + MEMORY.md — 這事可能已有定案,別重議) 2. 查 internet 官方說明了嗎?(LLM/平台/API 機制憑記憶會錯,查官方原文 + 給出處) 查完答案多半自己浮現,四題全否就自己裁、不問 leo。若查過仍需 leo 的品味/方向判斷(第④題),下一回合明說「查過,這是品味題」再問。 EOF exit 2 fi # 沒命中任何推卸句式 → 真的該停,放行 exit 0 # ── 升級路徑(不夠用時,leo 點頭)────────────────────────────────── # 輕→重三級: # L1(現在):command bash grep 句式三分流。最省,擋明顯推卸句。 # L2:改 type:prompt(Haiku 判語意),settings.json 寫 {"type":"prompt","prompt":"判斷這回合是否在推卸/未查證就停…"}。 # 擋「換句話說的推卸」,但每次停叫一次 Haiku(便宜)。 # L3:改 type:agent,spawn subagent 真查 wiki/SDD/派工表客觀驗「規劃是否做完」(像 Ralph 的 passes)。最強最貴。