Semi-Brain 系統迭代:68→94 分完整改進路徑與 Windows/WSL Hook 配置除錯¶
文檔資訊
- 分類: debugging
- 難度: advanced
- 預估閱讀時間: 15 分鐘
- 標籤:
semi-brain,claude-code-hooks,windows-wsl,stop-hook,ledger,tags-schema,ci,bash-background-execution,slug-collision,frontmatter-validation,document-fusion,session-deduplication
摘要¶
記錄 semi-brain 知識管理系統從 68 分迭代到 94 分的完整改進過程,涵蓋 ledger 去重語義修正、tags/graph/stats 資料契約統一、slug collision 防護、frontmatter 驗證、CI 接測試、依賴拆分、retry-failed 指令,以及 Windows Claude Code Stop hook 與 WSL 環境的完整除錯實錄。新增關鍵發現:Stop hook 在進行中的 session 會多次觸發,導致 14 篇重複文檔,解法是同 session_id 觸發時改為融合更新而非跳過。
關鍵學習¶
-
Windows Claude Code 執行的是 Windows 原生 shell(cmd/PowerShell),hook command 不能呼叫 wsl.exe 或 Linux binary — 會報 cannot execute binary file
-
WSL 內的 Claude Code 有獨立的 ~/.claude/settings.json,Windows 端位於 %USERPROFILE%.claude\settings.json,兩者設定完全分開
-
Stop hook 在 session 進行中每次對話結束都會觸發,不是 session 完全關閉才觸發,同一個長 session 會產生多次觸發
-
同 session 多次觸發時 hash 不同(因為對話內容在增長),ledger 去重機制擋不住 → 產生 14 篇重複文檔;解法:改為融合更新同 session_id 的已有文檔
-
bash background 執行(& 背景 / nohup)時,source 進來的函數不會被帶入 subshell,須避免依賴 sourced functions(如 ensure_spool_exists)
-
tee -a 搭配 >> 重導向會造成每行 log 重複兩次
-
retry-failed.sh 用 echo pipe 走 while 迴圈會產生 subshell,導致計數變數在外層讀不到;改用 here-string 解決
-
ledger 去重語義:failed 不應等同已處理,需允許 retry;processed 才是真正去重
-
tags.json 格式契約需統一,build-indexes / build-graph / generate-stats 三者讀取格式必須一致
-
文檔生成後需順手觸發 indexes/graph/stats 更新,且子 subprocess 的 returncode 需明確記錄(non-fatal 但要可見)
技術細節¶
分數演進路徑¶
| 輪次 | 分數 | 主要改進 |
|---|---|---|
| 初始 | 68 | 有野心的原型,舊 API pipeline 混雜,ledger 語義錯誤 |
| 第一輪 | 83 | 移除舊 API 流程、修正 ledger 語義+lock、統一資料契約、文檔發布後自動更新 indexes/graph/stats |
| 第二輪 | 89 | 補正式 tests(27 tests)、GitPublisher 記錄 subprocess returncode、挖出並修掉兩個真 bug、清理舊流程殘留 |
| 第三輪 | 91 | CI 接測試、core/docs/extras 依賴拆分、retry-failed 指令、.gitignore 補齊 |
| 第四輪 | 92 | 修正 retry-failed subshell 計數問題(here-string)、CI 補安裝 requirements、測試 11/11 通過 |
| 第五輪 | 94 | slug collision 防護、frontmatter 驗證、same-session dedupe 與真實模板對齊(session_id 進 frontmatter)、測試 27/27 |
Windows vs WSL Hook 環境差異(實際測試驗證)¶
Windows Claude Code 的 Stop hook 執行的是 Windows 原生 shell,不能使用:
- /c/Windows/system32/wsl — 報 cannot execute binary file
- /c/Windows/system32/wsl.exe — 同上
- /c/Windows/system32/cmd — 同上
正確做法:Windows 端設定用純 PowerShell/cmd 語法,WSL 端設定用 bash 語法,兩份設定完全獨立。
WSL 內設定位置:~/.claude/settings.json
Windows 端設定位置:%USERPROFILE%\.claude\settings.json 或專案 .claude/settings.local.json
Stop Hook 觸發時機(重要釐清)¶
Stop hook 不是 session 完全關閉才觸發,而是**每次 Claude 停止輸出**(即每次問答結束)都觸發。這意味著: - 一個長 session 中,每次回答結束都會觸發一次 - 同一個 session 的 hash 每次都不同(因為對話內容在增長) - ledger 的 hash-based 去重無法防止同 session 多次觸發產生重複文檔 - 實際案例:一個 session 觸發了 14 次,產生 14 篇重複文檔
解法:改為「融合更新」模式 — 偵測到已有相同 session_id 的文檔時,合併新舊內容而非跳過或覆蓋。
Bash Background 執行限制¶
# 這樣會失敗:ensure_spool_exists 找不到
nohup bash -c 'source utils.sh && ensure_spool_exists && ...' &
# 正確做法:獨立 script,不依賴 source
nohup bash /path/to/process-and-publish.sh &
ensure_spool_exists: command not found 在 log 中屬於非阻塞性錯誤,不影響主流程,但應將所需函數抽成獨立 script。
Log 重複問題¶
# 錯誤:tee -a 加上 >> 雙重導向,每行寫兩次
some_command | tee -a logfile >> logfile
# 正確:只用一種
some_command >> logfile 2>&1
# 或
some_command 2>&1 | tee -a logfile
依賴分層架構¶
requirements.txt → core: jinja2
requirements-docs.txt → docs: mkdocs, mkdocs-material, pyyaml
requirements-extras.txt → memory/graph: mem0ai, sentence-transformers, graphiti-core
mkdocs 屬於部署/預覽層,不是核心 runtime,主處理鏈路(process-session-local.py)不需要它。
What Changed¶
核心系統層(68→92 分)
移除舊 API pipeline(GitHub Actions + Anthropic API 流程),統一為 local Claude Code CLI 主流程。修正 ledger 去重語義:failed 允許 retry,processed 才是真正去重,加 lock 防併發。統一 tags.json 資料契約({tag: {count, docs:[...]}}),讓 build-indexes / build-graph / generate-stats 三者一致。文檔發布後自動觸發 indexes/graph/stats 更新並記錄各子步驟 returncode(non-fatal 但可見)。補 27 個自動化測試並接進 CI。拆分 core/docs/extras 依賴層。新增 retry-failed 指令並修正 subshell 計數問題(echo pipe → here-string)。
品質控制層(92→94 分)
新增 slug collision 防護(同名文檔自動加 -2, -3 後綴)。將 session_id 放入 frontmatter,讓 _resolve_filepath() 的同 session 重複判斷在正式模板下成立。新增 frontmatter schema 驗證(目前為 warning,非 hard gate)。測試從 11 增加到 27 個。
同 session 融合更新(94 分後新增)
發現 Stop hook 在 session 進行中每次問答結束就觸發,同一個長 session 產生 14 篇重複文檔。舊的「已有文檔則跳過」策略過於粗暴。改為「融合更新」:偵測到相同 session_id 的已有文檔時,根據新舊對話內容進行有脈絡的合併更新,取代直接覆蓋。
So What¶
這些改進讓 semi-brain 從「有野心的原型」進化成「可持續運作的 v1 系統」。
Stop hook 自動觸發確保每次 Claude Code session 結束後都會嘗試處理並發布知識。但 Stop hook 觸發時機的誤解(以為是 session 關閉,實際是每次問答結束)導致了 14 篇重複文檔的問題,揭示了「session 去重」和「文檔內容去重」必須分層處理的重要性。
融合更新模式是讓知識庫真正有意義的關鍵 — 同一議題的多次討論應該讓知識累積而非互相覆蓋,也不應該因為「已有文檔」就粗暴地跳過更豐富的後續內容。
Windows/WSL 雙環境的除錯過程揭示了 Claude Code hook 跨環境設定的重要差異,是任何在 Windows + WSL 混合環境使用 Claude Code hook 的人必須了解的知識。
Trade-offs¶
- Stop hook non-blocking 設計:以
nohup ... &異步執行避免阻塞使用者,但錯誤不會立即可見,需依賴 log 觀察 - 融合更新 vs 簡單跳過:融合更新保留知識演進脈絡,但需要 LLM 參與判斷,比跳過更耗資源;簡單跳過效率高但會遺失後續討論
- frontmatter 驗證為 warning 非 hard gate:保守策略避免阻斷發布流程,但不能強制品質門檻
- validate-docs --strict 排除 reports/、data/、stats.md:讓工具可執行(exit 0),但實際掃描文件數為 0,尚未成為有效的品質 gate
- 依賴拆分為 core/docs/extras:降低安裝負擔,但需維護三份 requirements 檔案
Try It Fast¶
# 查看最新 hook log(WSL)
tail -f ~/.local/share/semi-brain/logs/semi-brain-hook.log
# 手動觸發 semi-brain pipeline
/projects/semi-brain/scripts/hooks/process-and-publish.sh
# 測試 retry-failed(修正 subshell 計數後)
bash /projects/semi-brain/scripts/retry-failed.sh
# 跑完整測試(27 tests)
python3 -m unittest discover -s tests -v
# 驗證 tags schema 契約
python3 -c "import json; d=json.load(open('.indexes/tags.json')); print(type(list(d.values())[0]))"
# 確認 frontmatter 驗證工具(注意:目前排除 reports/ 和 stats.md)
python3 scripts/validate-docs.py --strict
# 確認 Stop hook 在 WSL settings 中正確設定
cat ~/.claude/settings.json | python3 -m json.tool | grep -A5 '"Stop"'
Recommendation¶
Priority 1 — Stop Hook 正確理解與配置
- 分開設定兩個環境 — Windows 端用純 PowerShell/cmd 語法;WSL 端用 bash。兩者設定位置完全不同,修改一個不影響另一個
- 理解觸發時機 — Stop hook 在每次問答結束就觸發,一個長 session 會觸發多次;系統設計必須能處理同一 session 的多次觸發
- bash background 用獨立 script — 背景執行時永遠將需要的函數抽成獨立 script,不依賴
source
Priority 2 — 知識品質保護
- 融合更新取代跳過 — 同 session_id 的後續觸發應融合更新,而非粗暴跳過或覆蓋;融合應基於新舊對話的實際內容演進
- ledger 三態語義 — 嚴格區分
processed/failed/pending,failed允許 retry 但需搭配 lock
Priority 3 — 品質門檻提升(到 95 分的最後一步)
- Frontmatter hard gate — 將 frontmatter 驗證從 warning 提升為真正的品質門檻;同時確認 validate-docs --strict 的掃描範圍涵蓋實際知識文檔
- 運行驗證 — 連續跑 2-4 週,確認成功率高、無重複文檔、無卡死、無錯誤去重,才能接近 100 分