diff --git a/hooks/hooks.json b/hooks/hooks.json index 2e0e750..a4727ae 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -60,6 +60,10 @@ { "matcher": "Skill", "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/restart-gate.sh\"" + }, { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/version-guard.sh\"" diff --git a/hooks/restart-gate.sh b/hooks/restart-gate.sh new file mode 100755 index 0000000..0172cd0 --- /dev/null +++ b/hooks/restart-gate.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env sh +# restart-gate.sh — 部署後強制重啟閘門(PreToolUse,matcher: Skill)。 +# +# 技能組更新後,正在跑的 CLI 行程載入的還是舊版:SKILL.md、hook 腳本與 tools 都在啟動當下 +# 讀進記憶體。所以部署收尾要求重新啟動,這道閘門負責讓「還沒重啟就繼續用技能」擋在門外。 +# +# 用法: +# restart-gate.sh hook 模式:狀態檔存在就擋下該次技能呼叫(exit 2) +# restart-gate.sh require {模式} [{domain}...] +# 寫入狀態檔,掛上閘門。由 jsc-cli:deploy 在 install 或 +# update 收尾時呼叫;模式為 install 或 update,之後接 +# 這次更新的 domain 清單。 +# exit 0 = 已掛上;exit 2 = 寫不進去(沒寫成等於沒掛)。 +# restart-gate.sh clear 清除狀態檔,放下閘門。由 session-timer.sh 在判定為 +# 新工作階段時呼叫(見下方「清除時機」)。檔案不存在也算成功。 +# restart-gate.sh report 印出狀態檔內容;沒有狀態檔就不印,一律 exit 0。 +# +# require、clear、report 都不讀標準輸入,只有 hook 模式讀。理由與 sdlc-gate.sh 相同: +# read_stdin 在標準輸入是管線又沒人關閉時會一直等,工具端呼叫就整支卡死。新增子命令照這個 +# 原則歸類,工具端呼叫一律再補 /dev/null | head -n1 +} + +case "${1:-}" in + require) + _mode="${2:-update}" + _domains="" + if [ "$#" -gt 2 ]; then shift 2; _domains="$*"; fi + mkdir -p "$JSC_HOME" 2>/dev/null || true + printf 'at=%s\nmode=%s\ndomains=%s\ncli=%s\n' \ + "$(now_iso)" "$_mode" "$_domains" "$(cli_name)" > "$STATE" 2>/dev/null || { + # 寫不進去要講出來:沒寫成就沒有閘門,部署卻以為掛上了。 + printf '[jsc][重啟閘門][ERR]:寫不進 %s,這次部署沒有掛上重啟閘門。\n' "$STATE" >&2 + exit 2 + } + exit 0 ;; + clear) + rm -f "$STATE" 2>/dev/null || true + exit 0 ;; + report) + if [ -f "$STATE" ] && [ -r "$STATE" ]; then cat "$STATE" 2>/dev/null || true; fi + exit 0 ;; + "") ;; # 落到下面的 hook 模式 + *) exit 0 ;; # 不認得的子命令一律安靜放行,不中斷宿主 CLI +esac + +read_stdin + +[ "${JSC_RESTART_GATE:-}" = "off" ] && exit 0 + +# 輸入相容:stdin JSON(Claude 格式)與環境變數(其他四支 CLI 接線時設定)都要收, +# 取法比照 version-guard.sh。工具名取不到就當成沒篩,繼續判技能名。 +tool="${JSC_TOOL_NAME:-${TOOL_NAME:-$(json_str tool_name)}}" +[ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0 + +skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}" +[ -n "$skill" ] || exit 0 + +# 只管本技能組(jsc-{domain}:{name})。別人的技能不受這道閘門影響。 +case "$skill" in + jsc-*:*) ;; + *) exit 0 ;; +esac + +# 豁免清單(理由見檔頭) +case "$skill" in + jsc-cli:deploy|jsc-hooks:hooks-install|jsc-gitea:wiki|jsc-log:worklog|jsc-log:learn|jsc-meta:*|jsc-ask:ask|jsc-git:pr|jsc-git:commit) + exit 0 ;; +esac + +# 狀態檔讀不到就放行:沒有「剛部署過」的證據,就沒有擋人的理由。 +[ -f "$STATE" ] && [ -r "$STATE" ] || exit 0 + +at=$(state_field at) +mode=$(state_field mode) +domains=$(state_field domains) + +# 重啟方式依實際 CLI 給。印別的 CLI 的執行檔名等於沒給指示。 +bin=$(cli_bin "$(cli_name)") +[ "$bin" = unknown ] && bin="目前的 CLI" + +# 訊息裡的部署資訊逐段接起來,缺欄位就少一段,不會留下空括號或多餘的逗號。 +info="" +[ -n "$at" ] && info="$at" +[ -n "$mode" ] && info="${info}${info:+,}模式 $mode" +[ -n "$domains" ] && info="${info}${info:+,}domain:$domains" + +printf '[jsc][重啟閘門][ERR]:技能組已更新%s,%s 還在跑舊版,新版要重新啟動才會載入。本次技能呼叫已擋下。\n' \ + "${info:+($info)}" "$bin" >&2 +printf '重新啟動:結束 %s 再重新開啟一次,狀態檔 %s 會在新工作階段開始時自動清除。\n' \ + "$bin" "$STATE" >&2 +printf '仍可使用:/jsc-cli:deploy、/jsc-hooks:hooks-install、/jsc-gitea:wiki、/jsc-log:worklog、/jsc-log:learn、/jsc-meta:*、/jsc-ask:ask、/jsc-git:pr、/jsc-git:commit(部署後的異動報告與工作日誌要寫得完) | 確定要略過閘門:JSC_RESTART_GATE=off\n' >&2 +exit 2 diff --git a/hooks/session-timer.sh b/hooks/session-timer.sh index 13867c7..5c7895f 100755 --- a/hooks/session-timer.sh +++ b/hooks/session-timer.sh @@ -11,16 +11,32 @@ # SessionStart 會再觸發一次,覆寫起始時間會讓花費時間歸零。 # restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default, # 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。 +# +# 這兩個子命令另外兼一件事:判定為「新的工作階段」時清除部署後的重啟閘門 +# (restart-gate.sh clear)。新工作階段代表 CLI 行程是新起的,新版技能組一定已經載入。 +# 判準只有這裡知道——start 分支的「起始檔不存在」就是這個 session id 第一次開始, +# 所以清除掛在這裡,不在 restart-gate.sh 裡自己再判一次。 HERE=$(dirname "$0"); . "$HERE/lib.sh" read_stdin sid=$(session_id) + +# 放下部署後的重啟閘門。狀態檔的路徑與格式只留在 restart-gate.sh,這裡不碰檔案。 +# 一律 /dev/null || true +} + case "${1:-mark}" in start) f="$JSC_HOME/sessions/$sid.start" - [ -f "$f" ] || now_epoch > "$f" ;; + if [ ! -f "$f" ]; then + now_epoch > "$f" + clear_restart_gate # 起始檔不存在=這個工作階段第一次開始,也就是行程新起的那一次 + fi ;; restart) now_epoch > "$JSC_HOME/sessions/$sid.start" - rm -f "$JSC_HOME/sessions/$sid.end" ;; + rm -f "$JSC_HOME/sessions/$sid.end" + clear_restart_gate ;; # 接不到 session id 的 CLI 每次工作階段開始都算新的,一律清 mark) now_epoch > "$JSC_HOME/sessions/$sid.end" ;; report)