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>
204 lines
10 KiB
Bash
Executable File
204 lines
10 KiB
Bash
Executable File
#!/bin/sh
|
||
# factory-idle-guard.sh — Stop hook:**工頭停工要被擋下,不是被提醒。**
|
||
#
|
||
# 🔴 2026-08-16 大改。leo 的原話就是這道閘的規格:
|
||
# 「做了一個又一個 hook,**你要閃過還是會閃過**,你應該讓原本那個可以改好。」
|
||
# 「拿 n8n 來說,所有的 agent **缺了一個 trigger**,你是按下 trigger 的人,但你不按。
|
||
# 所以警察不應該是催你按就好了,而是**會問你下一步是什麼,你回覆後就要交出
|
||
# 你已經派工的憑證**,不拿票就沒完成觸發。」
|
||
#
|
||
# ⇒ 所以本檔改的是**量測對象**與**強度**,不是再加一支新 hook。
|
||
#
|
||
# ── 舊版為什麼從來沒響過(2026-08-16 查證,兩層失效疊在一起)──────────
|
||
#
|
||
# ① `[ "$RUNNING" -gt 0 ] && exit 0`
|
||
# 有任何 agent 在跑就閉嘴 ⇒ **它量的是工廠,不是工頭**。
|
||
# 2026-08-16 一天四次停工**全部發生在有 agent 在跑的時候**:
|
||
# 產線忙著做 A,總管宣告了 B,然後沒有人做 B。工廠沒停,停的是按 trigger 的人。
|
||
#
|
||
# ② 池子查的是 `repos/Leo/$repo`——**org 在 2026-08-13 搬到 `inkstone`**。
|
||
# 實測:`Leo/arcrun-rag` 回 `not found`(被當成 0),`inkstone/arcrun-rag` 有 16 張。
|
||
# ⇒ 池子恆為 0 ⇒ 第 53 行直接 exit 0。**這道閘等於不存在。**
|
||
# (同族第四顆搬遷遺留座標:`ARCRUN_SHIP_NS`/`ARCRUN_REPO`/MEMORY.md 索引/本檔。)
|
||
#
|
||
# ③ 結尾 `exit 0`——純提醒。leo 2026-08-16:「**提醒可以忽略,deny 不能**。」
|
||
#
|
||
# ── 現在量什麼 ─────────────────────────────────────────────────
|
||
#
|
||
# 主閘(exit 2,會擋):**宣告了下一步,卻沒有派工憑證。**
|
||
# 憑證 = 這個回合真的呼叫過 Agent/Task(而那條路上已有 no-ticket-no-dispatch
|
||
# 在驗票號)⇒ **「有派工」本身就蘊含「有票」**,不必在這裡重複驗票。
|
||
# 也接受:宣告句裡直接寫明它在誰手上(帶 `#數字` 的票號指涉)。
|
||
#
|
||
# 副閘(exit 0,只提醒):真的全廠停工(零 agent + 池子有票)。org 已修。
|
||
#
|
||
# 🔴 **不擋純對話回合**:沒有宣告下一步就不觸發。
|
||
# 這是刻意的——永遠在響的警報等於訓練人忽略它(見 branch-holds.md 的同款教訓)。
|
||
set -eu
|
||
|
||
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||
PAYLOAD=$(cat 2>/dev/null || echo '{}')
|
||
|
||
# ── 主閘:宣告了下一步,有沒有交出派工憑證?────────────────────────
|
||
VERDICT=$(printf '%s' "$PAYLOAD" | python3 -c '
|
||
import sys, json, os, re
|
||
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
print("SKIP"); raise SystemExit
|
||
|
||
tp = d.get("transcript_path") or ""
|
||
if not tp or not os.path.exists(tp):
|
||
print("SKIP"); raise SystemExit # 讀不到就別亂擋
|
||
|
||
rows = []
|
||
try:
|
||
with open(tp) as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if line:
|
||
try: rows.append(json.loads(line))
|
||
except Exception: pass
|
||
except Exception:
|
||
print("SKIP"); raise SystemExit
|
||
|
||
# 這個回合=最後一則「真的來自使用者」的訊息之後(工具結果不算)
|
||
start = 0
|
||
for i, r in enumerate(rows):
|
||
if r.get("type") == "user":
|
||
c = (r.get("message") or {}).get("content")
|
||
blocks = c if isinstance(c, list) else [{"type": "text"}]
|
||
if not any(isinstance(b, dict) and b.get("type") == "tool_result" for b in blocks):
|
||
start = i
|
||
turn = rows[start:]
|
||
|
||
dispatched = False
|
||
blocks_text = []
|
||
for r in turn:
|
||
if r.get("type") != "assistant":
|
||
continue
|
||
for b in (r.get("message") or {}).get("content") or []:
|
||
if not isinstance(b, dict):
|
||
continue
|
||
if b.get("type") == "tool_use" and b.get("name") in ("Agent", "Task"):
|
||
dispatched = True
|
||
elif b.get("type") == "text":
|
||
blocks_text.append(b.get("text") or "")
|
||
|
||
if dispatched:
|
||
print("OK"); raise SystemExit # 按了 trigger ⇒ 放行
|
||
|
||
# 🔴 只看**最後一則**文字,不看整個回合(2026-08-16 第一次實跑就誤攔,修正)
|
||
# 病灶是「回合終止在宣告上」⇒ 該看的是那個終止動作本身。
|
||
# 掃整個回合會在「我這回合稍早說要讀 X、然後真的讀了」這種句子上開火——
|
||
# 那是**已完成事項的敘述**,不是未兌現的意圖。誤攔會訓練人忽略警報,
|
||
# 而那比沒有警報更糟(同 branch-holds.md 的教訓)。
|
||
text = blocks_text[-1] if blocks_text else ""
|
||
|
||
# 引用 leo 的話不算我的宣告(整段引言/引號內)——它常含「下一步」等字樣
|
||
text = re.sub(r"^\s*>.*$", "", text, flags=re.M)
|
||
# 🔴 2026-08-17 修:舊版把「所有」引號內容都刪掉,包括我自己句子裡的關鍵詞。
|
||
# leo 實撞:我寫「回『規劃』我就派人盤這份計畫」,`規劃` 被吃掉後變成
|
||
# 「回我就派人盤這份計畫」,DECL 一個都不匹配 ⇒ 該攔的沒攔。
|
||
# ⇒ 只刪「夠長的引言」(leo 的話通常成句),短引號是我自己的用詞,留著。
|
||
text = re.sub(r"「[^」]{12,400}」", "", text)
|
||
# 宣告下一步的句型(刻意收窄:只認「我接下來要做」,不認「現在的狀態是」)
|
||
DECL = re.compile(
|
||
r"(下一步(我|就是|是)?[::]?\s*(?!不是宣告)|接下來我|我(現在|接著|等下|等一下)(就)?(去|來|做|派|審|跑)"
|
||
r"|我(要|會)(去|來)?(做|派|審|跑|查|補|建)|稍後(我|再)|之後我(會|要)"
|
||
# 🔴 2026-08-17 leo 實撞補的一族:**把請示寫成條件句**——
|
||
# 沒問號、沒疑問詞,卻把動作的觸發權交回 leo。功能上是請示,句型上不像。
|
||
# 實例:「回『規劃』我就派人盤這份計畫」/「說一聲我就落」/「你點頭我就做」
|
||
r"|回[「『]?[^」』\n]{0,12}[」』]?(我)?就|說一聲(我)?(就)?|你(點頭|說可以|確認)(了)?(我)?(就)?"
|
||
r"|(確認|核准|同意|批准)(過|了)?(之)?後(我)?(才|再|就)"
|
||
r"|我(就)?(等|待)你|等你(說|回|點頭|確認)"
|
||
r"|我(就)?(不再|先不)(自己)?(動|做|派)"
|
||
r"|我(就)?(一次)?(落|派|做|補|審|清)(完|掉)?[。,,]?\s*$)")
|
||
hit = DECL.search(text)
|
||
if not hit:
|
||
print("OK"); raise SystemExit # 沒宣告 ⇒ 純對話回合,不擋
|
||
|
||
# 宣告句附近有票號指涉(=已經說明它在誰手上/哪張票)⇒ 放行
|
||
seg = text[max(0, hit.start() - 200): hit.end() + 400]
|
||
if re.search(r"#\d{1,5}", seg):
|
||
print("OK"); raise SystemExit
|
||
|
||
print("DECLARED_NO_TRIGGER::" + text[max(0, hit.start()-60): hit.end()+120].replace("\n", " ")[:200])
|
||
' 2>/dev/null || echo SKIP)
|
||
|
||
case "$VERDICT" in
|
||
DECLARED_NO_TRIGGER::*)
|
||
QUOTE=$(printf '%s' "$VERDICT" | sed 's/^DECLARED_NO_TRIGGER:://')
|
||
cat >&2 <<MSG
|
||
🏭 稼動率警察:**你宣告了下一步,但這個回合沒有按下 trigger。**
|
||
|
||
你寫的是:
|
||
「…${QUOTE}…」
|
||
|
||
leo 2026-08-16(本閘的規格):
|
||
「所有的 agent **缺了一個 trigger**,你是按下 trigger 的人,**但你不按**。
|
||
所以警察不應該是催你按就好了,而是**會問你下一步是什麼,
|
||
你回覆後就要交出你已經派工的憑證**,不拿票就沒完成觸發。」
|
||
|
||
━━ 為什麼這次是擋不是提醒 ━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
2026-08-16 一天內同一個病發作四次,**每一次都是這個形狀**:
|
||
回合終止在「輸出文字」⇒「下一步是 X」寫在那段文字裡
|
||
⇒ 在結構上 X 永遠落在回合結束之後。**不是忘記,是把 X 寫進了終結回合的動作裡。**
|
||
而說出意圖會消解掉做它的壓力——一份清楚的計畫**讀起來像進度**。
|
||
|
||
━━ 現在怎麼過這道閘(擇一)━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
① **現在就按 trigger**:這個回合直接呼叫 Agent/Task 派出去。
|
||
沒有票 → 先 \`scripts/ticket where <關鍵字>\` 搜該掛哪張,
|
||
再 \`ticket say <owner/repo#N> -F <檔>\`,然後帶【工單】派工。
|
||
② **它已經在別人手上**:把票號寫進那句話(例:「已派給 #44 comment 2761」)。
|
||
有票號指涉就放行——那不是「我等下做」,是「已經有人在做」。
|
||
③ **它其實不該現在做**:改寫那句話,說明它在等什麼(前置沒解除/是 leo 的閘),
|
||
不要寫成「下一步我要做 X」。
|
||
|
||
🔴 **不准只是把那句話刪掉再送一次。** 刪掉宣告=那件事從此沒人記得,
|
||
比宣告了沒做更糟——它連痕跡都不留。
|
||
MSG
|
||
exit 2 ;;
|
||
esac
|
||
|
||
# ── 副閘:真的全廠停工(零 agent + 池子有票)──────────────────────
|
||
RUNNING=0
|
||
for d in /private/tmp/claude-501/*/*/tasks; do
|
||
[ -d "$d" ] || continue
|
||
n=$(find "$d" -name '*.output' -newermt '-60 seconds' 2>/dev/null | wc -l | tr -d ' ')
|
||
RUNNING=$((RUNNING + n))
|
||
done
|
||
[ "$RUNNING" -gt 0 ] && exit 0
|
||
|
||
TOKEN=$(git -C "$PROJ" remote get-url gitea 2>/dev/null | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') || TOKEN=""
|
||
[ -z "$TOKEN" ] && exit 0
|
||
|
||
# 🔴 org 是 inkstone,不是 Leo(2026-08-13 搬遷;舊值讓本閘靜默失效到 08-16)
|
||
TODO=$(
|
||
for repo in mira arcrun-rag Arcrun InkStoneCo; do
|
||
curl -s --max-time 6 -H "Authorization: token $TOKEN" \
|
||
"https://git.uncle6.me/api/v1/repos/inkstone/$repo/issues?state=open&labels=s/todo&limit=50" 2>/dev/null \
|
||
| python3 -c 'import json,sys
|
||
try:
|
||
d=json.load(sys.stdin)
|
||
print(len(d) if isinstance(d,list) else 0)
|
||
except Exception: print(0)' 2>/dev/null
|
||
done | awk '{s+=$1} END {print s+0}'
|
||
)
|
||
[ "${TODO:-0}" -eq 0 ] && exit 0
|
||
|
||
cat >&2 <<MSG
|
||
🏭 稼動率警察:**全廠停工**——零 agent 在跑,而任務池還有 ${TODO} 張可開工的票。
|
||
|
||
leo 2026-08-10:「你的任務是**維護 loop**,你要去拿任務派任務,
|
||
**全工廠停工你要發現,這是警訊**。」
|
||
|
||
【現在該做的,不是回報,是派工】
|
||
1. 撈池子當場撈:\`scripts/ticket where <關鍵字>\`(或 labels=s/todo)
|
||
2. 照 CP 排序:\`system-dev/docs/3-specs/critical-paths/ship.md\` 是有序的六步
|
||
3. 一次派 2–4 件平行跑;同一個 repo 不要塞兩條(會搶工作區)
|
||
4. agent 死於環境錯誤(SSL/API)⇒ **那是要重派的訊號,不是完工**
|
||
MSG
|
||
exit 0
|