Files
ISEP/hooks/factory-idle-guard.sh
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

204 lines
10 KiB
Bash
Executable File
Raw Permalink 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.
#!/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**:這個回合直接呼叫 AgentTask 派出去。
沒有票 → 先 \`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,不是 Leo2026-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