今天要解的問題

Day29 調整的是 vault 的內容流程,今天往回看一層:給 Claude 的那些指令本身。

這個 vault 的設定是一個月裡一點一點長出來的:根目錄的 CLAUDE.md、.claude/skills/ 底下 16 個 skill、Day26 開始的 navi mod、幾支在 session 開始與寫檔前後執行的 hook。中間模型換過一輪,但沒有任何東西會去問:這些寫給舊模型的指令,現在還合用嗎?

我一開始丟給 Claude 的需求是這樣的:

我想要一個 skill 或 mod 定期檢查 claude.md、skill/mod/hook 的狀態,是否符合新的模型,若是需要修改請告訴我,與給我一個新的使用說明。不知道 claude 的 doctor 功能可否

今天做了兩件事:

  1. 主線:/claude-doctor,一個每月跑一次、換模型時會提醒的設定健檢
  2. 支線:第一輪稽核的結果,CLAUDE.md 瘦身、把 10 個 skill 的強調語氣降下來

同一個 session 後來還做了全 vault 的 source 欄位統一和藝評頁標題格式,留到 Day32 再寫。

想法與取捨

用內建的 /doctor,還是自己做一個

先查內建工具能做到哪裡:

  • claude doctor:在終端機跑、不開 session,看的是安裝狀態和設定檔能不能讀
  • session 裡的 /doctor:會看無效的設定、沒在用的 skill/plugin/MCP、CLAUDE.md 裡 Claude 自己就推得出來的贅述
  • /doctor prompt-audit:找過時或互相矛盾的指令、指向不存在檔案的參照。要 v2.1.283 以上,我的版本是 2.1.286,可以用
  • /skill-doctor:看每個 skill 佔多少 context、多常被叫到

沒有一個會回答「這些指令適不適合目前的模型」,也不知道這個 vault 自己的規則:CLAUDE.md 的 skill 表要跟 .claude/skills/ 資料夾一致、README 要跟實際的 hook 對得上。

所以做法是兩個都要:claude doctor 當成稽核的其中一個輸入,vault 自己的規則交給新的 skill。/doctor prompt-audit 和 /skill-doctor 是互動指令,skill 裡沒辦法代跑,就在報告最後提醒我自己去跑一次交叉比對。

先給計畫,還是先做研究

Claude 第一版直接交了設計:寫一支稽核腳本、加一個 SessionStart 提醒、每月排程。我沒有批准,回了這句:

我想要你先做個 research,或是每次都做個 research,來了解大家的使用,或是提出我可以修改的地方

理由很直接:「適不適合新模型」不是我或 Claude 憑印象能判斷的,標準會跟著官方文件和社群做法一起變。所以研究不是設計前的一次性步驟,而是 skill 每次執行的第 0 步。

第一輪研究派了兩個 subagent,一個查官方文件,一個查社群做法。跟這個 vault 直接相關的結論有幾條:

  • CLAUDE.md 官方建議每份 200 行以內,越長遵循度越低;我的有 251 行
  • 新模型對 system prompt 更敏感,為了防止「漏做」而寫的 CRITICAL、MUST、粗體,現在反而會讓它過度套用
  • 只在某些檔案才需要的規則,可以放進 .claude/rules/,用 paths: 限定,碰到對應檔案時才載入
  • <!-- --> 註解進 context 前會被拿掉,適合放給人看的歷史理由

為了讓「每次研究」不變成每次重講一樣的事,研究結果寫進 research-notes.md,下次只回報跟上一版比有變化的。裡面有一區叫「評估過不採用」,只增不減,例如社群有人主張 CLAUDE.md 要壓到 60 行以內,我判斷這份同時是 vault 的路由規則,砍到 60 行查找成本太高,這條就記進去,下個月不會再被提一次。

稽核完要不要直接改

稽核會找出很多該改的地方,但 skill 本身只報告、不改設定。會被它改寫的只有兩個地方:README 裡由腳本產生的那一段、研究摘要。

這是 Claude 在設計裡就放進去的原則,我沒有異議。設定檔是我跟 Claude 之間的約定,改哪一條、改成什麼語氣,應該由我挑。今天後面降強調、瘦身 CLAUDE.md,都是我看完報告之後一項一項點頭才做的。

換模型的提醒:放 hook,還是放 mod

第一版的提醒是兩支 hook。Claude 原本打算讓 SessionStart 從輸入裡讀模型名稱,查文件才發現 SessionStart 的輸入沒有模型欄位;有另一個事件 PostModelSwitch,會帶 from_model 和 to_model。所以變成 PostModelSwitch 負責換模型、SessionStart 負責「超過 35 天沒稽核」。

做完我問了一句:為什麼這個是用 hook,而不是用 mod?

Claude 的回答承認它是照著 vault 既有的守門 hook 寫,沒有拿 mod 來比過。比下來差別在訊息給誰看:

  • hook 的輸出進的是 Claude 的 context。要不要轉告我,是 Claude 決定的,我可能根本看不到;每個 session 還多佔一點 context
  • navi 的狀態列畫在我的畫面上,本來就只列「要處理的事」(Inbox 待分類數、raw 沒消化的域),多一項「設定 N 天未稽核」剛好

hook 唯一的優勢是不需要畫面也會跑,但沒人在場的情況已經由每月排程處理了,這個優勢用不到。

我接著問:那是不是 routine 跑的時候全部換成 mod、hook 都變成 mod?Claude 補清楚:守門 hook 不動。擋下「在主 checkout 寫檔」「推到 main」這種事,必須在任何環境都可靠生效,包括沒有畫面的排程,那就只能是 hook。最後的分工變成:

claude-doctor 的分工:skill 稽核並在 README 寫入稽核戳記,每月 routine 觸發 skill,navi 狀態列讀戳記與目前模型決定要不要提醒

  • skill 做事:研究、稽核、報告、重生 README,最後在 README 尾端蓋一個戳記:哪個模型、哪一天稽核的
  • routine 觸發:每月 1 號 09:00 自動跑一次,開 PR 等我看
  • mod 提醒:navi 讀那個戳記,模型不同或超過 35 天就在狀態列顯示

每 30 秒檢查一次,還是 turn 開始時檢查

改成 navi 之後,第一版是掛在 navi 原本就有的 30 秒刷新上。我問:30 秒是必要的嗎?模型應該不會很常更新。

確實不必要。會這樣寫只是因為那裡剛好有一個現成的計時器。模型只會在兩個 turn 之間用 /model 切換,戳記一個月才變一次,每 30 秒讀一次 README 是白做工。

改成三個時間點:session 開始時算一次、每個 turn 開始時重算、手動 /navi refresh 時重算。換模型後送出的第一句話就會反映,結果沒變就不重畫狀態列。Inbox 數那些項目照舊每 30 秒更新,兩件事拆開。

強調語氣要降到多少

第一輪稽核在 11 個檔案標出「強調密度偏高」,最高的是 vault-maintain:147 行裡有 81 個粗體、禁止、一律、絕不。

我先讓 Claude 改了 vault-maintain,改完再請它講清楚強調的好處和壞處,才決定其他 skill 要不要跟進。好處是標出真正不能錯的事、衝突時有優先順序、人掃讀方便;壞處是稀釋(81 個裡真正危險的兩條,看起來跟「回傳結構化結果」一樣重要)、新模型會過度套用、只有命令沒有理由時遇到例外無法判斷。

改 vault-maintain 時 Claude 用的標準是:只有做錯會丟資料或無法復原的規則留強調,旁邊附理由;其他改成「不做 X,因為 Y」。vault-maintain 最後只剩兩條:清 worktree 時不加 --force(會連未 commit 的工作一起刪)、I7 永不自動修(那些分支是 Inbox 擷取的唯一副本)。

Claude 自己也講了兩個限制,我照錄:門檻「每 100 行 8 個」是它依「只留少數幾條」的原則定的,不是官方數字;降強調之後表現會不會更好,這個 vault 還沒量過,目前只是照官方建議的預期。

實作

/claude-doctor 的流程

.claude/skills/claude-doctor/SKILL.md 分成六步:

步驟做什麼
Phase 0兩個 subagent 研究官方與社群做法,只回報跟上一版的差異
Phase 1claude doctor+稽核腳本 config_audit.py
Phase 2subagent 逐個讀 CLAUDE.md 與 SKILL.md,找語氣與過時步驟
Phase 3報告:新做法/應改/建議/通過
Phase 4重生 README 的自動產生區塊,蓋稽核戳記
Phase 5寫 log、開 PR

能用規則判斷的全部交給腳本,C1 到 C8:skill 表跟資料夾一致、frontmatter 欄位合法、檔案行數、寫死的模型名稱、hook 有沒有接好(--smoke 會實際餵一筆假輸入給 hook,確認寫根目錄 log.md 會被擋下)、強調密度、README 是否過時、權限規則指向的路徑還在不在。

驗證方式是在 scratchpad 複製一份 vault,故意弄壞四個地方:刪掉 skill 表的一列、加一個不存在的 frontmatter 欄位、讓 hook 指向不存在的腳本、把戳記改成 67 天前。四個都被對應的檢查抓到。

判斷邏輯獨立成 hooks/audit.ts,方便寫測試:

export function auditNote(readme: string, model: string, nowMs: number): string | undefined {
  const m = readme.match(STAMP)
  if (!m) return '設定未稽核(/claude-doctor)'
  const [, audited, date] = m
  // 只有別名(opus/sonnet)沒有版本號時無法判斷是否換過,略過模型比對
  const cur = normalizeModel(model)
  if (/\d/.test(cur) && cur !== normalizeModel(audited)) {
    return `模型已換 ${cur},設定未稽核(/claude-doctor)`
  }
  const days = Math.floor((nowMs - Date.parse(`${date}T00:00:00`)) / 86_400_000)
  if (days > AUDIT_MAX_DAYS) return `設定 ${days} 天未稽核(/claude-doctor)`
  return undefined
}

STAMP 比對的是 README 尾端 <!-- claude-doctor: model=... date=... --> 這行。normalizeModel 會把 claude-opus-5-5[1m]、帶日期的快照版本、顯示名稱「Opus 5.5」都轉成同一個 opus-5-5,同一個模型換個 context 長度不算換模型。

掛到 navi 上的部分:

// 換模型(/model)後的第一個 turn 就會反映;不 await,不拖慢 turn
on('turn.start', async ($, e, next) => {
  void checkAudit($)
  return next(e)
})

async function checkAudit($: $) {
  const note = await auditCheck($, await $.clock.now())
  if (note !== auditMsg) {
    auditMsg = note
    await refresh($)
  }
}

checkAudit 不 await,turn 不會因為讀 README 而卡住;結果跟上次一樣就不重畫。目前模型由 $.session.model() 取得,這是 mod API 本來就有的方法,不需要另外接事件。claude plugin test 跑 6 個測試全過:換模型、[1m] 後綴、只有別名、67 天前、沒有戳記。

CLAUDE.md 瘦身

照研究結果搬了三類東西出去:

  • 多個 session 共用主 checkout 時的 fallback 協定(8 條)和 hook 的細節,搬進 .claude/README.md,CLAUDE.md 只留一行指引
  • wiki 頁的 frontmatter 規格、書頁連結的路徑限定、圖片放置,搬進 .claude/rules/wiki-format.md,加上 paths: 讓它只在編輯 wiki、raw、logs、hubs 時載入
  • 「為什麼 log 要切成月檔」這類歷史理由,改成 HTML 註解,人看得到、不佔 context

結果是 251 行、19,278 字變成 179 行、11,313 字。

10 個 skill 降強調

vault-maintain 由 Claude 直接改,另外 9 個分給 5 個 subagent 平行改。這種批次改寫最怕「順手」改到內容,所以收回來之後用腳本逐檔比對:frontmatter、行數、所有 code span、所有數字,都要跟改之前一模一樣。全部通過,只有兩處差異,一處是新加的理由裡多了一個 `TODO.md`,一處是原本藏在粗體裡的數字現在被算到了,都不是內容變動。

10 個 skill 改前改後的強調標記數:book-review 85→2、vault-maintain 81→2、note-explore 55→13、note-enrich 52→2、inbox-triage 51→2、art-review 40→2、subtitle-draft 35→11、topic-probe 35→4、wiki-build 33→3、edit-article 9→0

合計 476 個強調標記降到 41 個。剩下的多半在 skill 的輸出範本裡,那是 skill 要印出來的格式本身,拿掉會改變輸出,就留著。

踩到的坑

強調密度把 glob 當成粗體

剛搬出去的 .claude/rules/wiki-format.md 被標成強調密度偏高:36 行裡 3 個標記。可是這個檔裡根本沒有粗體。

🟡 C6 .claude/rules/wiki-format.md  emphasis density 8.3/100 lines (3 markers in 36 lines)

一查,三個標記是 frontmatter 裡的 paths:"**/wiki/**"、"**/raw/**"、"**/logs/**"。兩個星號夾著文字,正好符合粗體的正規式。

第一次修,是在計算前把 frontmatter 和程式碼區塊整段刪掉。重跑之後反而多冒出三個警告:CLAUDE.md、claude-doctor、medium-cover 都超標了。原因是刪掉程式碼區塊也刪掉了那些行,分母變小,密度就變大。

第二次改成「把內容換成同樣數量的空行」:

blank = lambda m: "\n" * m.group(0).count("\n")
visible = re.sub(r"<!--.*?-->", "", text, flags=re.S)
visible = re.sub(r"\A---\n.*?\n---\n", blank, visible, flags=re.S)
visible = re.sub(r"```.*?```", blank, visible, flags=re.S)
visible = re.sub(r"`[^`\n]*`", "", visible)

這樣排除的東西不算進分子,行數還是照原檔算。之後的做法:要從分子排除的東西,不要連帶改到分母。

subagent 回報的「問題」,本身判斷錯

降強調時 Claude 請每個 subagent 順便列出「看起來過時或矛盾、但沒動」的內容。收回來二十幾條,大部分是真的,例如 book-review 寫「status → 2」,但欄位定義的值是「閱讀完畢」這種文字。

其中一條說 art-review 用裸名 [[{展名}]] 有歧義,因為 art/raw 和 art/wiki 有同名檔,應該像書一樣加路徑。這條是錯的:wiki-format.md 寫明了只有 books 的 raw 和 wiki 都是有效的連結目標,其他域的 raw 是被 wiki 取代的沉澱,裸名照慣例指向 wiki 頁。

不過這條錯誤的回報並非沒價值。它讓 Claude 回頭發現 vault-maintain 的 E4 寫的是「裸名命中多個同名檔就是違規」,跟 wiki-format 的慣例確實矛盾,之後跑 vault-maintain 可能會誤報 art 頁。subagent 給的是線索,不是結論;採用之前要回到規則本身對一次。

小結

  • 新增 /claude-doctor:每次先研究、再用腳本稽核 C1–C8、報告只建議不改設定、重生 README 並蓋稽核戳記
  • 每月 1 號 09:00 的 routine 自動跑,第一次在 11/1
  • 換模型與 35 天未稽核的提醒放在 navi 狀態列,在 session 與 turn 開始時檢查;原本的兩支提醒 hook 移除,守門 hook 不動
  • CLAUDE.md 從 251 行瘦到 179 行,wiki 寫作格式拆成 path-scoped rules;5 個手動才用的 skill 加了 disable-model-invocation,不再出現在每輪的 skill 清單裡
  • vault-maintain 強調標記 81 → 2,另外 9 個 skill 395 → 39

已經知道、還沒做的有兩件:book-review 抓書封寫死了一個這個 session 沒有的瀏覽器工具(已記進 TODO.md);vault-maintain 的 E4 跟 wiki-format 的同名慣例矛盾(還沒決定怎麼改)。降強調的效果,要等之後用一段時間才知道。