diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1c187..aa1d036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,40 @@ --- +## 1.16.0 — 讓 wiki 真的被讀到:查詢即搜尋+subagent 自動注入 + +**病根(leo 2026-07-20 點破,真實事故)**:總管三次擋回 leo「某機制早已棄用」的正確判斷, +查證後 leo 全對。根因不是知識不足,是**讀取流程**: +① wiki 只讀開頭就開工(關鍵記載在第 56 行,答案一直在那裡) +② 派 subagent 只叫它讀 code、沒叫讀 wiki → agent 從稿子推論,**必然**得出過時結論 +③ 把 wiki 的「當時狀態」當永久事實(沒核對解除條件) + +leo:「我需要的不是你記住,而是如何解這題不再發生,**機制面的解法**」 +「如果你不是讀而是**搜尋** wiki,就不會只讀 50 行就下定論,而是像 cmd+F 那樣高亮。」 + +**新增兩支 hook(皆不依賴任何人自覺)**: + +- **`wiki-first-search.sh`**(PreToolUse: `Grep|Glob|Read`) + 在「正要去翻 code/文件」的當下,用同一組關鍵字 grep `system-dev/wiki/`, + **只推命中行**(非開場 push 全文——那必然只被讀開頭)。提醒不阻擋: + wiki 沒記載時本來就該翻原文,唯一目的是消滅「不知道 wiki 有寫」。 + +- **`subagent-wiki-guard.sh`**(PreToolUse: `Task`) + 偵測查證/實作類任務 → **注入**「先查 wiki」指示給 subagent。 + ⚠️ 第一版設計為「上游 prompt 沒交代就擋下」,經 leo 指正改為注入式: + 「subagent 的問題跟你一樣——**它只要聽到查,就應該主動查 wiki**, + 因為每個 repo 都有維護自己的 wiki。」依賴上游記得寫指示 = 同一個病。 + +**安裝行為**:`update.sh` 除同步兩支 hook 外,**自動註冊進 settings.json**(不只提醒)。 +理由同上——靠人看提醒手動補,等於把同一個病搬到安裝環節。 + +**注入給 subagent 的三條硬規則**: +1. wiki 與程式碼衝突 → **以 wiki 為準**,回報衝突,不自行用 code 推翻 wiki +2. wiki 寫「不可動/待廢除/進行中」→ 讀它的**解除條件**逐條核對(那是當時狀態,非永久禁令) +3. 翻原文後得到新結論 → 回報「wiki 該更新」(wiki 過時是債,要還) + +--- + ## 1.15.0 — SDD 生命週期鐵律:單一活性 SDD(issue #6) leo 拍板全體系採「單一活性 SDD」制度:任何時刻每個 repo 只有一份現行 SDD(`status: active`),所有開發任務唯一對應它的 tasks。prompt 軟約束+檔案系統硬約束(hook)雙層。 diff --git a/scripts/update.sh b/scripts/update.sh index ba53606..62268a7 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -228,6 +228,9 @@ if $HAS_WIKI; then update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md" update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh" update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh" + # 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG) + update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh" + update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh" update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md" update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md" update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md" @@ -337,6 +340,34 @@ if [ -f ".claude/settings.json" ]; then $HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh") $HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh") $HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh") + + # ── 1.16.0:兩支 wiki 讀取 hook 自動註冊(不只提醒)── + # 理由:這兩支的整個存在意義就是「不依賴任何人記得」。 + # 若靠人看提醒去手動補 settings.json,等於把同一個病搬到安裝環節。 + if $HAS_WIKI && command -v python3 >/dev/null 2>&1; then + python3 - <<'PYEOF' 2>/dev/null || true +import json, os +p = ".claude/settings.json" +try: + with open(p) as f: d = json.load(f) +except Exception: + raise SystemExit(0) # 壞掉的 settings 不碰,交給下方提醒 +pre = d.setdefault("hooks", {}).setdefault("PreToolUse", []) +blob = json.dumps(pre) +added = [] +if "wiki-first-search" not in blob: + pre.append({"matcher": "Grep|Glob|Read", "hooks": [ + {"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"}]}) + added.append("wiki-first-search") +if "subagent-wiki-guard" not in blob: + pre.append({"matcher": "Task", "hooks": [ + {"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"}]}) + added.append("subagent-wiki-guard") +if added: + with open(p, "w") as f: json.dump(d, f, ensure_ascii=False, indent=2) + print(" ✅ 已自動註冊 wiki 讀取 hook:" + "、".join(added)) +PYEOF + fi if [ ${#MISSING[@]} -gt 0 ]; then echo "" t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \ diff --git a/template/.claude/VERSION b/template/.claude/VERSION index 141f2e8..15b989e 100644 --- a/template/.claude/VERSION +++ b/template/.claude/VERSION @@ -1 +1 @@ -1.15.0 +1.16.0 diff --git a/template/.claude/hooks/subagent-wiki-guard.sh b/template/.claude/hooks/subagent-wiki-guard.sh new file mode 100755 index 0000000..6819fb6 --- /dev/null +++ b/template/.claude/hooks/subagent-wiki-guard.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki +# +# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。 +# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。 +# +# 🔑 設計轉向(leo 2026-07-21): +# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**, +# 跟「我記得讀 wiki」是同一個病。leo 點破: +# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki, +# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」 +# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條 +# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。 +# +# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。 +# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。 +set -euo pipefail + +INPUT=$(cat) + +PROMPT=$(printf '%s' "$INPUT" | python3 -c " +import json,sys +try: + d=json.load(sys.stdin) + print(d.get('tool_input',{}).get('prompt','')) +except Exception: print('') +" 2>/dev/null || echo "") + +[ -z "$PROMPT" ] && exit 0 + +# 上游已經交代了 → 不必重複注入 +if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then + exit 0 +fi + +# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要) +if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then + exit 0 +fi + +python3 - <<'PY' +import json + +guidance = """【自動注入:查任何東西之前,先查 wiki】 + +你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。 +**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。** + +做法(30 秒,省下大量白工): + grep -rin "<本題關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/ + +為什麼這是划算的: + • wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。 + • 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。 + 從稿子推論會系統性得出過時結論。 + • wiki 沒記載,才值得花力氣翻原文。 + +三條硬規則: + 1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突, + 不要自行用 code 推翻 wiki。 + 2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。 + 那是當時狀態,不是永久禁令;條件已滿足就是可動。 + (2026-07-20 實際事故:agent 只看到「不可動」就回報不能動, + 實際上解除條件早已滿足,害負責人被擋三輪。) + 3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。 +""" + +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": guidance + } +}, ensure_ascii=False)) +PY + +exit 0 diff --git a/template/.claude/hooks/wiki-first-search.sh b/template/.claude/hooks/wiki-first-search.sh new file mode 100755 index 0000000..309d637 --- /dev/null +++ b/template/.claude/hooks/wiki-first-search.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前 +# +# 病根(2026-07-20 leo 點破,mistakes 第一鐵律): +# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。 +# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論, +# 而是就像我直接在頁面 cmd+F,那些都會高亮。」 +# +# 設計要點(為什麼是這個形狀): +# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。 +# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。 +# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。 +# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。 +# 唯一目的是消滅「不知道 wiki 有寫」這件事。 +# +# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。 +# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。 +set -euo pipefail + +INPUT=$(cat) +TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "") + +# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分 +QUERY=$(printf '%s' "$INPUT" | python3 -c " +import json,sys,os,re +try: + d=json.load(sys.stdin); ti=d.get('tool_input',{}) + q = ti.get('pattern') or '' + if not q: + p = ti.get('file_path') or ti.get('path') or '' + q = os.path.splitext(os.path.basename(p))[0] if p else '' + # grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞 + words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q) + print(max(words, key=len) if words else '') +except Exception: + print('') +" 2>/dev/null || echo "") + +[ -z "$QUERY" ] && exit 0 + +WIKI_DIR="system-dev/wiki" +[ -d "$WIKI_DIR" ] || exit 0 + +# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了) +TARGET=$(printf '%s' "$INPUT" | python3 -c " +import json,sys +try: + d=json.load(sys.stdin); ti=d.get('tool_input',{}) + print(ti.get('file_path') or ti.get('path') or '') +except Exception: print('') +" 2>/dev/null || echo "") +case "$TARGET" in + *system-dev/wiki*) exit 0 ;; +esac + +# grep wiki(不分大小寫、含行號),最多 12 行避免洗版 +HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true) +[ -z "$HITS" ] && exit 0 + +COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ') + +echo "════════════════════════════════════════════════" +printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT" +echo "════════════════════════════════════════════════" +printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |' +echo "" +echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。" +echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。" +echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對," +echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。" +echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。" +echo "" + +exit 0 diff --git a/template/.claude/settings.json b/template/.claude/settings.json index 6fe65d1..e05057e 100644 --- a/template/.claude/settings.json +++ b/template/.claude/settings.json @@ -28,7 +28,25 @@ "command": ".claude/hooks/wiki-secret-scan.sh" } ] + }, + { + "matcher": "Grep|Glob|Read", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh" + } + ] + }, + { + "matcher": "Task", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh" + } + ] } ] } -} +} \ No newline at end of file