feat(spec-version-guard): 新增版本前置檢查規範、共用腳本與 hook
新增 spec-version-guard 規範(定義遠端發佈版本 vs 當前實際載入版本的比對規則、 fail-closed、錯誤訊息格式)與 scripts/version-guard.mjs(hook/CLI 雙模式, hook 模式輸出 Claude Code/Copilot 相容的 PreToolUse deny JSON);spec-preflight 的載入順序補上版本檢查第 0 步;新增 hooks/hooks.json 掛 PreToolUse; do-wiki/models/plan-wiki/plugins-uninstall/todo-wiki 五個 skill 檔頭引用新規範 (plugins-install 刻意排除,避免版本落後時擋住自己的修復手段)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: spec-version-guard
|
||||
description: JSC plugins 共用「plugin 版本前置檢查規範」:定義三種版本(遠端發佈版本/助理註冊版本/當前實際載入版本)的差異、規定比對對象只能是「遠端發佈版本 vs 當前實際載入版本」不得只看註冊版本、查不到遠端版本一律 fail-closed 阻擋、錯誤訊息格式依 spec-time-log、阻擋後導向 plugins-install 或 spec-plugin-cli 更新指令。當其他 skill 內文引用 spec-version-guard 或 /jsc-shared:spec-version-guard、或執行任何 JSC skill 前需要先確認本機版本是否落後於 Gitea master 時載入此 skill。單獨被使用者呼叫時,直接說明本規範內容。
|
||||
---
|
||||
|
||||
# spec-version-guard — 共用 plugin 版本前置檢查規範
|
||||
|
||||
任何 JSC skill 在做任何實質步驟之前,都必須先確認自己實際載入的內容是否落後於已發佈的版本;落後時繼續執行等於用舊規則做事,可能直接違反已經改掉的規則(例如舊版 `todo-wiki` 用連字號命名 TODO 頁、完全不知道 `spec-wiki-contents` 存在)。本規範定義這個檢查該比對什麼、比對不到時怎麼辦、以及失敗訊息長什麼樣子。
|
||||
|
||||
## 三種版本,只有兩種能拿來比
|
||||
|
||||
| 版本 | 意義 | 取得方式 |
|
||||
| --- | --- | --- |
|
||||
| 遠端發佈版本 | Gitea `master`(發佈分支)上 `plugin.json` 的 `version` 欄位;四個 JSC plugin 各自對應 `plugins/<name>.git` | `GET https://<host>/api/v1/repos/plugins/<name>/raw/plugin.json?ref=master` |
|
||||
| 助理註冊版本 | 各助理自己的 plugin 安裝清單記錄的版本(例如 Claude Code 的 `~/.claude/plugins/installed_plugins.json`、`claude plugin list` 的輸出) | 助理原生指令或設定檔 |
|
||||
| **當前實際載入版本** | 本次 session/本次 hook 呼叫,實際從磁碟讀進來執行的那份 plugin 內容的版本 | 見〔取得「當前實際載入版本」〕 |
|
||||
|
||||
**唯一合法的比對對象是「遠端發佈版本 vs 當前實際載入版本」**,**不得只比對「遠端發佈版本 vs 助理註冊版本」**。原因:助理的安裝清單只記錄「上次安裝/更新時寫入的版本號」,跟「這次 session 實際從哪個目錄讀取內容」是兩回事——同一個助理的快取目錄底下可能同時存在好幾個版本的 plugin 副本(例如 Claude Code 的 `~/.claude/plugins/cache/shared/jsc-shared/` 下並存 `0.1.2`/`0.1.3`/`0.1.4`/`0.1.5`/`0.2.0` 五個版本目錄),註冊表可能已經寫著最新版號,但本次 session 因為快取或會話啟動時機問題,實際載入的還是舊目錄。只比註冊版本會誤判「一切正常」,抓不到這種落差。
|
||||
|
||||
## 取得「當前實際載入版本」
|
||||
|
||||
- **Claude Code/GitHub Copilot CLI**:hook 情境下讀 `${CLAUDE_PLUGIN_ROOT}/plugin.json` 的 `version`(`CLAUDE_PLUGIN_ROOT` 由這兩家助理在呼叫 hook 時注入,指向這次 session 實際掛載的 plugin 目錄,不是註冊表路徑);skill/command 執行情境下,可從呼叫端提供的「Base directory for this skill」之類的實際路徑取得同等資訊。
|
||||
- **Codex**:無對應環境變數時,由呼叫端傳入 plugin root 路徑(見 `shared/scripts/version-guard.mjs` 的 `--plugin-root` 參數),或依 skill 執行時的實際路徑推得。
|
||||
- **Antigravity/OpenCode**:兩者走「clone 到固定目錄+本地路徑安裝」(見 `/jsc-shared:spec-plugin-cli`),當前實際載入版本=該固定 clone 目錄下 `plugin.json` 的 `version`。
|
||||
- 一律**不得**用「助理安裝清單版本」或「使用者記得自己上次更新到幾版」頂替上述任何一種取得方式。
|
||||
|
||||
## 強制機制形態:規範+腳本+hook 雙層,僅部分助理有真阻擋
|
||||
|
||||
依實測與官方文件比對(見〔各助理 hook 真阻擋能力〕),五家助理只有 **Claude Code** 與 **GitHub Copilot CLI** 具備官方文件保證、可真正中止工具呼叫的 hook 事件(`PreToolUse`/`preToolUse`,兩者格式相容,可共用同一份 `hooks/hooks.json`);**Codex**(hook 能擋,但每次腳本內容變動都要人工 `/hooks` 重新信任,不算全自動生效)、**Antigravity**(只有社群 issue 佐證、無官方文件)、**OpenCode**(已知 bug:subagent 發出的工具呼叫會繞過 hook)三家都**不掛 hook**,改為完全依賴本規範的規範層自律(讀到 `model:`/`spec-version-guard` frontmatter 或本規範內容時,執行者自行比對版本並中止,而非靠工具強制擋下)。
|
||||
|
||||
### 各助理 hook 真阻擋能力(研究結論,供設計依據)
|
||||
|
||||
| 助理 | 可用 hook 事件 | 是否真中止 | 本規範採用方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| Claude Code | `PreToolUse` | ✅ 真中止(`persona/hooks/guard.mjs` 已驗證) | 掛 hook |
|
||||
| GitHub Copilot CLI | `preToolUse`/`PreToolUse` | ✅ 真中止,且腳本錯誤/逾時預設視為 deny(fail-closed,與本規範精神一致) | 掛 hook |
|
||||
| Codex | 同名事件技術上可掛 | ⚠️ 需人工 `/hooks` 逐次重新信任,不算自動 | 不掛 hook,僅規範層 |
|
||||
| Antigravity | JSON deny 型 hook | ⚠️ 僅社群 issue 佐證,無官方文件 | 不掛 hook,僅規範層 |
|
||||
| OpenCode | `tool.execute.before` | ⚠️ 已知會被 subagent 呼叫繞過 | 不掛 hook,僅規範層 |
|
||||
|
||||
此表為使用者於 2026/08/17 依上述研究結果裁示採用「只對 Claude Code/Copilot 掛 hook,其餘三家靠規範自律」。
|
||||
|
||||
### hook 設計:matcher 與檢查範圍
|
||||
|
||||
Claude Code/Copilot 官方文件都**沒有明確記載** `Skill` 工具呼叫的 `tool_input` 欄位結構、也沒有說明 `PreToolUse` payload 是否帶有「這次呼叫屬於哪個 plugin」的資訊(已實際查證官方文件,查無此欄位說明)。在無法可靠辨識「這次工具呼叫是不是我這個 plugin 自己的 skill」的前提下,本規範採兩個保守決定:
|
||||
|
||||
1. **matcher 沿用 `persona/hooks/hooks.json` 已驗證可真正阻擋的既有 pattern**——`Read|Write|Edit|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash`,而不是賭一個沒有文件佐證的 `Skill` 工具名稱;這樣至少能保證 hook 真的會被觸發,不會因為 matcher 打錯字而變成裝了跟沒裝一樣。
|
||||
2. **腳本一律無條件檢查自己這個 plugin 的版本**,不嘗試判斷「這次工具呼叫是不是我的 skill 觸發的」。四個 JSC plugin 大量互相引用彼此的 `spec-*` 規範,任一個過期都有風險,因此「呼叫任何一個 JSC skill 時,四個 plugin 的 hook 都一起檢查一次自己的版本」是刻意的保守設計,不是失誤。
|
||||
|
||||
這兩個決定都還沒有實機驗證(`Skill` 工具呼叫時 `PreToolUse` 是否真的會帶著這個 matcher 一起觸發、四個 hook 同時觸發會不會互相干擾),留給第 14/23 項的端到端驗證實測確認;若實測發現行為與預期不符,以實測結果為準修正本節,不得憑本節文字繼續假設它一定成立。
|
||||
|
||||
## fail-closed:查不到遠端版本一律阻擋
|
||||
|
||||
- 遠端查詢失敗(逾時、網路錯誤、404、host 不可達)時,**視同版本不符**,一律阻擋,不得因為「連不上網」就放行——寧可誤擋,不可誤放。
|
||||
- 查詢必須設逾時(建議 5 秒),避免拖住工作階段啟動或每次工具呼叫。
|
||||
|
||||
## 錯誤訊息格式
|
||||
|
||||
阻擋時的訊息依 `/jsc-shared:spec-time-log` 的 `[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息` 格式,階段固定為「版本檢查」,等級固定 `ERR`:
|
||||
|
||||
```
|
||||
[yyyy/MM/dd HH:mm:ss][版本檢查][ERR]: <plugin 名> 遠端發佈版本為 <remote-version>,
|
||||
當前實際載入版本為 <loaded-version>(或「查無法取得遠端版本」)。
|
||||
請執行 /jsc-shared:plugins-install 更新後重新開啟工作階段。
|
||||
```
|
||||
|
||||
- 找不到遠端版本時,`<remote-version>` 替換為「查無法取得遠端版本」,訊息其餘部分不變。
|
||||
- 阻擋後一律導向 `/jsc-shared:plugins-install`(一次更新四個 JSC plugin)或 `/jsc-shared:spec-plugin-cli` 對應助理小節的「更新」指令,不得只說「請更新」而不給出具體指令。
|
||||
|
||||
## 適用範圍
|
||||
|
||||
依 `/jsc-shared:spec-preflight`〔載入順序〕,本規範的檢查排在**所有其他 spec 載入之前**執行(見 `spec-preflight` 第 0 步);規範檔本身(`spec-*`)依 `spec-preflight`〔適用範圍〕不引用 preflight,因此也不引用本規範,避免循環依賴。
|
||||
|
||||
**`/jsc-shared:plugins-install` 額外排除,理由是避免自我鎖死**:`plugins-install` 正是 `spec-version-guard` 阻擋後指引使用者執行的修復手段;若 `plugins-install` 自己也引用本規範,遇到 `shared` 版本落後時,它會在修復自己之前就先被自己判定版本不符而擋下——使用者永遠跑不到能修好問題的那個 skill。因此 `plugins-install` 不列入〔給實作端的備註〕所述「六個非 spec skill」的檔頭清單,其餘 skill(含 `plugins-uninstall`)不受此例外影響。
|
||||
|
||||
## 給實作端(腳本、hook)的備註
|
||||
|
||||
本規範只定義「比對什麼、失敗了算什麼、訊息長怎樣」;實際取值、發 HTTP 請求、決定 hook 事件的程式碼是共用腳本 `shared/scripts/version-guard.mjs` 與四個 plugin 各自的 `hooks/hooks.json` 的職責,不在本規範重複描述實作細節。
|
||||
Reference in New Issue
Block a user