From 60b6b8fe00f3c1aca742e71613bbbfa56869b06d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 11:52:38 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(version-guard):=20=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=89=8D=E6=AA=A2=E6=9F=A5=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E8=90=BD=E5=BE=8C=E9=81=A0=E7=AB=AF=E5=B0=B1=E6=93=8B?= =?UTF-8?q?=E4=B8=8B=E4=B8=A6=E6=8F=90=E7=A4=BA=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- hooks/hooks.json | 64 +++++++++++++-- hooks/version-guard.sh | 144 ++++++++++++++++++++++++++++++++++ skills/hooks-install/SKILL.md | 5 +- 3 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 hooks/version-guard.sh diff --git a/hooks/hooks.json b/hooks/hooks.json index 11cc543..a3899d7 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,22 +1,70 @@ { "hooks": { "SessionStart": [ - { "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" start" } ] } + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" start" + } + ] + } ], "UserPromptSubmit": [ - { "hooks": [ - { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\"" }, - { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check" } - ] } + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\"" + }, + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check" + } + ] + } ], "Stop": [ - { "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" } ] } + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" + } + ] + } ], "SessionEnd": [ - { "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" } ] } + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/version-guard.sh\"" + } + ] + } ], "PostToolUse": [ - { "matcher": "Skill", "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/skill-usage.sh\"" } ] } + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/skill-usage.sh\"" + } + ] + } ] } } diff --git a/hooks/version-guard.sh b/hooks/version-guard.sh new file mode 100644 index 0000000..9f5dccb --- /dev/null +++ b/hooks/version-guard.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env sh +# version-guard.sh — 技能使用前的版本前置檢查(PreToolUse,matcher: Skill)。 +# +# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。 +# +# 判準與取值: +# - 比對對象是「遠端發佈版本」與「本機**實際載入**的版本」。 +# 實際載入版本要從 installed_plugins.json 的 installPath 讀該版目錄下的 +# plugin.json,不能只看註冊在 installed_plugins.json 的版本欄位——那兩者 +# 可能不同,只看註冊值會放過真正被載入的舊版。 +# - 只擋落後。本機版本等於或超前遠端一律放行:開發技能組時本機本來就會 +# 超前 master,擋下去會讓維護者自己動不了。 +# - 遠端版本查不到(離線、站台維護、repo 改名)一律**擋**(fail-closed), +# 避免「查不到就當作沒事」而讓落後版本靜靜跑下去。逃生門見下。 +# +# 豁免(這些技能永遠放行): +# jsc-cli:deploy 更新整組技能的入口,擋了就沒有任何方法更新,會死鎖 +# jsc-hooks:hooks-install 更新後要重新接線,擋了會讓更新做一半卡住 +# jsc-cli:models SDLC 閘門依賴它產生 model-tags.tsv +# jsc-meta:* 開發技能組本身的工具,擋了就修不了技能組 +# +# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。 +# +# 快取:$JSC_HOME/version-cache/{domain},單行「{版本} {epoch}」, +# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。 +HERE=$(dirname "$0"); . "$HERE/lib.sh" +read_stdin + +[ "${JSC_VERSION_GUARD:-}" = "off" ] && exit 0 + +# 只管 Skill 工具 +tool=$(json_str tool_name) +[ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0 + +skill=$(json_str skill) +[ -n "$skill" ] || exit 0 + +# 只管本技能組(jsc-{domain}:{name}) +case "$skill" in + jsc-*:*) ;; + *) exit 0 ;; +esac + +domain=${skill#jsc-} +domain=${domain%%:*} +[ -n "$domain" ] || exit 0 + +# 豁免清單 +case "$skill" in + jsc-cli:deploy|jsc-hooks:hooks-install|jsc-cli:models|jsc-meta:*) exit 0 ;; +esac + +TTL="${JSC_VERSION_TTL:-600}" +cache_dir="$JSC_HOME/version-cache" +mkdir -p "$cache_dir" 2>/dev/null || true + +deny() { # $1=訊息 + printf '[jsc][版本檢查][ERR]:%s\n' "$1" >&2 + printf '更新指令:claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc\n' "$domain" >&2 + printf '更新整組:/jsc-cli:deploy | 確定要略過檢查:JSC_VERSION_GUARD=off\n' >&2 + exit 2 +} + +# 本機實際載入版本:從 installPath 的 plugin.json 讀,不用註冊欄位 +local_ver=$(python3 - "$domain" <<'PY' 2>/dev/null +import json, os, sys +domain = sys.argv[1] +try: + reg = json.load(open(os.path.expanduser("~/.claude/plugins/installed_plugins.json"))) +except Exception: + sys.exit(0) +entries = reg.get("plugins", {}).get(f"jsc-{domain}@jsc") or [] +for e in entries: + p = os.path.join(e.get("installPath", ""), "plugin.json") + try: + print(json.load(open(p)).get("version", "")); sys.exit(0) + except Exception: + continue +# 讀不到實際檔案時退回註冊版本,並在後面標記為次要來源 +if entries and entries[0].get("version"): + print(entries[0]["version"]) +PY +) +[ -n "$local_ver" ] || deny "讀不到本機 jsc-$domain 的實際載入版本(installed_plugins.json 或該版目錄的 plugin.json 不可用)" + +# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST +remote_src=$(python3 - <<'PY' 2>/dev/null +import json, os, re +try: + d = json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json"))) +except Exception: + raise SystemExit +url = (d.get("jsc", {}).get("source", {}) or {}).get("url", "") +m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$", url) +if m: + print(m.group(1), m.group(2)) +PY +) +host=$(printf '%s' "$remote_src" | cut -d' ' -f1) +owner=$(printf '%s' "$remote_src" | cut -d' ' -f2) +if [ -z "$host" ] || [ -z "$owner" ]; then + host="${GITEA_HOST:-}"; owner="${JSC_GITEA_OWNER:-plugins}" +fi +[ -n "$host" ] || deny "推導不出 Gitea 站台(known_marketplaces.json 無 jsc 來源,GITEA_HOST 也未設定)" + +# 快取 +cache="$cache_dir/$domain" +now=$(now_epoch) +remote_ver="" +if [ -f "$cache" ]; then + c_ver=$(cut -d' ' -f1 "$cache" 2>/dev/null) + c_at=$(cut -d' ' -f2 "$cache" 2>/dev/null) + if [ -n "$c_ver" ] && [ -n "$c_at" ] && [ $((now - c_at)) -lt "$TTL" ]; then + remote_ver="$c_ver" + fi +fi + +if [ -z "$remote_ver" ]; then + url="$host/$owner/$domain/raw/branch/master/plugin.json" + # 暫時性網路失敗不該誤判成版本問題,所以失敗重試一次再放棄 + for _try in 1 2; do + body=$(curl -sS --max-time 10 "$url" 2>/dev/null) && \ + remote_ver=$(printf '%s' "$body" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + [ -n "$remote_ver" ] && break + sleep 1 + done + [ -n "$remote_ver" ] || deny "查不到 jsc-$domain 的遠端發佈版本($url)。無法確認本機是否為最新,依 fail-closed 規則擋下" + printf '%s %s\n' "$remote_ver" "$now" > "$cache" 2>/dev/null || true +fi + +# 語意化比較:只擋「本機 < 遠端」 +cmp=$(awk -v a="$local_ver" -v b="$remote_ver" ' + BEGIN { + n = split(a, x, "."); m = split(b, y, ".") + for (i = 1; i <= 3; i++) { + xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0) + if (xi < yi) { print -1; exit } + if (xi > yi) { print 1; exit } + } + print 0 + }') + +[ "$cmp" = "-1" ] || exit 0 +deny "jsc-$domain 本機版本 $local_ver 落後遠端發佈版本 $remote_ver,本次技能呼叫已擋下" diff --git a/skills/hooks-install/SKILL.md b/skills/hooks-install/SKILL.md index e59f4a7..5b87c09 100644 --- a/skills/hooks-install/SKILL.md +++ b/skills/hooks-install/SKILL.md @@ -1,11 +1,12 @@ --- name: hooks-install -description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate) into every installed AI CLI. Detect CLIs via jsc-cli detect-clis.sh, then apply native hook config, the jsc-wrap.sh launcher, or an instruction-file fallback per CLI. Use after installing or updating the jsc plugin set; not for writing new hooks. +description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard) into every installed AI CLI. Detect CLIs via jsc-cli detect-clis.sh, then apply native hook config, the jsc-wrap.sh launcher, or an instruction-file fallback per CLI. Use after installing or updating the jsc plugin set; not for writing new hooks. --- # hooks-install — wire jsc hooks into every installed CLI -Goal: make the four hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`) effective in every CLI. +Goal: make the five hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`) effective in every CLI. +`version-guard.sh` runs on PreToolUse(Skill) and blocks a skill whose locally loaded plugin version is behind the published one. Where a CLI has no pre-tool hook, that guard cannot be wired — say so in the report rather than implying every CLI is covered. Claude wiring is automatic via `hooks.json`. On codex and kiro the SDLC gate degrades to the skill-step check only; the lock file still works because the SDLC skills call `sdlc-gate.sh lock {stage}` directly — that call is where the capability-tag comparison happens, so the gate keeps its force even where the prompt hook cannot be wired. The gate needs `$JSC_HOME/model-tags.tsv`; when it is missing, report that `jsc-cli:models` (or `jsc-cli/tools/model-tags.sh sync`) must run once, because `sdlc-gate.sh lock` refuses to lock without it. The detailed flow **MUST run as a sub agent**; the main agent only reports the summary. -- 2.53.0 From 922a95f1472ba9a8d749f4c87e59d80fd83f5d00 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 11:52:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(README):=20=E8=A3=9C=E4=B8=8A=20versio?= =?UTF-8?q?n-guard=20=E8=88=87=E5=85=B6=E7=92=B0=E5=A2=83=E8=AE=8A?= =?UTF-8?q?=E6=95=B8=E8=AA=AA=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7c45069..f5fae02 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | --- | --- | --- | | `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) | | `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖;`report` 子指令供 `jsc-log:worklog` 取花費時間 | +| `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令。只擋落後(超前放行,開發技能組時本機本來就會超前);遠端查不到一律擋(fail-closed),逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-cli:models`、`jsc-meta:*` | | `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 | | `hooks/sdlc-gate.sh` | UserPromptSubmit | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門 | @@ -64,6 +65,8 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技 | 變數 | 用途 | 未設定時 | | --- | --- | --- | | `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` | +| `JSC_VERSION_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 | +| `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 | | `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供 | 安靜降級 | | `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh` 比對;優先序在 transcript 實際值與 stdin `model` 之後 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 | -- 2.53.0 From 3b0c8f0e33316d687f00debaa703e15b125fa6ea Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 11:52:38 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(plugin=20=E7=89=88=E6=9C=AC):=20?= =?UTF-8?q?=E4=B8=89=E4=BB=BD=20manifest=20=E5=8D=87=E7=89=88=E8=87=B3=200?= =?UTF-8?q?.0.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e58406d..ec0ecdb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.0.7", + "version": "0.0.8", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index eeff076..e9fdf6b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.0.7", + "version": "0.0.8", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 9084f0a..3490e7c 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.0.7", + "version": "0.0.8", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", "skills": "./skills/" } -- 2.53.0