chore: template 1.16.0——wiki 讀取兩支 hook(查詢即搜尋 wiki+subagent 自動注入)

leo 2026-07-20:「花很多力氣去產生 wiki,最重要的就是要可以查詢,
結果要查的時候就跳過,那就白寫了」

- wiki-first-search.sh(Grep|Glob|Read):查 code 的當下用同組關鍵字 grep wiki,只推命中行
- subagent-wiki-guard.sh(Task):查證類任務自動注入「先查 wiki」給 subagent
- update.sh/install.sh 來源改指 Gitea(原指 GitHub uncle6me-web 已 suspend=自動更新早就死了)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 01:40:55 +08:00
parent 20c7610371
commit df1b953d1f
11 changed files with 521 additions and 20 deletions
+161
View File
@@ -0,0 +1,161 @@
# /wiki-extract — vault 增量萃取(Logseq / Obsidian → system-dev/wiki
把**筆記 vault**Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**
AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB。
> **跟 `/wiki-init` 的分工**
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
> - `/wiki-extract` 是**之後每次**的增量重萃——vault 會被 Syncthing/cron 持續灌新筆記,
> 這支負責「只萃變動的、沒變的不碰、不浪費 AI run」。給 Routine / cloud-worker 反覆跑。
> - **跑它的是你(CC / Routine)=LLM 本人,不需任何 token**。
> **邊界(硬規矩,別越界)**
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue),不是這支的事。
> - **原始筆記唯讀**`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
---
## 執行流程
### 第一步:確認這是 vault repo,定位 raw source
偵測邏輯**同 install.sh / wiki-init**
| 偵測到 | 型態 | raw source(要掃的原文) |
|--------|------|--------------------------|
| 根目錄有 `logseq/` | Logseq vault | `journals/*.md` + `pages/*.md` |
| 根目錄有 `.obsidian/` | Obsidian vault | vault 根下所有 `.md` |
| 都沒有 | **不是 vault** | → 停手。這支只處理 vault;一般 dev repo 開發時就手寫 `.claude`/`system-dev/wiki`,不需萃取 |
沒有 `system-dev/wiki/`?→ 先跑 `/wiki-init`(首次建結構+首萃),再回來用這支做增量。
### 第二步:content_hash 冪等 —— 決定哪些檔要萃(省 run 的核心)
讀萃取 manifest`system-dev/wiki/.extract-manifest.json`(不存在=首次,視同全部要萃)。
格式:
```json
{
"version": 1,
"algo": "sha256",
"sources": {
"journals/2026_07_01.md": {
"content_hash": "<sha256 of file bytes>",
"extracted_at": "2026-07-06",
"cards": ["Prompt能力即拆解自己邏輯的能力", "程式化邏輯可圖解任何主題不限AI"],
"skipped_reason": null
},
"journals/2026_06_25.md": {
"content_hash": "<sha256>",
"extracted_at": "2026-07-06",
"cards": [],
"skipped_reason": "空檔/訊息量不足,無可萃知識點"
}
}
}
```
對每個 raw source 檔:
1. 算目前 `content_hash``sha256sum <file>`,取檔案 bytes 的 hash)。
2. 跟 manifest 裡該檔的 `content_hash` 比:
- **相同 → skip,不讀不萃、不呼叫任何 AI 推理**(就算它上次 `cards: []` 也 skip——空檔沒變還是空)。
- **不同或不在 manifest → 這檔要(重)萃**。
3. manifest 有、但檔已不存在 → 該檔被刪,把它的 entry 從 manifest 移除(卡片是否連帶處理見第五步)。
> **這一步是「省 run」的重點**:vault 每天可能只動 1~2 個 journal,其餘幾十個檔 hash 沒變
> 就整批跳過,AI 只對真正變動的檔動腦。**重跑一個沒變動的 vault = 零 AI 呼叫、零 diff。**
### 第三步:對「要萃」的檔,抓知識點 + 任務
逐個變動檔讀原文,分兩類抽取:
**(a) 知識點 → 概念原子卡**
判準與卡片格式**完全依 `/wiki-init` 第五步**frontmatter `tags:`/`gloss:`、H1、麵包屑
`← [[<bucket>/00-INDEX]]``**來源**``## 摘要``## 重點``## 實體``## 關聯`
typed-edge 三元組、TAXONOMY 受控標籤、硬自檢等)——**不在這裡重寫格式,一律回去讀那份**。
廢話/訊息量薄的段落略過(在 manifest 記 `skipped_reason`,誠實留痕、不留卡)。
**(b) Logseq 任務 marker → 任務卡(task_status**
解析**完全依** `system-dev/docs/4-guides/logseq-markers.md`(單一真相源,與 template#4
tasks 投影共用同一套;**別自己另寫 mapping**)。摘要:
- 任務行 regex`^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+`
- 狀態正規化:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`
DONE→`done`、CANCELED/CANCELLED→`closed`
- 跳過 `:LOGBOOK:…:END:` 區塊與 `key:: value` 屬性行(`collapsed::``id::`
`SCHEDULED::``DEADLINE::`…),**別把 marker 或屬性當任務內文**。
有實質內容的任務 → 產一張任務卡進 `cards/tasks/` bucketfrontmatter 帶 `task_status`
```markdown
---
tags: [<領域標籤,依 TAXONOMY>]
task_status: todo # ← 依上表正規名;這是任務卡才有的欄位
gloss: 一句話定義這個任務要達成什麼(供下游 normalize)
---
# <任務一句話標題(marker 後的內文,去掉 marker>
← [[tasks/00-INDEX]]
**來源**`journals/2026_07_01.md`TODO block
**最後更新**YYYY-MM-DD
## 摘要
[任務要做什麼、脈絡]
## 實體
- **<關鍵實體正規名>**<同義詞>)— <一句描述>
## 關聯
### 內文知識關係(端點=上方 `## 實體` 正規名,一字不差)
- <實體A> >> <謂詞> >> <實體B>
### 卡片關係(卡對卡)
- [[本任務卡]] >> 涉及 >> [[相關概念卡]]
```
> 純瑣事任務(「買菜」這種無知識量)不必成獨立卡——可在 `cards/tasks/00-INDEX.md`
> 列一行帶狀態即可,避免灌垃圾卡。判準同 D16:有沒有知識/專案價值。
### 第四步:更新桶索引與 INDEX
- 每個動到的 bucket(如 `cards/notes/``cards/tasks/`)更新其 `00-INDEX.md`
(容器:只連不重寫,H2/H3 分節)。
- 更新 `system-dev/wiki/INDEX.md` 的標籤視圖與卡片清單。
- 任務卡可在 INDEX 開一個「任務視圖」按 `task_status` 聚類。
### 第五步:寫回 manifest + 驗證原文 0 動
1. 把這次萃過的每個檔的**新 `content_hash`**、`extracted_at`、產出的 `cards`
(或 `skipped_reason`)寫回 `system-dev/wiki/.extract-manifest.json`
**沒動到的檔的 entry 原樣保留**(別整檔重寫掉別人的 hash)。
2. 驗證原文零異動(踩過的坑):
```
git status --short journals/ pages/ # Obsidian 則看根目錄 .md ——須 0 新增 0 修改
```
有任何原文變動 → 你誤寫了 raw source,回滾。
### 第六步:完成報告
```
✅ wiki-extract 完成(增量)
掃描:N 個 raw source 檔
萃取:M 個(content_hash 變動)→ 產出 X 張概念卡 + Y 張任務卡
跳過:K 個(hash 未變,零 AI 呼叫)
任務狀態分布:todo A / in-progress B / done C / …
原文驗證:journals/ pages/ git status 0 異動 ✅
manifestsystem-dev/wiki/.extract-manifest.json 已更新
```
---
## 冪等自檢(Routine 反覆跑必守)
- [ ] 跑之前先讀 manifesthash 相同的檔**完全不進 AI**(不是「讀了才發現一樣」,是靠 hash 先擋)。
- [ ] 對「同一個沒變動的 vault」連跑兩次:第二次應是**零萃取、零卡片 diff、零 manifest 變化**。
- [ ] 只有 `system-dev/wiki/` 有寫入;`journals/`、`pages/` git status 全乾淨。
- [ ] 任務狀態用正規名,marker/屬性沒混進內文(照 `logseq-markers.md` 自檢)。
+5
View File
@@ -228,3 +228,8 @@ git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0
原文驗證:pages/ journals/ git status 0 異動 ✅
下一步:用 /wiki-capture 把重要決策存進 wiki
```
> **vault repo 首萃後的增量重萃**Logseq / Obsidian vault 會被持續灌新筆記。首萃(本命令)
> 之後,改用 **`/wiki-extract`** 做增量——它靠 content_hash 只萃變動的檔(沒變=零 AI 呼叫),
> 並解析 Logseq 大寫任務 markerTODO/DOING/DONE…→ `task_status`,見
> `system-dev/docs/4-guides/logseq-markers.md`)。適合掛給 Routine / cloud-worker 反覆跑。
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# subagent-wiki-guard.sh — PreToolUse(Task) hooksubagent 聽到「查」就自己先查 wiki
#
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEYprompt 都只叫它「去查 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
+74
View File
@@ -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 用 patternGlob/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
+19 -1
View File
@@ -42,6 +42,24 @@
"timeout": 5
}
]
},
{
"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"
}
]
}
],
"PostToolUse": [
@@ -68,4 +86,4 @@
}
]
}
}
}