c2638668e3
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>
177 lines
7.7 KiB
Bash
Executable File
177 lines
7.7 KiB
Bash
Executable File
#!/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 <<EOF
|
||
🚓 自走警察【語意判官】:你在徵詢 leo,但這題**四題全否**——那就是你自己該裁的。
|
||
|
||
判官的話:$reason
|
||
|
||
【leo 2026-08-04】「不要停下來問要不要繼續,**這是前面明說要做的事**」
|
||
【藍圖 v3 §5.5】問題跟記憶走同一種階梯:先窮盡已有答案,答不出才升一級。
|
||
到 leo 是**最後一級**,不是預設一級。
|
||
|
||
送出前先在這四處找答案(找到就別問,直接做):
|
||
A. leo 這個 session 說過的話——他是不是已經給了優先序/清單/「開工」?
|
||
B. root.md/journeys.md/派工表——下一站就是答案
|
||
C. decisions / ADR / pending-changes 已裁決區——這個做法定案過嗎?
|
||
D. KBDB 語意搜尋——換句話說的同一題(grep 猜不中用詞時的保險)
|
||
|
||
📌 真要升級到 leo,格式是:「<檔案:行> 記錄 X,我判斷走 A 因為 Y,回『好』我就繼續」
|
||
——裸問句、空白選擇題=視同沒查。
|
||
EOF
|
||
exit 2
|
||
;;
|
||
esac
|
||
|
||
exit 0
|