發佈 jsc-hooks 0.0.8:技能版本前置檢查 #13
+56
-8
@@ -1,22 +1,70 @@
|
|||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"SessionStart": [
|
"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": [
|
"UserPromptSubmit": [
|
||||||
{ "hooks": [
|
{
|
||||||
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\"" },
|
"hooks": [
|
||||||
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check" }
|
{
|
||||||
] }
|
"type": "command",
|
||||||
|
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"Stop": [
|
"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": [
|
"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": [
|
"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\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,本次技能呼叫已擋下"
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: hooks-install
|
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
|
# 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.
|
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 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.
|
The detailed flow **MUST run as a sub agent**; the main agent only reports the summary.
|
||||||
|
|||||||
Reference in New Issue
Block a user