From fedcfc8b05c5dcf1781c8ecf076cd0a08b861291 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(hooks):=20=E8=A3=9C=E9=BD=8A=E7=A8=BD?= =?UTF-8?q?=E6=A0=B8=E7=BC=BA=E5=A4=B1=E4=B8=A6=E4=BF=AE=E6=8E=89=E8=AD=B7?= =?UTF-8?q?=E6=AC=84=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- hooks/lib.sh | 36 ++++ hooks/sdlc-gate.sh | 14 +- hooks/session-timer.sh | 12 +- hooks/version-guard.sh | 338 ++++++++++++++++++---------------- skills/hooks-install/SKILL.md | 23 +-- tools/jsc-wrap.sh | 6 +- tools/report-error.sh | 136 ++++++++++++++ tools/wire-cli.sh | 239 +++++++++++++++++++----- 8 files changed, 580 insertions(+), 224 deletions(-) mode change 100644 => 100755 hooks/version-guard.sh create mode 100755 tools/report-error.sh diff --git a/hooks/lib.sh b/hooks/lib.sh index 89961d2..0607907 100755 --- a/hooks/lib.sh +++ b/hooks/lib.sh @@ -8,6 +8,10 @@ JSC_HOME="${JSC_HOME:-$HOME/.jsc}" mkdir -p "$JSC_HOME/sessions" "$JSC_HOME/usage" 2>/dev/null || true +# 呼叫端腳本所在目錄。source 不會改變 $0,所以這裡取到的是 hooks/ 或 tools/。 +JSC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd) +JSC_SCRIPT_DIR="${JSC_SCRIPT_DIR:-.}" + # 讀完 stdin(可能為空;非阻塞宿主) read_stdin() { if [ -t 0 ]; then STDIN_JSON=""; else STDIN_JSON=$(cat 2>/dev/null || true); fi @@ -47,5 +51,37 @@ cli_name() { else printf 'unknown'; fi } +# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則), +# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。 +# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。 +jsc_gitea_sh() { + if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then + printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0 + fi + _root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}" + # 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea + for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do + [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } + done + # 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版) + _c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \ + "$_root"/../../gitea/*/tools/gitea.sh \ + "$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \ + | sort | tail -n1) + [ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } + _c=$(command -v gitea.sh 2>/dev/null || true) + [ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; } + return 1 +} + +# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名) +cli_bin() { # $1=CLI 代號 + case "$1" in + antigravity) printf 'agy' ;; + kiro) printf 'kiro-cli' ;; + *) printf '%s' "$1" ;; + esac +} + now_epoch() { date +%s; } now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; } diff --git a/hooks/sdlc-gate.sh b/hooks/sdlc-gate.sh index 1ca02ea..1cd9d52 100755 --- a/hooks/sdlc-gate.sh +++ b/hooks/sdlc-gate.sh @@ -35,15 +35,21 @@ stage_tags() { # $1=階段 awk -F'\t' -v s="$1" '$1 == "stage" && $2 == s { print $3; exit }' "$TAGS_TSV" } -# 某模型的能力標籤。模型鍵與實際 id 雙向包含即視為同一家族 -#(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001);多筆命中取最長鍵。 +# 某模型的能力標籤。查法:先找完全相同的鍵,沒有才退回「表列鍵是實際 id 的前綴」 +# 之中最長的一筆(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001)。 +# 只認前綴這個方向。反向包含(實際 id 是表列鍵的前綴)會讓 gpt-5.x 命中更長的 +# gpt-5.x-mini,最長鍵勝出就把 mini 的標籤發給 gpt-5.x,把夠格的模型擋掉。 model_tags() { # $1=模型 id [ -s "$TAGS_TSV" ] || return 0 awk -F'\t' -v m="$1" ' - $1 == "model" && (index(m, $2) > 0 || index($2, m) > 0) { + $1 == "model" && $2 == m { exact = $3 } + $1 == "model" && $2 != m && substr(m, 1, length($2)) == $2 { if (length($2) > best_len) { best_len = length($2); best = $3 } } - END { if (best != "") print best } + END { + if (exact != "") print exact + else if (best != "") print best + } ' "$TAGS_TSV" } diff --git a/hooks/session-timer.sh b/hooks/session-timer.sh index dc9368f..13867c7 100755 --- a/hooks/session-timer.sh +++ b/hooks/session-timer.sh @@ -1,9 +1,16 @@ #!/usr/bin/env sh # session-timer.sh — 記錄工作階段花費時間(供 jsc-log:worklog 取用)。 # 用法: -# session-timer.sh start # SessionStart:記錄起始時間 +# session-timer.sh start # SessionStart:記錄起始時間(已有紀錄就不動) +# session-timer.sh restart # SessionStart:一律覆寫起始時間 # session-timer.sh mark # Stop/SessionEnd:更新最後活動時間 # session-timer.sh report [sid] # 印出 {sid} {seconds};無紀錄印 0 +# +# start 與 restart 的差別在「同一個 session id 會不會重複開始」: +# start 給 Claude 這種每個工作階段都有自己 session id 的 CLI。續接同一階段時 +# SessionStart 會再觸發一次,覆寫起始時間會讓花費時間歸零。 +# restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default, +# 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。 HERE=$(dirname "$0"); . "$HERE/lib.sh" read_stdin sid=$(session_id) @@ -11,6 +18,9 @@ case "${1:-mark}" in start) f="$JSC_HOME/sessions/$sid.start" [ -f "$f" ] || now_epoch > "$f" ;; + restart) + now_epoch > "$JSC_HOME/sessions/$sid.start" + rm -f "$JSC_HOME/sessions/$sid.end" ;; mark) now_epoch > "$JSC_HOME/sessions/$sid.end" ;; report) diff --git a/hooks/version-guard.sh b/hooks/version-guard.sh old mode 100644 new mode 100755 index 5685de2..48aa6a7 --- a/hooks/version-guard.sh +++ b/hooks/version-guard.sh @@ -3,15 +3,22 @@ # # 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。 # +# 輸入:stdin JSON(Claude 格式)或環境變數,兩者都收。 +# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name +# 技能名 JSC_SKILL、SKILL、stdin 的 skill +# 兩邊都拿不到就安靜降級 exit 0。 +# # 判準與取值: # - 比對對象是「遠端發佈版本」與「本機**實際載入**的版本」。 -# 實際載入版本要從 installed_plugins.json 的 installPath 讀該版目錄下的 -# plugin.json,不能只看註冊在 installed_plugins.json 的版本欄位——那兩者 -# 可能不同,只看註冊值會放過真正被載入的舊版。 +# 實際載入版本只認 installed_plugins.json 的 installPath 底下那份 plugin.json, +# 不看註冊在 installed_plugins.json 的版本欄位——那兩者可能不同,註冊值比較新時 +# 會放過真正被載入的舊版。讀不到那份檔案就當查不到,安靜放行。 # - 只擋落後。本機版本等於或超前遠端一律放行:開發技能組時本機本來就會 -# 超前 master,擋下去會讓維護者自己動不了。 -# - 遠端版本查不到(離線、站台維護、repo 改名)一律**擋**(fail-closed), -# 避免「查不到就當作沒事」而讓落後版本靜靜跑下去。逃生門見下。 +# 超前預設分支,擋下去會讓維護者自己動不了。 +# - **只有「本機落後遠端」會擋**。查不到資料一律放行(exit 0):本機版本、 +# Gitea 站台、遠端版本全部來自 Claude 的 plugin 檔案與 Gitea API,沒裝 +# Claude 或離線的機器一筆都讀不到。那種情況擋下去,等於在沒有任何版本 +# 證據時停掉每一次技能呼叫,護欄變成故障點。 # # 豁免(這些技能永遠放行): # jsc-cli:deploy 更新整組技能的入口,擋了就沒有任何方法更新,會死鎖 @@ -22,84 +29,148 @@ # 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。 # # 快取:$JSC_HOME/version-cache/{domain},單行「{版本} {epoch}」, -# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。 +# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。hook 與 report 共用同一份。 # # 另有一個非 hook 的子指令: # version-guard.sh report 把每個已安裝 jsc-* plugin 的版本比對印成 TSV,每行 # 「{domain}{本機}{遠端}{落後|最新|超前|查詢失敗}」, # 最後一行「behind{落後個數}」。供 jsc-cli:deploy 判斷要不要 # 把「更新」設成推薦選項。report 只讀不擋,永遠 exit 0。 +# 本機沒有 Claude 的 plugin 註冊檔時改印「noregistry{路徑}」 +# 再接 behind 0:那代表這台機器無法做版本檢查,跟「全部最新」是兩件事。 HERE=$(dirname "$0"); . "$HERE/lib.sh" +REG="$HOME/.claude/plugins/installed_plugins.json" +MK="$HOME/.claude/plugins/known_marketplaces.json" + +# 從檔案取 JSON 字串欄位。與 lib.sh 的 json_str 同一種 naive 解析,只是來源是檔案: +# 先把換行換成空白、再以逗號斷行,這樣每行最多一個欄位,取值不會被貪婪比對吃掉。 +file_json_str() { # $1=檔案 $2=欄位名 + [ -f "$1" ] || return 0 + tr '\n' ' ' < "$1" | tr ',' '\n' \ + | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n1 +} + +# 本機實際載入版本:先取該 plugin 的 installPath,再讀那個目錄下的 plugin.json。 +# 只認 installPath 底下那份檔案。註冊在 installed_plugins.json 的 version 欄位不當備援: +# 註冊值可能比實際載入的版本新,拿它來比對會放過真正被載入的舊版,護欄形同虛設。 +# 讀不到那份檔案就當「查不到本機載入版本」,由呼叫端安靜放行。 +local_version() { # $1=domain + [ -f "$REG" ] && [ -r "$REG" ] || return 0 + _seg=$(tr -d '\n' < "$REG" \ + | sed -n "s/.*\"jsc-$1@jsc\"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p") + [ -n "$_seg" ] || return 0 + _path=$(printf '%s' "$_seg" | tr ',' '\n' \ + | sed -n 's/.*"installPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + [ -n "$_path" ] || return 0 + file_json_str "$_path/plugin.json" version +} + +# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST。 +# 印出「{host} {owner}」;推導不出來時 host 為空字串。 +remote_host_owner() { + _url="" + if [ -f "$MK" ]; then + _url=$(tr -d '\n' < "$MK" | sed -n 's/.*"jsc"[[:space:]]*:[[:space:]]*{//p' \ + | tr ',' '\n' \ + | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + fi + _h=$(printf '%s' "$_url" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p') + _o=$(printf '%s' "$_url" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p') + if [ -z "$_h" ] || [ -z "$_o" ]; then + _h="${GITEA_HOST:-}"; _o="${JSC_GITEA_OWNER:-plugins}" + fi + printf '%s %s' "$_h" "$_o" +} + +# 遠端發佈版本:一律經由 jsc-gitea 的 gitea.sh(技能準則),它會帶 GITEA_TOKEN, +# 並在缺 token 或 401/403 時退回 tea 的登入金鑰,私有存取庫才讀得到。 +# 不指定 ref:Gitea 的 raw 端點預設就取該存取庫的預設分支,比在這裡寫死分支名準。 +# 查不到就回傳空字串,由呼叫端放行。 +remote_version() { # $1=domain $2=host $3=owner + _gsh=$(jsc_gitea_sh) || return 0 + _v="" + for _try in 1 2; do # 暫時性網路失敗不該誤判成版本問題,失敗重試一次 + _body=$(GITEA_HOST="$2" sh "$_gsh" api GET "/repos/$3/$1/raw/plugin.json" 2>/dev/null) \ + && _v=$(printf '%s' "$_body" | tr '\n' ' ' | tr ',' '\n' \ + | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + [ -n "$_v" ] && break + sleep 1 + done + printf '%s' "$_v" +} + +TTL="${JSC_VERSION_TTL:-600}" +cache_dir="$JSC_HOME/version-cache" + +# 帶快取的遠端版本查詢。hook 與 report 共用同一份快取與同一個 TTL: +# report 每個 domain 各打一次網路(還帶重試),/jsc-cli:deploy 一跑就是全部 domain, +# 不共用快取等於每次部署都付一輪網路成本。 +cached_remote_version() { # $1=domain $2=host $3=owner + _cache="$cache_dir/$1" + _now=$(now_epoch) + if [ -f "$_cache" ]; then + _cv=$(cut -d' ' -f1 "$_cache" 2>/dev/null) + _ca=$(cut -d' ' -f2 "$_cache" 2>/dev/null) + if [ -n "$_cv" ] && [ -n "$_ca" ] && [ $((_now - _ca)) -lt "$TTL" ]; then + printf '%s' "$_cv"; return 0 + fi + fi + _rv=$(remote_version "$1" "$2" "$3") + if [ -n "$_rv" ]; then + mkdir -p "$cache_dir" 2>/dev/null || true + printf '%s %s\n' "$_rv" "$_now" > "$_cache" 2>/dev/null || true + fi + printf '%s' "$_rv" +} + +# 語意化比較:印出 -1($1 落後)、0(相等)、1($1 超前) +ver_cmp() { # $1=版本 A $2=版本 B + awk -v a="$1" -v b="$2" ' + 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 + }' +} + # ── report:一次比對所有已安裝的 jsc plugin(非 hook 模式,不讀 stdin) if [ "${1:-}" = "report" ]; then - python3 - <<'PY' -import json, os, re, subprocess, sys - -home = os.path.expanduser("~") -try: - reg = json.load(open(f"{home}/.claude/plugins/installed_plugins.json")).get("plugins", {}) -except Exception: - print("behind\t0"); sys.exit(0) - -host = owner = "" -try: - mk = json.load(open(f"{home}/.claude/plugins/known_marketplaces.json")) - m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$", - (mk.get("jsc", {}).get("source", {}) or {}).get("url", "")) - if m: - host, owner = m.group(1), m.group(2) -except Exception: - pass -if not host: - host = os.environ.get("GITEA_HOST", "") - owner = os.environ.get("JSC_GITEA_OWNER", "plugins") - - -def ver(v): - parts = ((v or "").split(".") + ["0", "0", "0"])[:3] - return tuple(int(p) if p.isdigit() else 0 for p in parts) - - -behind = 0 -for key in sorted(reg): - m = re.match(r"^jsc-([^@]+)@", key) - if not m: - continue - domain = m.group(1) - # 本機版本一律以 installPath 底下那份 plugin.json 為準(實際載入版本), - # 讀不到才退回註冊欄位。 - local = "" - for e in reg[key]: - try: - local = json.load(open(os.path.join(e.get("installPath", ""), "plugin.json"))).get("version", "") - break - except Exception: - local = e.get("version", "") - remote = "" - if host: - url = f"{host}/{owner}/{domain}/raw/branch/master/plugin.json" - for _ in range(2): # 暫時性網路失敗不該誤判成版本問題,失敗重試一次 - try: - out = subprocess.run(["curl", "-sS", "--max-time", "10", url], - capture_output=True, text=True, timeout=20).stdout - mm = re.search(r'"version"\s*:\s*"([^"]+)"', out) - if mm: - remote = mm.group(1) - break - except Exception: - pass - if not remote: - state = "查詢失敗" - elif ver(local) < ver(remote): - state = "落後"; behind += 1 - elif ver(local) > ver(remote): - state = "超前" - else: - state = "最新" - print(f"{domain}\t{local or '?'}\t{remote or '?'}\t{state}") -print(f"behind\t{behind}") -PY + # 註冊檔不存在或讀不到就明講。這裡不能只印 behind 0:呼叫端會把它讀成「都是最新」, + # 於是把「這台機器無法做版本檢查」誤報成「不用更新」。 + # 舊版用 `tr -d '\n' < "$REG" 2>/dev/null`,那個 2>/dev/null 只蓋住 tr 的 stderr, + # 蓋不住 shell 開檔失敗的訊息,所以沒裝 Claude 的機器會先漏一行 cannot open。 + if [ ! -f "$REG" ] || [ ! -r "$REG" ]; then + printf 'noregistry\t%s\n' "$REG" + printf 'behind\t0\n' + exit 0 + fi + ho=$(remote_host_owner) + r_host=$(printf '%s' "$ho" | cut -d' ' -f1) + r_owner=$(printf '%s' "$ho" | cut -d' ' -f2) + behind=0 + domains=$(tr -d '\n' < "$REG" | tr ',' '\n' \ + | sed -n 's/.*"jsc-\([a-z0-9][a-z0-9-]*\)@[^"]*"[[:space:]]*:.*/\1/p' | sort -u) + for d in $domains; do + lv=$(local_version "$d") + rv="" + [ -n "$r_host" ] && rv=$(cached_remote_version "$d" "$r_host" "$r_owner") + if [ -z "$rv" ]; then + st="查詢失敗" + else + case "$(ver_cmp "$lv" "$rv")" in + -1) st="落後"; behind=$((behind + 1)) ;; + 1) st="超前" ;; + *) st="最新" ;; + esac + fi + printf '%s\t%s\t%s\t%s\n' "$d" "${lv:-?}" "${rv:-?}" "$st" + done + printf 'behind\t%s\n' "$behind" exit 0 fi @@ -107,11 +178,13 @@ read_stdin [ "${JSC_VERSION_GUARD:-}" = "off" ] && exit 0 -# 只管 Skill 工具 -tool=$(json_str tool_name) +# 輸入相容:stdin JSON(Claude 格式)與環境變數(其他四支 CLI 接線時設定)都要收。 +# 只讀 stdin 的話,用環境變數餵資料的 CLI 一律拿到空值,檢查會整支靜靜放行。 +# 兩者都缺才是真的沒資料,那時照舊安靜降級 exit 0。 +tool="${JSC_TOOL_NAME:-${TOOL_NAME:-$(json_str tool_name)}}" [ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0 -skill=$(json_str skill) +skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}" [ -n "$skill" ] || exit 0 # 只管本技能組(jsc-{domain}:{name}) @@ -129,95 +202,44 @@ 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 +# 更新指令依實際 CLI 給。印別的 CLI 的指令等於沒給指令,使用者照著打只會失敗。 +update_cmd() { # $1=domain + case "$(cli_name)" in + claude) + printf 'claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc' "$1" ;; + codex) + printf 'codex plugin marketplace upgrade jsc' ;; + copilot) + printf 'copilot plugin marketplace update jsc && copilot plugin update jsc-%s@jsc' "$1" ;; + antigravity) + printf 'git -C ~/plugins/%s pull && agy plugin uninstall jsc-%s && agy plugin install ~/plugins/%s' "$1" "$1" "$1" ;; + kiro) + printf 'kiro-cli plugin marketplace update jsc && kiro-cli plugin update jsc-%s@jsc' "$1" ;; + *) + printf '用你的 CLI 的 plugin 更新指令更新 jsc-%s@jsc' "$1" ;; + esac +} 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 '更新指令:%s\n' "$(update_cmd "$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 不可用)" +# 讀不到本機實際載入版本就放行:沒有版本證據時擋下等於停掉每一次技能呼叫 +local_ver=$(local_version "$domain") +[ -n "$local_ver" ] || exit 0 -# 遠端站台與 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 也未設定)" +ho=$(remote_host_owner) +host=$(printf '%s' "$ho" | cut -d' ' -f1) +owner=$(printf '%s' "$ho" | cut -d' ' -f2) +[ -n "$host" ] || exit 0 -# 快取 -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 +# 遠端版本(走 hook 與 report 共用的快取與 TTL) +remote_ver=$(cached_remote_version "$domain" "$host" "$owner") +[ -n "$remote_ver" ] || exit 0 -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 +# 只擋「本機 < 遠端」這一種情況 +[ "$(ver_cmp "$local_ver" "$remote_ver")" = "-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 5b87c09..a2c29b5 100644 --- a/skills/hooks-install/SKILL.md +++ b/skills/hooks-install/SKILL.md @@ -6,28 +6,21 @@ description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SD # hooks-install — wire jsc hooks into every installed 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. +Only claude has both PreToolUse and UserPromptSubmit, so only claude reports `wired`. On codex, copilot, antigravity and kiro the version guard cannot be wired at all and the SDLC gate degrades to the skill-step check, so all four report `degraded` — report that gap as the script words it instead of implying every CLI is covered. +The lock file still works on those four 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. ## Steps 1. Run `jsc-cli/tools/detect-clis.sh`. Done when you hold the list of installed CLIs; when the list is empty, report that and stop. -2. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script performs the config edit, wrapper alias install, or hook file creation for that CLI, and replaces its `` (or `# jsc-hooks`) marker block idempotently — reruns never duplicate content. Read its exit code and first output line (`status=wired|degraded|skipped reason=...`), then confirm against the table below: - -| CLI | Exit / status | Verify | -| --- | --- | --- | -| claude | `status=wired` (exit 0) — hooks.json auto-wires everything, nothing to write | `claude plugin list` shows `jsc-hooks` and `/hooks` shows the registrations | -| codex | `status=degraded` (exit 1) — notify + AGENTS.md prompt fallback | `~/.codex/config.toml` contains the `notify` entry and `AGENTS.md` contains the block | -| copilot | `status=wired` (exit 0) when the CLI is detected, `status=skipped` (exit 3) otherwise | the alias resolves to `jsc-wrap.sh copilot` and `copilot-instructions.md` contains the block | -| antigravity | `status=wired` (exit 0) when the CLI is detected, `status=skipped` (exit 3) otherwise | the alias resolves to `jsc-wrap.sh antigravity` and the rules file contains the block | -| kiro | `status=degraded` (exit 1) | the hook file exists under `.kiro/hooks/` and names the script | - -A row counts as done only when its verify check passes. Exit 2 means bad usage (wrong CLI name), not a wiring outcome. -3. Report the exact `status=` line `tools/wire-cli.sh` printed for each CLI; do not reinterpret or recompute the outcome by hand. Done when every detected CLI has exactly one reported status: wired, degraded, or skipped with a reason. +2. For each installed CLI, run `tools/wire-cli.sh {cli}`. The script owns both the wiring and its verification: it writes the config, alias or hook file inside a `` (or `# jsc-hooks`) marker block, re-reads every file it wrote, and confirms the block is present and correctly placed before it prints a success status. Trust its first line, `status=wired|degraded|skipped|failed reason=...`. Exit 2 means a bad CLI name, not a wiring outcome — fix the name and rerun. Done when every installed CLI has printed exactly one `status=` line and none exited 2. +3. For each CLI whose status is `failed`, record it: run `tools/report-error.sh --hook wire-cli.sh --exit 4 --summary "{the reason field}" --cli {cli}` and feed the script's `[jsc]` output in on stdin. Done when each `failed` CLI has either an `ERROR_{HASH}` page name on stdout, or an empty exit 0 meaning `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset — in that second case carry the reason into step 4 instead. Skip this step when no CLI reported `failed`. +4. Report the exact `status=` line `tools/wire-cli.sh` printed for each CLI, plus the `ERROR_{HASH}` page for any `failed` one. Done when every detected CLI has exactly one reported status: wired, degraded, skipped or failed, each with its reason. ## Notes -- The hook scripts accept both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `JSC_MODEL`); `jsc-wrap.sh` sets the first two itself. +- Every hook script accepts both stdin JSON and environment variables (`JSC_CLI`, `JSC_SESSION_ID`, `JSC_SKILL`, `JSC_TOOL_NAME`, `JSC_MODEL`); `jsc-wrap.sh` sets the first two itself. +- `session-timer.sh` takes `start` (keep an existing start time), `restart` (always overwrite it, for a CLI with no session id — kiro), `mark` and `report`. `wire-cli.sh` picks the right one per CLI; do not hand-edit the generated hook files. +- `tools/report-error.sh` is operator- or skill-invoked only. Never wire it to fire from a failing hook: hooks stay silent and exit 0, and a failing hook that reports itself can loop. - Data lands in `$JSC_HOME` (default `~/.jsc`), consumed by `jsc-log:worklog` and `jsc-log:stats`. diff --git a/tools/jsc-wrap.sh b/tools/jsc-wrap.sh index 88bcf6b..cc3adf6 100755 --- a/tools/jsc-wrap.sh +++ b/tools/jsc-wrap.sh @@ -3,20 +3,24 @@ # 用法: jsc-wrap.sh {cli} [args...] # 行為: 匯出 JSC_CLI 與 JSC_SESSION_ID → session-timer start → 執行 CLI → # 結束後 session-timer mark 並以 scan-logs.sh 回填用量,最後回傳 CLI 的結束碼。 +# 注意: JSC_CLI 存的是 CLI 代號(antigravity、kiro),實際執行的是 cli_bin 對應的 +# 執行檔(agy、kiro-cli)。直接拿代號當指令跑會 127,因為沒有這兩個執行檔。 HERE=$(cd "$(dirname "$0")" && pwd) HOOKS="$HERE/../hooks" +. "$HOOKS/lib.sh" cli="${1:-}" if [ -z "$cli" ]; then echo "用法:jsc-wrap.sh {cli} [args...]" >&2 exit 2 fi shift +bin=$(cli_bin "$cli") JSC_CLI="$cli" # 未提供 session id 就自動產生({cli}-時間戳-PID),讓計時與用量共用同一個 session [ -n "${JSC_SESSION_ID:-}" ] || JSC_SESSION_ID="$cli-$(date +%Y%m%d%H%M%S)-$$" export JSC_CLI JSC_SESSION_ID sh "$HOOKS/session-timer.sh" start &2 + exit 2 +fi + +gsh=$(jsc_gitea_sh) || exit 0 +wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0 +[ -n "$wrepo" ] || exit 0 + +# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫) +if [ -z "$repo" ]; then + origin=$(git config --get remote.origin.url 2>/dev/null || true) + repo=$(printf '%s' "$origin" \ + | sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//') +fi +[ -n "$repo" ] || repo="-" +[ -n "$cli" ] || cli=$(cli_name) +[ -n "$session" ] || session=$(session_id) +[ -n "$code" ] || code="-" +[ -n "$source_kind" ] || source_kind="command" +[ -n "$symptom" ] || symptom="$summary" +[ -n "$cause" ] || cause="待查" +[ -n "$action" ] || action="待處理" + +ts=$(date +'%Y-%m-%d %H:%M:%S') +ticket_ts=$(date +'%Y%m%d_%H%M%S') +if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi +[ -n "$detail" ] || detail="(無)" +# 摘要與相關輸出都落在 markdown 表格欄位裡,半形 | 會把欄位切斷,改成全形 +detail=$(printf '%s' "$detail" | sed 's/|/|/g') +summary=$(printf '%s' "$summary" | sed 's/|/|/g') + +hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0 +[ -n "$hash" ] || exit 0 +page="ERROR_$hash" + +# sed 取代值要先轉義:& 與分隔字元 | 會被 sed 當語法,換行會整行斷掉 +esc() { printf '%s' "$1" | tr '\n' ' ' | sed 's/[\\&|]/\\&/g'; } +fill() { sed "s|$1|$(esc "$2")|g"; } + +tmp_page=$(mktemp) || exit 0 +tmp_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; } +trap 'rm -f "$tmp_page" "$tmp_list"' EXIT + +fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \ + | fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \ + | fill '{owner}/{repo}' "$repo" \ + | fill '{cli}' "$cli" \ + | fill '{session_id}' "$session" \ + | fill '{hook_name}' "$hook" \ + | fill '{exit_code}' "$code" \ + | fill '{error_summary}' "$summary" \ + | fill '{現象描述}' "$symptom" \ + | fill '{可能原因}' "$cause" \ + | fill '{處理方式}' "$action" \ + | fill '{stdin / env / command}' "$source_kind" \ + | fill '{stdout / stderr 摘要}' "$detail" \ + | fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page" + +row=$(printf '| %s | [[%s|%s]] | %s | %s | %s | %s |' \ + "$ts" "$hook 異常 $ts" "$page" "$repo" "$hook" "$code" "$summary") + +# 目錄頁:已存在就把新列附在文末(最新一筆在最後);不存在就用範本建立 +if sh "$gsh" wiki-get "$wrepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null \ + && [ -s "$tmp_list" ]; then + printf '%s\n' "$row" >> "$tmp_list" +else + fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \ + | fill '{HASH}' "$hash" \ + | fill '{error title}' "$hook 異常 $ts" \ + | fill '{owner}/{repo}' "$repo" \ + | fill '{hook_name}' "$hook" \ + | fill '{exit_code}' "$code" \ + | fill '{error_summary}' "$summary" > "$tmp_list" +fi + +if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then + echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2 + exit 4 +fi +if ! sh "$gsh" wiki-put "$wrepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then + echo "[jsc] 寫入 ERROR_CONTENTS 失敗($wrepo),$page 已建立。" >&2 + exit 4 +fi + +url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null || true) +printf '%s %s\n' "$page" "$url" diff --git a/tools/wire-cli.sh b/tools/wire-cli.sh index 22f9ae1..f78e38f 100755 --- a/tools/wire-cli.sh +++ b/tools/wire-cli.sh @@ -1,24 +1,39 @@ #!/usr/bin/env sh -# wire-cli.sh — 把 jsc 四支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。 +# wire-cli.sh — 把 jsc 五支 hook 接線到單一 CLI(供 hooks-install 技能呼叫)。 # 用法: wire-cli.sh {claude|codex|copilot|antigravity|kiro} # 行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加): -# claude — 什麼都不用寫,hooks.json 已自動接線四支 hook -# codex — 在 config.toml 設 notify(呼叫 session-timer.sh mark,JSC_CLI=codex); +# claude — 什麼都不用寫,hooks.json 已自動接線五支 hook +# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時); +# 在 config.toml 設 notify(每輪補 session-timer.sh start 再 mark,JSC_CLI=codex); # 在 AGENTS.md 附加 STE100 規則段落(prompt 降級) # copilot — 在 shell rc 檔加上 copilot 別名,轉呼叫 tools/jsc-wrap.sh copilot; # 在 copilot-instructions.md 附加 STE100 規則段落 # antigravity — 在 shell rc 檔加上 agy 別名,轉呼叫 tools/jsc-wrap.sh antigravity; # 在全域規則檔附加 STE100 規則段落 -# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(JSC_CLI=kiro) +# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(每輪 mark 加 STE100) +# 與 jsc-hooks-session-start.json(sessionStart 開始計時),皆帶 JSC_CLI=kiro # -# 輸出: 第一行固定為 `status={wired|degraded|skipped} reason=...`(可供程式判讀), +# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護: +# claude 五支 hook 全接,回報 wired +# codex、copilot、antigravity、kiro 只有別名或規則檔,接不上 PreToolUse 與 +# UserPromptSubmit,版本前置檢查與 SDLC 模型鎖 +# 都沒接上,一律回報 degraded 並在 reason 講明 +# +# 寫入後自我驗證,通過才回報成功:每個寫過的檔案重新讀一次,確認標記段落存在且落在 +# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對 +# 且 on、run 在最上層)。腳本說寫好了卻寫錯位置,是最難查的失敗,所以驗證放在腳本裡。 +# +# 輸出: 第一行固定為 `status={wired|degraded|skipped|failed} reason=...`(可供程式判讀), # 其後為人類可讀的繁中說明。 # 結束碼: 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查) # 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過) +# 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報) set -u HERE=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$HERE/.." && pwd) HOOKS="$ROOT/hooks" +# cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份 +. "$HOOKS/lib.sh" cli="${1:-}" case "$cli" in @@ -28,25 +43,20 @@ case "$cli" in exit 2 ;; esac -# 每個 CLI 對應的實際執行檔名稱(antigravity 的執行檔是 agy,其餘與 CLI 代號同名) -cli_bin() { - case "$1" in - antigravity) printf 'agy' ;; - kiro) printf 'kiro-cli' ;; - *) printf '%s' "$1" ;; - esac -} - # STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出 ste100_text() { sh "$HOOKS/ste100-guard.sh" 2>/dev/null | sed '/^exit /d'; } # 以標記整段取代(冪等);標記不存在就在檔尾新增;檔案不存在就建立。 +# 適用 markdown 規則檔與 shell rc 檔:這兩種檔案沒有「區段」概念,附在檔尾就對了。 # $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容 replace_block() { file="$1"; bopen="$2"; bshut="$3"; content="$4" dir=$(dirname "$file") mkdir -p "$dir" 2>/dev/null || return 1 touch "$file" 2>/dev/null || return 1 + # touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗, + # 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。 + [ -f "$file" ] || return 1 block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut") if grep -qF "$bopen" "$file" 2>/dev/null; then awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" ' @@ -54,14 +64,85 @@ replace_block() { $0==bshut { skip=0; next } skip { next } { print } - ' "$file" > "$file.jsc-tmp" 2>/dev/null && mv "$file.jsc-tmp" "$file" + ' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } + mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } else - printf '\n%s\n' "$block" >> "$file" + # 包一層子 shell 才蓋得住 shell 自己的開檔失敗訊息(>> 失敗時那行不走命令的 stderr) + ( printf '\n%s\n' "$block" >> "$file" ) 2>/dev/null || return 1 fi } +# TOML 版的整段取代:標記段落一律放在第一個表頭(`[table]`、`[[array]]`)之前。 +# TOML 的根層鍵只在第一個表頭之前有效,附在檔尾會被歸進最後那張表——檔案照樣解析 +# 得過,codex 卻永遠讀不到 notify,hook 靜靜失效。所以位置本身就是正確性的一部分。 +# 舊版寫錯位置的段落也會被這支函式移到正確位置(先整段刪除,再插到表頭之前)。 +# $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容 +replace_block_toml() { + file="$1"; bopen="$2"; bshut="$3"; content="$4" + dir=$(dirname "$file") + mkdir -p "$dir" 2>/dev/null || return 1 + touch "$file" 2>/dev/null || return 1 + # touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗, + # 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。 + [ -f "$file" ] || return 1 + block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut") + awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" ' + $0==bopen { skip=1; next } + $0==bshut { skip=0; next } + skip { next } + # 表頭樣式:整行只有 [name] 或 [[name]]。多行陣列裡的 [1, 2], 不會命中。 + !done && /^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { print block; print ""; done=1 } + { print } + END { if (!done) print block } + ' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } + mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } +} + +# 驗證:檔案裡有這段標記嗎($1=檔案 $2=開頭標記行) +has_block() { grep -qF "$2" "$1" 2>/dev/null; } + +# 驗證:TOML 的某個鍵是不是落在根層(第一個表頭之前)。$1=檔案 $2=鍵名 +toml_root_key() { + awk -v k="$2" ' + /^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { intable=1; next } + !intable && $0 ~ "^[ \t]*" k "[ \t]*=" { found=1 } + END { exit(found ? 0 : 1) } + ' "$1" 2>/dev/null +} + +# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名)。 +# 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。 +json_top_key() { + awk -v k="$2" ' + { s = s $0 "\n" } + END { + n = length(s); depth = 0; i = 1; found = 0; bad = 0 + while (i <= n) { + c = substr(s, i, 1) + if (c == "\"") { + buf = ""; i++; closed = 0 + while (i <= n) { + c = substr(s, i, 1) + if (c == "\\") { i += 2; continue } + if (c == "\"") { i++; closed = 1; break } + buf = buf c; i++ + } + if (!closed) { bad = 1; break } + j = i + while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++ + if (substr(s, j, 1) == ":" && depth == 1 && buf == k) found = 1 + continue + } + if (c == "{" || c == "[") depth++ + else if (c == "}" || c == "]") { depth--; if (depth < 0) { bad = 1; break } } + i++ + } + exit((found && !bad && depth == 0) ? 0 : 1) + }' "$1" 2>/dev/null +} + # 找出可寫入別名的 shell rc 檔;都不存在就以 ~/.bashrc 為預設(自動建立)。 -# 印出找到/建立的 rc 檔路徑,一行一個。 +# 印出找到或建立的 rc 檔路徑,一行一個。 rc_files() { found="" for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do @@ -70,32 +151,74 @@ rc_files() { [ -n "$found" ] || printf '%s\n' "$HOME/.bashrc" } +# 把別名寫進每個 rc 檔並逐檔驗證。$1=標記名(不含 # 與 /)$2=別名內容 +# 迴圈不可以放在管線右邊:那會變成子 shell,寫入失敗的旗標傳不回來, +# 明明沒寫成功也照樣回報 wired。改成從暫存檔讀,迴圈就留在本 shell。 +write_alias_rc() { + _mark="$1"; _line="$2"; _ok=1 + _list=$(mktemp) || return 1 + rc_files > "$_list" || { rm -f "$_list"; return 1; } + while IFS= read -r rc; do + [ -n "$rc" ] || continue + replace_block "$rc" "# $_mark" "# /$_mark" "$_line" || { _ok=0; continue; } + grep -qF "$_line" "$rc" 2>/dev/null || _ok=0 + done < "$_list" + rm -f "$_list" + [ "$_ok" = 1 ] +} + skip() { # $1=reason printf 'status=skipped reason=%s\n' "$1" echo "[jsc] 略過:$1" exit 3 } +fail() { # $1=reason + printf 'status=failed reason=%s\n' "$1" + echo "[jsc] 接線沒生效:$1" >&2 + echo "[jsc] 請以 tools/report-error.sh 回報這次失敗,再修好原因重跑本腳本。" >&2 + exit 4 +} + case "$cli" in claude) + bin=$(cli_bin claude) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 claude 執行檔" + [ -f "$HOOKS/hooks.json" ] || fail "找不到 $HOOKS/hooks.json,claude 接不到任何 hook" printf 'status=wired reason=%s\n' "hooks.json 自動接線" - echo "[jsc] claude:由 hooks/hooks.json 自動接線全部四支 hook,無需寫入設定。" + echo "[jsc] claude:由 hooks/hooks.json 自動接線全部五支 hook,無需寫入設定。" exit 0 ;; codex) - command -v "$(cli_bin codex)" >/dev/null 2>&1 || skip "未偵測到 codex 執行檔" + bin=$(cli_bin codex) + command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 codex 執行檔" CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" config="$CODEX_HOME/config.toml" agents="$CODEX_HOME/AGENTS.md" - notify_line="notify = [\"env\", \"JSC_CLI=codex\", \"sh\", \"$HOOKS/session-timer.sh\", \"mark\"]" - replace_block "$config" "# jsc-hooks" "# /jsc-hooks" "$notify_line" \ - || skip "無法寫入 $config" + # codex 的 notify 只在每一輪結束時觸發,沒有工作階段開始事件,所以計時分兩段接: + # 1. shell 別名走 jsc-wrap.sh:啟動當下就 session-timer start,並給這次工作階段 + # 一個 JSC_SESSION_ID,codex 內觸發的 notify 會沿用同一個 id。 + # 2. notify 每輪先補 start 再 mark。start 已有紀錄就不動,所以沒走別名啟動時 + # 仍拿得到起始時間(從第一輪算起)。少了這一段,worklog 只會拿到 0 秒。 + timer="sh '$HOOKS/session-timer.sh'" + notify_line="notify = [\"env\", \"JSC_CLI=codex\", \"sh\", \"-c\", \"$timer start " "" "$(ste100_text)" \ - || skip "無法寫入 $agents" - printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查" - echo "[jsc] codex:已設定 $config 的 notify 呼叫 session-timer.sh mark。" + || fail "無法寫入 $agents" + has_block "$agents" "" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落" + printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查" + echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。" + echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。" + echo "[jsc] codex:已設定 $config 的 notify(根層鍵,已驗證),每輪補 session-timer.sh start 再 mark。" echo "[jsc] codex:已在 $agents 寫入 STE100 規則段落(prompt 降級)。" echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" + echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" exit 1 ;; copilot) @@ -103,47 +226,73 @@ case "$cli" in command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔" instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" copilot'" - rc_files | while IFS= read -r rc; do - replace_block "$rc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" "$alias_line" - done + write_alias_rc "jsc-hooks:copilot" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔" replace_block "$instr" "" "" "$(ste100_text)" \ - || skip "無法寫入 $instr" - printf 'status=wired reason=%s\n' "wrapper 別名 + 規則檔已接線" + || fail "無法寫入 $instr" + has_block "$instr" "" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落" + printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查" echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。" - echo "[jsc] copilot:已在 $instr 寫入 STE100 規則段落。" - exit 0 ;; + echo "[jsc] copilot:已在 $instr 寫入 STE100 規則段落(prompt 降級)。" + echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才生效。" + echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" + echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" + exit 1 ;; antigravity) bin=$(cli_bin antigravity) command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 antigravity(agy)執行檔" rules="${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" antigravity'" - rc_files | while IFS= read -r rc; do - replace_block "$rc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" "$alias_line" - done + write_alias_rc "jsc-hooks:antigravity" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔" replace_block "$rules" "" "" "$(ste100_text)" \ - || skip "無法寫入 $rules" - printf 'status=wired reason=%s\n' "wrapper 別名 + 規則檔已接線" + || fail "無法寫入 $rules" + has_block "$rules" "" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落" + printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查" echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。" - echo "[jsc] antigravity:已在 $rules 寫入 STE100 規則段落。" - exit 0 ;; + echo "[jsc] antigravity:已在 $rules 寫入 STE100 規則段落(prompt 降級)。" + echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才生效。" + echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" + echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" + exit 1 ;; kiro) command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔" hookdir="./.kiro/hooks" hookfile="$hookdir/jsc-hooks.json" - mkdir -p "$hookdir" 2>/dev/null || skip "無法建立 $hookdir" - cat > "$hookfile" 2>/dev/null </dev/null || fail "無法建立 $hookdir" + # 計時要分兩個檔:kiro 的一個 hook 檔只有一組 run,所有事件共用。 + # sessionStart 單獨一檔跑 restart,才算得出這一次工作階段的花費時間; + # kiro 給不到 session id,紀錄共用 default,不覆寫起始時間就會把上一階段算進來。 + cat > "$startfile" 2>/dev/null < "$hookfile" 2>/dev/null </dev/null || fail "$f 缺少 JSC_CLI=kiro" + grep -qF 'session-timer.sh' "$f" 2>/dev/null || fail "$f 的 run 沒有接到 session-timer.sh" + done + grep -qF '"sessionStart"' "$startfile" 2>/dev/null || fail "$startfile 沒有接在 sessionStart" + grep -qF '"userPromptSubmit"' "$hookfile" 2>/dev/null || fail "$hookfile 沒有接在 userPromptSubmit" + printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查" + echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。" echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" + echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" exit 1 ;; esac -- 2.53.0 From 83170e1b4362223167e2e1015b1e0a4015bdf82a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(hooks):=20=E5=90=8C=E6=AD=A5=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=88=87=E5=8F=83=E8=80=83=E8=B3=87=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:更新 README、AGENTS.md、templates 與 references,讓文件敘述與實際行為一致。 Why:稽核發現多處文件與程式行為分歧,違反「每個意義只有單一真實來源」。 How:以實際程式行為為準改寫敘述,重複的規則收成單一來源並以一行指引指過去。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- AGENTS.md | 2 +- README.md | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5196855..e627860 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # jsc-hooks — 給 AI 助理的指引 -本 repo 是 jsc 技能組的 `hooks` domain(跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。 +本 repo 是 jsc 技能組的 `hooks` domain(跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查),可同時被 Claude Code / Codex / Copilot / Antigravity / Kiro 使用。 ## 規則 diff --git a/README.md b/README.md index ebd86ae..949d4d9 100644 --- a/README.md +++ b/README.md @@ -23,29 +23,35 @@ 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/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間 | +| `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給)。只擋落後這一種情況:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本也一律放行。逃生門 `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` 為逃生門 | -Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。 +Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。 + +> 覆蓋範圍要據實看待:只有 claude 同時有 PreToolUse 與 UserPromptSubmit,五支 hook 全接得上,回報 `wired`。codex、copilot、antigravity、kiro 都沒有 pre-tool hook,接不上 `version-guard.sh` 的版本前置檢查,SDLC 模型鎖也只剩技能步驟檢查,這四個 CLI 一律回報 `degraded`,靠 `/jsc-cli:deploy` 定期更新。codex 另外沒有工作階段開始事件,計時改由 `tools/jsc-wrap.sh` 的 `codex` 別名在啟動當下開始;沒走別名啟動時,時間從第一輪回應算起。 > `version-guard.sh report` 是非 hook 的子指令:印出每個已安裝 jsc plugin 的 > 「{domain} {本機} {遠端} {落後|最新|超前|查詢失敗}」,最後一行 `behind {落後個數}`。 +> 本機沒有 Claude 的 plugin 註冊檔時改印 `noregistry {路徑}` 再接 `behind 0`, +> 代表這台機器無法做版本檢查,跟「全部最新」是兩件事。查遠端版本走與 hook 同一份快取 +> 與同一個 `JSC_VERSION_TTL`,一次部署不會為每個 domain 各打一輪網路。 > `jsc-cli:deploy` 用它決定要不要把「更新」設成推薦選項。 ## 工具 | 腳本 | 用途 | | --- | --- | -| `tools/jsc-wrap.sh` | 無 hook 系統 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填 | +| `tools/jsc-wrap.sh` | 沒有完整 hook 系統的 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填。`JSC_CLI` 存 CLI 代號,實際執行的是對應的執行檔(antigravity 是 agy、kiro 是 kiro-cli) | | `tools/scan-logs.sh` | 離線回填:解析 copilot、antigravity、codex 的原生日誌,把技能用量與階段界線補進 `$JSC_HOME`,重掃不重複 | -| `tools/wire-cli.sh` | 單一 CLI 的接線流程:`{cli}` 對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段取代,重跑不重複;以 `status=wired\|degraded\|skipped` 回報結果 | +| `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。wiki 位置由 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR` 解析,解析不出來就安靜降級。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) | +| `tools/wire-cli.sh` | 單一 CLI 的接線流程:`{cli}` 對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段取代,重跑不重複。寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層);以 `status=wired\|degraded\|skipped\|failed` 回報結果 | ## 失敗回報範本 -這兩個模板給外層的 hook 失敗回報流程使用,不改動現有 hook 行為。 -失敗時若要寫入 wiki,套用這兩個檔案即可。 +這兩個模板是失敗回報頁的文案來源,由 `tools/report-error.sh` 填欄位後寫進 wiki。 +用法:`tools/report-error.sh --hook {名稱} --exit {碼} --summary {摘要}`,錯誤輸出摘要走標準輸入。 | 範本 | 用途 | | --- | --- | @@ -60,7 +66,7 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技 ### `hooks-install` -把四支 hook 接線到所有已安裝的 CLI:偵測 CLI 後,逐一呼叫 `tools/wire-cli.sh {cli}` 完成接線(claude 由 `hooks.json` 自動接線,無需寫入)。copilot、antigravity 由該腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍追加到各自的規則檔(以 `` 標記整段取代,不重複追加)。codex、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入。腳本以 `status=wired|degraded|skipped` 回報結果,供技能對照 verify 表。 +把五支 hook 接線到所有已安裝的 CLI:偵測 CLI 後,逐一呼叫 `tools/wire-cli.sh {cli}` 完成接線(claude 由 `hooks.json` 自動接線,無需寫入)。codex、copilot、antigravity 由該腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍追加到各自的規則檔(以 `` 標記整段取代,不重複追加)。codex、copilot、antigravity、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入;這四個 CLI 沒有 pre-tool hook,版本前置檢查接不上,腳本會在 `reason` 裡講明,只有 claude 回報 `wired`。腳本第一行以 `status=wired|degraded|skipped|failed reason=...` 回報結果;`failed` 由技能改呼叫 `tools/report-error.sh` 寫一頁 `ERROR_{HASH}`。 @@ -69,9 +75,11 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技 | 變數 | 用途 | 未設定時 | | --- | --- | --- | | `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` | +| `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` | +| `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | `tools/report-error.sh` 安靜降級,不寫 wiki | | `JSC_VERSION_GUARD` | 設 `off` 完全略過版本前置檢查(離線工作用) | 啟用檢查 | | `JSC_VERSION_TTL` | 遠端版本查詢的快取秒數 | 預設 600 | -| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供 | 安靜降級 | +| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` / `JSC_TOOL_NAME` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供,代替 stdin JSON 的 `session_id`、`skill`、`tool_name`(`version-guard.sh` 也收沒有前綴的 `SKILL`、`TOOL_NAME`) | 安靜降級 | | `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh` 比對;優先序在 transcript 實際值與 stdin `model` 之後 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 | ## 相關 domain -- 2.53.0 From 2982ca4e9f8fbd0c229601e61a489b1bc4b8357f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(hooks):=20=E4=B8=89=E4=BB=BD=20manife?= =?UTF-8?q?st=20=E5=90=8C=E6=AD=A5=E5=8D=87=E7=89=88=E4=B8=A6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20marketplace=20=E6=AD=A3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:三份 plugin manifest 版本同步 bump,兩份 marketplace 檔與 plugins/meta 正本對齊。 Why:準則要求技能異動必須同步升版;marketplace 副本必須與正本完全一致。 How:以 jsc-meta 的 tools/sync-skill-manifest.sh 升版,marketplace 檔由正本複製。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- .agents/plugins/marketplace.json | 6 +++--- .claude-plugin/marketplace.json | 6 +++--- .claude-plugin/plugin.json | 4 ++-- .codex-plugin/plugin.json | 4 ++-- plugin.json | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index a9b4e66..8a2f91b 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -43,7 +43,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/hooks.git" }, - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄" + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查" }, { "name": "jsc-log", @@ -75,7 +75,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/review.git" }, - "description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組" + "description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組" }, { "name": "jsc-sdlc", @@ -83,7 +83,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/sdlc.git" }, - "description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)" + "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a9b4e66..8a2f91b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -43,7 +43,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/hooks.git" }, - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄" + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查" }, { "name": "jsc-log", @@ -75,7 +75,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/review.git" }, - "description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組" + "description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組" }, { "name": "jsc-sdlc", @@ -83,7 +83,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/sdlc.git" }, - "description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)" + "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index adb53bd..57ff1c7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jsc-hooks", - "version": "0.0.9", - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", + "version": "0.0.11", + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills", "author": { "name": "JSC" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 6bcf27b..fe46539 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.0.9", - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", + "version": "0.0.11", + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 780ac3c..0ddeef3 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.0.9", - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖", + "version": "0.0.11", + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills/" } -- 2.53.0