Files
ISEP/hooks/self-drive-police.sh
T
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:41:46 +08:00

166 lines
9.5 KiB
Bash
Executable File
Raw 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
# 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. **wikitasks.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:promptHaiku 判語意),settings.json 寫 {"type":"prompt","prompt":"判斷這回合是否在推卸/未查證就停…"}。
# 擋「換句話說的推卸」,但每次停叫一次 Haiku(便宜)。
# L3:改 type:agentspawn subagent 真查 wiki/SDD/派工表客觀驗「規劃是否做完」(像 Ralph 的 passes)。最強最貴。