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>
184 lines
8.6 KiB
Bash
Executable File
184 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# subagent-claim-worksheet.sh — subagent 交回時,把它的宣稱抽成一張待驗工作單
|
||
# (SubagentStop;leo 2026-08-18 立)
|
||
#
|
||
# ── leo 的原話(本閘的規格)─────────────────────────────────────────
|
||
# 「你如何確認說的都是真的?subagent 做完事,你應該在 SubagentStop 掛動作,
|
||
# **它說可以,你去驗證;它說不行,你想辦法,不行再報告**,
|
||
# 這解決一半的未查證。你常常直接把 subagent 說的不經查證就回報,
|
||
# **它視野小,它說的你有更多資訊去檢驗**。」
|
||
#
|
||
# ── 為什麼是「工作單檔案」而不是提醒文字 ─────────────────────────────
|
||
# leo 2026-08-17 診斷過:文字層封路是打不贏的軍火競賽,要封**動作**。
|
||
# 所以這支不勸、也不偵測我的措辭——它**在磁碟上放一個必須被處理掉的東西**。
|
||
# 配套的 Stop 閘(claim-verify-police.sh)看的是「那個檔還在不在」,
|
||
# 而檔案只能被動作消滅,不能被文案消滅。
|
||
#
|
||
# ── 兩類宣稱,處理方式不同(leo 的規格就是這兩條)──────────────────
|
||
# ✅ 它說可以 → 我去驗證(我有跨 repo 視野、有 token、有瀏覽器,它沒有)
|
||
# ⛔ 它說不行 → 我想辦法(換路徑/查憑證地圖/自己撞一次),真的不行才報告 leo
|
||
#
|
||
# ⚠️ 這裡的關鍵詞比對只用來**列出候選**,不用來判定對錯——
|
||
# 列多了我自己劃掉(要寫理由),列少了不影響我另外查。
|
||
# ⇒ 它的失敗模式是「多列幾條」,不是「擋錯事」。
|
||
#
|
||
# ── 留痕(InkStoneCo#48:36 支閘只有 2 支記錄自己做了什麼)─────────
|
||
INPUT=$(cat)
|
||
DIR="$CLAUDE_PROJECT_DIR/.claude/pending-verification"
|
||
LOG="$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-claim-worksheet.log"
|
||
mkdir -p "$DIR" 2>/dev/null
|
||
|
||
OUT=$(printf '%s' "$INPUT" | DIR="$DIR" python3 -c '
|
||
import json, os, re, sys, hashlib
|
||
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
print("SKIP:bad-json"); raise SystemExit
|
||
|
||
tp = d.get("transcript_path") or ""
|
||
if not tp or not os.path.exists(tp):
|
||
print("SKIP:no-transcript"); raise SystemExit
|
||
|
||
# ── 取 subagent 的交件 ────────────────────────────────────────────
|
||
# 🔴 2026-08-18 首次實跑就抓到自己的缺陷:`SubagentStop` 給的 `transcript_path`
|
||
# 指向**主對話**,不是子代理的紀錄 ⇒ 原本「取最後一則 assistant 訊息」
|
||
# 抓到的是**總管自己寫的句子**,這支 hook 當場退化成它要避免的「文字層自咬」。
|
||
#
|
||
# 正解:子代理的交件是以 `<task-notification>…<result>…</result>` 的形式
|
||
# 進到主對話的 **user 訊息**裡。只認那個區塊,其餘一律不看。
|
||
# ⇒ 判準回到「這段話是誰說的」,而不是「這段話長什麼樣」。
|
||
last = ""
|
||
try:
|
||
blob = []
|
||
with open(tp, "r", encoding="utf-8", errors="replace") as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
ev = json.loads(line)
|
||
except Exception:
|
||
continue
|
||
msg = ev.get("message") or {}
|
||
if msg.get("role") != "user":
|
||
continue
|
||
parts = msg.get("content")
|
||
if isinstance(parts, str):
|
||
txt = parts
|
||
elif isinstance(parts, list):
|
||
txt = "".join(p.get("text", "") for p in parts
|
||
if isinstance(p, dict) and p.get("type") == "text")
|
||
else:
|
||
txt = ""
|
||
if "<task-notification>" in txt:
|
||
blob.append(txt)
|
||
if blob:
|
||
m = re.search(r"<result>(.*?)</result>", blob[-1], re.S)
|
||
last = m.group(1) if m else ""
|
||
except Exception:
|
||
print("SKIP:read-fail"); raise SystemExit
|
||
|
||
if not last.strip():
|
||
print("SKIP:no-subagent-report"); raise SystemExit
|
||
|
||
if not last.strip():
|
||
print("SKIP:empty-report"); raise SystemExit
|
||
|
||
# ── 抽候選宣稱 ──────────────────────────────────────────────────
|
||
# 成功類:它說某件事成立/做好了 ⇒ 我去驗
|
||
OK = ("驗過", "測過", "通過", "全綠", "已部署", "部署完成", "可以用", "成立",
|
||
"完成", "修好", "已推", "已併", "沒問題", "正常", "一致", "對上",
|
||
"✅", "green", "passed", "verified", "deployed")
|
||
# 阻擋類:它說做不到 ⇒ 我想辦法,不行才報告 leo
|
||
NG = ("無法", "不行", "做不到", "被擋", "擋下", "失敗", "不存在", "找不到",
|
||
"沒有權限", "權限不足", "需要 leo", "要 leo", "請總管", "交給你",
|
||
"❌", "blocked", "denied", "cannot", "failed", "not found")
|
||
|
||
def lines_of(text):
|
||
out = []
|
||
for raw in text.split("\n"):
|
||
s = raw.strip().lstrip("-*#>| ").strip()
|
||
if len(s) < 8 or len(s) > 220:
|
||
continue
|
||
out.append(s)
|
||
return out
|
||
|
||
ls = lines_of(last)
|
||
ok_hits, ng_hits = [], []
|
||
for s in ls:
|
||
low = s.lower()
|
||
if any(k in s or k.lower() in low for k in NG):
|
||
ng_hits.append(s)
|
||
elif any(k in s or k.lower() in low for k in OK):
|
||
ok_hits.append(s)
|
||
|
||
# 兩類都沒有 ⇒ 它交的是純資料/純提問,沒有事實宣稱要驗
|
||
if not ok_hits and not ng_hits:
|
||
print("SKIP:no-claims"); raise SystemExit
|
||
|
||
sid = (d.get("session_id") or "nosid")[:8]
|
||
aid = (d.get("agent_id") or d.get("subagent_id") or "")[:10]
|
||
stamp = hashlib.sha1((tp + sid + aid).encode()).hexdigest()[:8]
|
||
path = os.path.join(os.environ["DIR"], "claims-%s-%s.md" % (sid, stamp))
|
||
|
||
def block(title, items, howto, cap):
|
||
if not items:
|
||
return ""
|
||
body = "\n".join("- [ ] %s" % x for x in items[:cap])
|
||
more = ""
|
||
if len(items) > cap:
|
||
more = "\n- [ ] (還有 %d 條同類,讀原始交件)" % (len(items) - cap)
|
||
return "## %s\n\n%s\n%s%s\n\n" % (title, howto, body, more)
|
||
|
||
doc = []
|
||
doc.append("# 待驗工作單(subagent 交回的宣稱)\n")
|
||
doc.append("> leo 2026-08-18:「**它視野小,它說的你有更多資訊去檢驗。**」\n")
|
||
doc.append("> 交件全文在:`%s`\n" % tp)
|
||
doc.append("\n🔴 **這張單子只能用動作消滅,不能用文字消滅。**")
|
||
doc.append("逐條處理完之後,把本檔**刪掉或移走**;在那之前 Stop 閘會擋。\n\n")
|
||
doc.append(block(
|
||
"✅ 它說「可以」——我要自己驗一次",
|
||
ok_hits,
|
||
"判準:**我不採信,我自己跑一次**。它只看得到自己那個 repo 與自己那次呼叫;\n"
|
||
"我有跨 repo 視野、有 token、有瀏覽器。驗完把證據貼進回覆或票裡。\n",
|
||
12))
|
||
doc.append(block(
|
||
"⛔ 它說「不行」——我先想辦法,真的不行才報告 leo",
|
||
ng_hits,
|
||
"判準:**它的「不存在/被擋」往往只是「我這裡看不到」**。\n"
|
||
"先做三件:① 查憑證地圖/wiki ② 換一條路徑 ③ **自己真的撞一次並貼出拒絕原文**。\n"
|
||
"🔴 沒撞過就不准說「我被 X 擋住」——那是虛構的閘(2026-08-18 已第五次)。\n"
|
||
"真的是人閘 ⇒ 走三件機械動作:`Human` 標籤 + 指派給 Leo + **一張它自己的票**,\n"
|
||
"不是在別張票的留言裡寫一段話(leo 08-18:「不是默默塞進錯的地方」)。\n",
|
||
12))
|
||
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
f.write("".join(doc))
|
||
|
||
print("WROTE:%s:ok=%d:ng=%d" % (os.path.basename(path), len(ok_hits), len(ng_hits)))
|
||
' 2>/dev/null)
|
||
|
||
printf '%s\t%s\n' "$(date +%FT%T)" "${OUT:-SKIP:no-output}" >> "$LOG" 2>/dev/null
|
||
|
||
case "$OUT" in
|
||
WROTE:*)
|
||
F=$(printf '%s' "$OUT" | cut -d: -f2)
|
||
N=$(printf '%s' "$OUT" | sed 's/.*ok=\([0-9]*\):ng=\([0-9]*\)/\1 成功宣稱、\2 阻擋宣稱/')
|
||
cat <<EOF
|
||
🔍 交件查證:subagent 交回了 $N,已列成待驗工作單。
|
||
|
||
【leo 2026-08-18 的規格】
|
||
「它說可以,你去驗證;它說不行,你想辦法,不行再報告。
|
||
**它視野小,它說的你有更多資訊去檢驗。**」
|
||
|
||
工作單:.claude/pending-verification/$F
|
||
|
||
🔴 **不要直接把它說的轉給 leo。** 逐條處理完,把那個檔刪掉;在那之前 Stop 閘會擋一次。
|
||
· 它說「可以」→ 你自己跑一次(測試/curl/瀏覽器/git,看那條宣稱是什麼)
|
||
· 它說「不行」→ 先換路徑、查憑證地圖;**要說「被擋」就得自己撞一次並貼原文**
|
||
EOF
|
||
;;
|
||
esac
|
||
exit 0
|