From dbf9aac25ebb7a834833d18a5e070e13413d253b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 16:34:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(restart-gate):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E5=BE=8C=E5=BC=B7=E5=88=B6=E9=87=8D=E5=95=9F?= =?UTF-8?q?=E9=96=98=E9=96=80=EF=BC=8C=E4=B8=A6=E6=8E=A5=E4=B8=8A=E6=96=B0?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E9=9A=8E=E6=AE=B5=E8=87=AA=E5=8B=95=E6=B8=85?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增第八支 hook `hooks/restart-gate.sh`。hook 模式在 `$JSC_HOME/restart-required` 存在時以 exit 2 擋下 jsc 技能呼叫,另有三個子命令:`require {模式} [{domain}...]` 寫入狀態檔掛上閘門、`clear` 清除狀態檔、`report` 印出狀態檔內容。`hooks/hooks.json` 把它接到 PreToolUse(Skill),排在 `version-guard.sh` 前面。`hooks/session-timer.sh` 的 `start` 與 `restart` 在判定為新工作階段時轉呼叫 `clear`。 Why:部署換掉的是磁碟上的技能檔,正在跑的 CLI 行程載入的還是舊版——SKILL.md、hook 腳本與 tools 都在啟動當下讀進記憶體。這段落差期間跑技能,改動看起來沒生效,人會以為部署失敗又重跑一次。所以部署收尾要求重新啟動,這道閘門負責讓「還沒重啟就繼續用技能」擋在門外。 How:判定看的是「檔案在不在」,欄位只用在擋人訊息上——欄位缺了只讓訊息少幾個字,不影響判定。狀態檔用純文字 `key=value`(`at`、`mode`、`domains`、`cli`),格式與工作包狀態檔同一套,`jsc-hooks` 與 `jsc-cli` 兩邊各自實作也對得上。放行原則比照 `version-guard.sh`:只擋確定違規,狀態檔讀不到、技能名取不到、工具名不是 `Skill`、技能不是 `jsc-*:*` 一律 exit 0,沒有證據時擋下等於停掉每一次技能呼叫。九支豁免(`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`)的理由是同一件事:部署後還要寫得完技能組異動報告與工作日誌,整批擋下去「先重啟」與「先寫完報告」會互相打死。後三支自己不是收尾規則的主體,是為了讓前六支走得完才補進來的——清單認的是技能名,不是呼叫鏈。清除交給 `session-timer.sh`,不由本檔自己判:新舊工作階段的判準(`sessions/{sid}.start` 在不在)只有那支腳本知道,兩邊各寫一份就會漂移;續接同一階段走不到那一段,閘門就一路留到真的重新啟動。`require`、`clear`、`report` 都不讀標準輸入,只有 hook 模式讀,工具端呼叫一律再補 `/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)