#!/usr/bin/env bash # wiki-first-police.sh — 記憶警察(Stop / SubagentStop hook) # # 病根(leo 2026-07-26 兩句原話): # ①「你應該隨時記錄進 wiki,然後用記憶回答,而不是每次都去實測,除非 wiki 沒做好記錄找不到」 # ②「你要把不憑記憶回答改成檢查 wiki……不依賴你自己的記憶,那個過 session 就死了」 # ③「我看 CP 也沒用,因為你執行時沒去更新」 # # 兩個病,同一根:**做完不落帳** → 下次(或下個 session)只好重新實測/憑會死的記憶亂答。 # - 病 A:驗完就往下做,CP/wiki 事後才補 → leo 看 CP 永遠落後現況=「看了也沒用」。 # - 病 B:被問狀態時憑對話記憶或重跑實測,而不是查 wiki → 過 session 就失憶。 # # 為什麼要 hook 而不是寫規則:CLAUDE.md 早就寫了「tasks.md 是唯一進度來源,不靠對話記憶」, # 照樣違反——自評的人跟做事的人是同一個。同 delivery-police:在「要收工」那刻攔截。 # # 機制:這回合有「實測/修好/部署」這類狀態變更 → 檢查有沒有動過 wiki/CP/tasks → 沒動就 exit 2。 # 豁免:純查詢/純對話回合、已動過帳、撞權限閘。 input="$(cat)" 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 # 取本回合(最後一次 user 訊息之後)的所有 assistant 文字 + 工具輸入 turn="$(python3 -c ' import sys, json path = sys.argv[1] out = [] try: with open(path) as f: lines = f.readlines() # 從尾巴往回走到最近一則 user 訊息為止 buf = [] for line in reversed(lines): try: ev = json.loads(line) except Exception: continue msg = ev.get("message", ev) role = msg.get("role") if role == "user": break if role == "assistant": content = msg.get("content", "") if isinstance(content, list): for b in content: if not isinstance(b, dict): continue if b.get("type") == "text": buf.append(b.get("text", "")) elif b.get("type") == "tool_use": buf.append(json.dumps(b.get("input", {}), ensure_ascii=False)) elif isinstance(content, str): buf.append(content) out = list(reversed(buf)) except Exception: pass print("\n".join(out)) ' "$transcript" 2>/dev/null)" [ -z "$turn" ] && exit 0 # ── 豁免一:撞閘/明說停等 → 放行 ── if printf '%s' "$turn" | grep -qiE '權限閘|classifier|分類器擋|四題第|人閘|denied by'; then exit 0 fi # ── 這回合有沒有「狀態變更」?(值得落帳的事)── # 部署、修好、實測通過、端到端驗證、立案——這些都是下次會被問到的狀態。 if ! printf '%s' "$turn" | grep -qiE '部署|deploy|已上線|修好|修掉|真兇|實測|端到端|驗證通過|全綠|通了|立案'; then exit 0 fi # ── 豁免〇(2026-08-08 新增):**這回合根本沒有動過任何 repo → 純討論,不必落帳** ── # # 🔴 為什麼補這道(leo 08-08 問「為什麼花了一整天」時查出來的): # 上面那個「有沒有狀態變更」的判斷,比對的是**我回覆裡的散文**。 # 於是我只要在回答中**談到**「修好/實測/全綠/部署」——例如向 leo 解釋 # 今天做了什麼、或提出改善流程的建議——就會被判成「有狀態變更卻沒落帳」。 # ⇒ **把「談論」當成「執行」**。這是同一天第三個同款 bug # (前兩個在我自己寫的 stage-before-prod-guard:讀腳本、commit 訊息提到關鍵字都被誤攔)。 # ⇒ 代價很實在:leo 每插話一次,我就被判一次「沒落帳」,於是又寫一段落帳、 # commit、push。今天 65 個 commit 裡 41 個是落帳,這是其中一個來源。 # # 判準改成看**事實**:這 30 分鐘內有沒有任何 repo 真的產生 commit。 # 沒有 ⇒ 這回合是討論/回答,沒有東西需要被記住,放行。 ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" recent_commits=0 for r in "$ROOT" "$ROOT/products/arcrun-rag" "$ROOT/matrix/arcrun"; do [ -d "$r/.git" ] || continue n=$(git -C "$r" log --since="30 minutes ago" --oneline 2>/dev/null | wc -l | tr -d ' ') recent_commits=$((recent_commits + n)) done [ "$recent_commits" -eq 0 ] && exit 0 # ── 豁免二:這回合確實動過帳(wiki / CRITICAL-PATH / tasks.md / memory)── # # 🔴 同上,這裡原本也是比對散文——**只要回覆裡「提到」wiki 路徑就算落帳**, # 等於「說了就算做了」。改成先看 git:這 30 分鐘的 commit 有沒有真的動到帳本檔。 doc_commits=0 for r in "$ROOT" "$ROOT/products/arcrun-rag" "$ROOT/matrix/arcrun"; do [ -d "$r/.git" ] || continue n=$(git -C "$r" log --since="30 minutes ago" --name-only --pretty=format: 2>/dev/null \ | grep -cE 'system-dev/wiki/|CRITICAL-PATH\.md|critical-paths/|tasks\.md|memory/.*\.md' || true) doc_commits=$((doc_commits + n)) done if [ "$doc_commits" -gt 0 ]; then # 🔗 落帳了,但有沒有標「這件事牽涉哪些 repo」? # leo 2026-08-01:「你需要在每次記錄你自己的 wiki 時建立連到發生此事件的 hyperlink 或三元組, # 每件事牽涉到 2 個 repo,就可以在該事件查到那兩個 repo,細節記錄在該 repo 裡。」 # 病根:08-01 實測 status.md 今日 34 段落帳中,只有 2 段標題提到 repo、 # 0 筆有結構化 repo 欄位、僅 2 行三元組 ⇒ 下次仍要重查「這件事在哪」。 # 只在「跨 repo 事件」時要求(單 repo 的事不必標)。 if printf '%s' "$turn" | grep -qiE 'arcrun-rag|matrix/arcrun|console-ui|installer|portal|bundle|kbdb|cypher|跨 ?repo'; then # 🔴 2026-08-08 第二刀:這一段原本也在比對**我的回覆散文**—— # 於是 wiki 條目裡明明寫了 📍 repo 與三元組,只因為**回覆裡沒提**就被判「沒標 repo」。 # (早上已把上層觸發條件改看 git,這裡是同一個病漏掉的另一半。) # ⇒ 改成看**剛剛真的寫進帳本的內容**:近 30 分鐘 commit 新增的行有沒有 repo 指針。 doc_added="" for r in "$ROOT" "$ROOT/products/arcrun-rag" "$ROOT/matrix/arcrun"; do [ -d "$r/.git" ] || continue doc_added="$doc_added$(git -C "$r" log --since='30 minutes ago' -p --unified=0 \ -- 'system-dev/wiki/*' 'system-dev/docs/3-specs/*' 2>/dev/null \ | grep '^+' || true)" done # ⚠️ 樣式別寫死 '📍 repo'——實際格式是 `📍 **repo**:`(粗體), # 寫死會永遠對不上(08-08 實撞:條目明明有指針卻一直被判沒有)。 if ! printf '%s' "$doc_added$turn" | grep -qE '牽涉 repo|📍|真身在|真兇|位於|\[\[.*\]\]'; then cat >&2 << 'EOF2' 🔗 記憶警察(連結層):你落帳了,但**沒標這件事牽涉哪些 repo**。 【leo 2026-08-01】「你需要在每次記錄你自己的 wiki 時建立連到發生此事件的 hyperlink 或三元組, **每件事牽涉到 2 個 repo,就可以在該事件查到那兩個 repo**,細節記錄在該 repo 裡。」 【為什麼】08-01 實測:status.md 當日 34 段落帳,只有 2 段標題提到 repo、 **0 筆有結構化 repo 欄位** ⇒ 下次查「這件事在哪」還是要從頭找, 這就是同一天猜錯真身 3 次的根源。 補一行就好(擇一格式,放在該段落帳裡): 📍 repo:`products/arcrun-rag`(landing/worker.js)+`matrix/arcrun`(console-ui/public/portal/) 或三元組:`portal 視覺 >> 真身在 >> matrix/arcrun:console-ui/public/portal/index.html` 原則:**頂層 wiki 記「事件+牽涉哪些 repo」,細節記在該 repo 自己的 wiki**。 EOF2 exit 2 fi fi exit 0 fi cat >&2 << 'EOF' 🚓 記憶警察:這回合有狀態變更(實測/修復/部署),但我沒看到你落帳。 【病根】leo 2026-07-26: 「我看 CP 也沒用,因為你執行時沒去更新。」 「不依賴你自己的記憶——那個過 session 就死了。」 → 你現在腦裡有的東西,下個 session 一個字都不剩。沒寫進檔案=沒發生過。 收工前補完這兩件(擇需要的做,一句話也算): 1. **狀態變更落帳**: - CP 關卡狀態變了 → 改 `system-dev/docs/3-specs/CRITICAL-PATH.md`(✅/◐/❌+實測輸出) - 任務進度/新發現 → 改對應 `tasks.md`(含真兇、殘項、下一步) - 跨 session 要記得的操作知識 → `system-dev/wiki/`(status/mistakes/agent-memory) 2. **下次別再重測**:把「這件事現在是什麼狀態、證據在哪」寫成一句話, 讓下個 session(或被 leo 問時)**查得到就不用重跑**。 ⚠️ 落帳=寫檔案,不是寫在回覆裡。回覆會消失,檔案不會。 EOF exit 2