release: v0.2.6 develop 到 master #38

Merged
admin merged 5 commits from develop into master 2026-08-27 10:58:03 +00:00
2 changed files with 123 additions and 33 deletions
Showing only changes of commit 7517566156 - Show all commits
+119 -32
View File
@@ -5,15 +5,19 @@
# 讀進記憶體。所以部署收尾要求重新啟動,這道閘門負責讓「還沒重啟就繼續用技能」擋在門外。
#
# 用法:
# restart-gate.sh hook 模式:狀態檔存在就擋下該次技能呼叫(exit 2)
# restart-gate.sh hook 模式:當前 CLI 那份狀態檔存在就擋下該次技能
# 呼叫(exit 2)。別支 CLI 那幾份不看。
# 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。
# 寫入當前 CLI 那份狀態檔,掛上這一支的閘門。由
# jsc-cli:deploy 在 install 或 update 收尾時呼叫;模式為
# install 或 update,之後接這次更新的 domain 清單。
# exit 0 = 已掛上;exit 2 = 取不到 CLI 代號或寫不進去
# (兩種都等於沒掛上)。
# restart-gate.sh clear 只清除當前 CLI 那份狀態檔,放下這一支的閘門。由
# session-timer.sh 在判定為新工作階段時呼叫(見下方
# 「清除時機」)。檔案不存在也算成功。
# restart-gate.sh report 印出每一份狀態檔的內容,一支 CLI 一行(格式見下方
# 「report 輸出格式」);一份都沒有就不印,一律 exit 0。
#
# require、clear、report 都不讀標準輸入,只有 hook 模式讀。理由與 sdlc-gate.sh 相同:
# read_stdin 在標準輸入是管線又沒人關閉時會一直等,工具端呼叫就整支卡死。新增子命令照這個
@@ -21,14 +25,42 @@
#
# --- 狀態檔格式 ---
#
# $JSC_HOME/restart-required(JSC_HOME 未設定時為 ~/.jsc),純文字 key=value,一行一欄位,
# 順序不拘,不認得的鍵一律忽略。格式壓到最簡,jsc-hooks 與 jsc-cli 兩邊各自實作也對得上。
# $JSC_HOME/restart-required.d/{CLI 代號}(JSC_HOME 未設定時為 ~/.jsc),一支 CLI 一份,
# 檔名就是 CLI 代號(claude、codex、copilot、antigravity、kiro)。內容為純文字 key=value,
# 一行一欄位,順序不拘,不認得的鍵一律忽略。格式壓到最簡,jsc-hooks 與 jsc-cli 兩邊各自
# 實作也對得上。
# at={ISO 時間} 部署收尾時間,UTC
# mode={install|update} 這次部署的模式
# domains={domain 清單} 這次更新到的 domain,空白分隔
# cli={CLI 代號} 執行部署的 CLI
# 欄位只用在擋人訊息上。判定看的是「檔案在不在」——檔案存在就是還沒重啟過的證據,欄位缺了
# 只讓訊息少幾個字,不影響判定。
# cli={CLI 代號} 執行部署的 CLI,與檔名相同
# 欄位只用在擋人訊息上。判定看的是「當前 CLI 那份檔案在不在」——檔案存在就是這一支還沒重啟
# 過的證據,欄位缺了只讓訊息少幾個字,不影響判定。
#
# 為什麼一支 CLI 一份:一台機器上五支 CLI 各自是獨立行程,各自載入自己記憶體裡的那一版。
# 早先的單一檔案設計有兩個實測抓到的洞——並行部署互相覆寫(後寫的把 domains 與 cli 蓋掉,
# 欄位不再代表先寫的那一支),以及任一支 CLI 重啟就把五支的閘門一起解除(其餘四支沒重啟卻
# 不再被擋,閘門等於半失效)。拆成一支一份之後,寫入、判定、清除三件事都只碰自己那一份。
#
# --- report 輸出格式 ---
#
# 一行一份狀態檔,欄位以空白分隔,domains 可能含空白所以擺最後:
# {CLI 代號} at={ISO 時間} mode={install|update} domains={domain 清單}
# 有幾行就代表有幾支 CLI 還沒重啟。欄位缺值時只留鍵名(例如 domains=)。第一欄印 legacy 的
# 那一行代表舊格式的單一狀態檔(見下方「舊檔相容」),它不屬於任何一支 CLI。
#
# --- 舊檔相容(過渡用) ---
#
# 舊版把狀態寫進 $JSC_HOME/restart-required 單一檔案。改用狀態目錄的第一輪部署,機器上可能
# 還留著那份舊檔:完全不認它,那一輪的閘門會整輪漏掉,檔案本身也會永遠留著變垃圾。所以:
# 判定:舊檔存在就一律擋,視為「每一支 CLI 都有未重啟的部署」。舊檔沒有 per-CLI 資訊,
# 分不出是哪一支寫的,寧可擋多不擋少。擋人訊息會標明這是舊格式紀錄。
# 清除:clear 除了刪當前 CLI 那一份,也一併刪掉舊檔。取捨講白:clear 只在新工作階段被
# 呼叫,呼叫到就代表確實有一支 CLI 重新啟動過了;舊檔沒有 per-CLI 資訊,留著會讓五支
# CLI 一路被擋到有人手動刪,刪掉是唯一收斂的做法。代價是同一輪部署的其他 CLI 少擋
# 一次,只影響改用狀態目錄的那一輪。
# 這一段是過渡用的:所有機器都跑過一次寫狀態目錄的部署與重啟之後,舊檔不會再被寫出來,屆時
# 可以整段移除——LEGACY_STATE、hook 判定裡的舊檔分支、clear 裡的舊檔刪除、report 的
# legacy 行、以及本節。
#
# --- 清除時機 ---
#
@@ -36,11 +68,13 @@
# 新舊工作階段的判準(sessions/{sid}.start 在不在)只有那支腳本知道,兩邊各寫一份就會漂移。
# 新的工作階段代表 CLI 行程是新起的,新版一定已經載入,所以清除是對的。續接同一階段
# (SessionStart 再觸發、resume、compact)不會走到那一段,閘門就一路留到真的重新啟動。
# 清除的範圍就是呼叫端那一支 CLI:那一支重啟了,不代表別支也重啟了。
#
# --- 判定原則 ---
#
# 比照 version-guard.sh:只擋確定違規,查不到基礎資訊一律放行(exit 0)。狀態檔讀不到、
# 技能名取不到、工具名不是 Skill,三種都放行——沒有證據時擋下等於停掉每一次技能呼叫。
# CLI 代號取不到、技能名取不到、工具名不是 Skill,四種都放行——沒有證據時擋下等於停掉每一次
# 技能呼叫。
#
# 豁免(這些技能永遠放行,改動前想清楚後果):
# jsc-cli:deploy 部署入口本身,也是唯一能把技能組換成新版的路徑,擋了會死鎖
@@ -64,12 +98,32 @@
# 逃生門:JSC_RESTART_GATE=off 完全略過這道閘門。
HERE=$(dirname "$0"); . "$HERE/lib.sh"
STATE="$JSC_HOME/restart-required"
STATE_DIR="$JSC_HOME/restart-required.d"
# 舊格式的單一狀態檔。只為過渡而讀,可移除的時機見檔頭「舊檔相容」。
LEGACY_STATE="$JSC_HOME/restart-required"
# 從狀態檔取一個欄位;檔案讀不到或欄位不存在就不輸出。
state_field() { # $1=鍵名
[ -f "$STATE" ] && [ -r "$STATE" ] || return 0
sed -n "s/^$1=//p" "$STATE" 2>/dev/null | head -n1
# 當前 CLI 代號;取不到就不輸出,由呼叫端決定怎麼降級。取法與其他 hook 一致(JSC_CLI 優先,
# 其次 lib.sh 的 cli_name)。不像代號的值一併當成取不到:這個值直接拿去當檔名,帶斜線或
# 點號開頭的值會把檔案寫到狀態目錄外面去。
cli_code() {
_c=$(cli_name)
case "$_c" in
""|unknown) return 0 ;;
.*|*[!A-Za-z0-9._-]*) return 0 ;;
esac
printf '%s' "$_c"
}
# 從指定狀態檔取一個欄位;檔案讀不到或欄位不存在就不輸出。
state_field() { # $1=狀態檔 $2=鍵名
[ -f "$1" ] && [ -r "$1" ] || return 0
sed -n "s/^$2=//p" "$1" 2>/dev/null | head -n1
}
# 一份狀態檔印一行,格式見檔頭「report 輸出格式」。$1=第一欄要印的名稱 $2=狀態檔
state_line() {
printf '%s at=%s mode=%s domains=%s\n' "$1" \
"$(state_field "$2" at)" "$(state_field "$2" mode)" "$(state_field "$2" domains)"
}
case "${1:-}" in
@@ -77,19 +131,37 @@ case "${1:-}" in
_mode="${2:-update}"
_domains=""
if [ "$#" -gt 2 ]; then shift 2; _domains="$*"; fi
mkdir -p "$JSC_HOME" 2>/dev/null || true
_cli=$(cli_code)
if [ -z "$_cli" ]; then
# 取不到代號就不知道該寫哪一份,寫成別的檔名也沒用:hook 模式同樣取不到代號,那一份
# 永遠不會被讀到。沒掛上就要講出來,不能讓部署以為掛上了。
printf '[jsc][重啟閘門][ERR]:取不到可用的 CLI 代號(JSC_CLI 未設定,或值不是代號),這次部署沒有掛上重啟閘門。\n' >&2
exit 2
fi
mkdir -p "$STATE_DIR" 2>/dev/null || true
printf 'at=%s\nmode=%s\ndomains=%s\ncli=%s\n' \
"$(now_iso)" "$_mode" "$_domains" "$(cli_name)" > "$STATE" 2>/dev/null || {
"$(now_iso)" "$_mode" "$_domains" "$_cli" > "$STATE_DIR/$_cli" 2>/dev/null || {
# 寫不進去要講出來:沒寫成就沒有閘門,部署卻以為掛上了。
printf '[jsc][重啟閘門][ERR]:寫不進 %s,這次部署沒有掛上重啟閘門。\n' "$STATE" >&2
printf '[jsc][重啟閘門][ERR]:寫不進 %s,這次部署沒有掛上重啟閘門。\n' "$STATE_DIR/$_cli" >&2
exit 2
}
exit 0 ;;
clear)
rm -f "$STATE" 2>/dev/null || true
_cli=$(cli_code)
# 只刪自己那一份。別支 CLI 沒有跟著重啟,它們的閘門要留著。
[ -n "$_cli" ] && rm -f "$STATE_DIR/$_cli" 2>/dev/null
# 舊檔一併刪,取捨與可移除時機見檔頭「舊檔相容」。
rm -f "$LEGACY_STATE" 2>/dev/null || true
exit 0 ;;
report)
if [ -f "$STATE" ] && [ -r "$STATE" ]; then cat "$STATE" 2>/dev/null || true; fi
# 目錄裡一份都沒有時,未展開的樣式字串會由 -f 判斷擋掉。
for _f in "$STATE_DIR"/*; do
[ -f "$_f" ] && [ -r "$_f" ] || continue
state_line "$(basename "$_f")" "$_f"
done
if [ -f "$LEGACY_STATE" ] && [ -r "$LEGACY_STATE" ]; then
state_line legacy "$LEGACY_STATE"
fi
exit 0 ;;
"") ;; # 落到下面的 hook 模式
*) exit 0 ;; # 不認得的子命令一律安靜放行,不中斷宿主 CLI
@@ -119,26 +191,41 @@ case "$skill" in
exit 0 ;;
esac
# 狀態檔讀不到就放行:沒有「剛部署過」的證據,就沒有擋人的理由。
[ -f "$STATE" ] && [ -r "$STATE" ] || exit 0
# CLI 代號取不到就放行:不知道現在跑的是哪一支,就不知道該讀哪一份狀態檔,等同沒有證據。
cli=$(cli_code)
[ -n "$cli" ] || exit 0
at=$(state_field at)
mode=$(state_field mode)
domains=$(state_field domains)
# 只看自己那一份;沒有才退回看舊檔。兩份都沒有就放行——沒有「剛部署過」的證據,就沒有擋人的
# 理由。別支 CLI 那幾份一律不看:那些是別的行程,重啟與否跟這一支無關。
state="$STATE_DIR/$cli"
legacy=no
if [ -f "$state" ] && [ -r "$state" ]; then
:
elif [ -f "$LEGACY_STATE" ] && [ -r "$LEGACY_STATE" ]; then
state="$LEGACY_STATE"; legacy=yes
else
exit 0
fi
at=$(state_field "$state" at)
mode=$(state_field "$state" mode)
domains=$(state_field "$state" domains)
# 重啟方式依實際 CLI 給。印別的 CLI 的執行檔名等於沒給指示。
bin=$(cli_bin "$(cli_name)")
[ "$bin" = unknown ] && bin="目前的 CLI"
bin=$(cli_bin "$cli")
# 訊息裡的部署資訊逐段接起來,缺欄位就少一段,不會留下空括號或多餘的逗號。
info=""
[ -n "$at" ] && info="$at"
[ -n "$mode" ] && info="${info}${info:+,}模式 $mode"
[ -n "$domains" ] && info="${info}${info:+,}domain:$domains"
# 舊格式紀錄要標出來:它分不出是哪一支 CLI 部署的,所以每一支都擋,看到訊息的人才不會以為
# 系統認定就是這一支剛部署過。
[ "$legacy" = yes ] && info="${info}${info:+,}舊格式紀錄,分不出是哪一支 CLI 部署的"
printf '[jsc][重啟閘門][ERR]:技能組已更新%s,%s 還在跑舊版,新版要重新啟動才會載入。本次技能呼叫已擋下。\n' \
"${info:+($info)}" "$bin" >&2
printf '重新啟動:結束 %s 再重新開啟一次,狀態檔 %s 會在新工作階段開始時自動清除。\n' \
"$bin" "$STATE" >&2
"$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
+4 -1
View File
@@ -16,11 +16,14 @@
# (restart-gate.sh clear)。新工作階段代表 CLI 行程是新起的,新版技能組一定已經載入。
# 判準只有這裡知道——start 分支的「起始檔不存在」就是這個 session id 第一次開始,
# 所以清除掛在這裡,不在 restart-gate.sh 裡自己再判一次。
# 清除的範圍是「跑到這一支腳本的那個 CLI 自己那一份狀態檔」,由 restart-gate.sh clear 認定,
# 這裡不必也不能過問:這個工作階段開始的只有一支 CLI,別支沒重啟,閘門要留著。
HERE=$(dirname "$0"); . "$HERE/lib.sh"
read_stdin
sid=$(session_id)
# 放下部署後的重啟閘門。狀態檔的路徑與格式只留在 restart-gate.sh,這裡不碰檔案。
# 放下這一支 CLI 的部署後重啟閘門。狀態檔的路徑、範圍與格式只留在 restart-gate.sh,
# 這裡不碰檔案,所以改成一支 CLI 一份狀態檔之後這裡不用跟著改。
# 一律 </dev/null:clear 不讀標準輸入,但這裡的標準輸入是宿主餵進來的管線,不關掉會卡住。
clear_restart_gate() {
sh "$HERE/restart-gate.sh" clear </dev/null 2>/dev/null || true