develop
master
$JSC_HOME/restart-required.d/{cli}
hooks/version-guard.sh
version-guard.sh
plugin.json
require
>
clear
rm -f
hooks/restart-gate.sh
cli_code
report
hooks/session-timer.sh
restart-gate.sh
tools/wire-cli.sh
smoke_rs_file
README.md
skills/hooks-install/SKILL.md
.claude-plugin/plugin.json
.codex-plugin/plugin.json
session-timer.sh
/
.
[A-Za-z0-9._-]
JSC_CLI=../evil
a/b
..
$JSC_HOME/restart-required
restart-gate.sh report
tools/ste100-lint.sh
sh -n
JSC_RESTART_GATE=off
</dev/null
timeout
tools/wire-cli.sh smoke claude
status=ok
What:`hooks/restart-gate.sh` 的狀態檔從全機器單一檔案 `$JSC_HOME/restart-required` 改為狀態目錄 `$JSC_HOME/restart-required.d/{CLI 代號}`,一支 CLI 一份,檔名就是 CLI 代號,四行 key=value 的格式不變。新增 `cli_code()` 取當前 CLI 代號(含不能當檔名的髒值防護)與 `state_line()` 統一 `report` 的輸出格式;`require` 只寫自己那一份、hook 判定只讀自己那一份、`clear` 只刪自己那一份、`report` 一支 CLI 一行印出還沒重啟的是哪幾支。舊格式的單一檔案存在時一律擋,訊息標明是舊格式紀錄,`clear` 會一併刪掉它。檔頭註解同步改寫,新增「report 輸出格式」與「舊檔相容(過渡用)」兩節。`hooks/session-timer.sh` 程式碼不動,兩處註解補上「清除範圍只有跑到這支腳本的那一支 CLI」。 Why:這道閘門 2026-08-27 才上線,部署現場實測抓到兩個缺陷。第一,`require` 用 `>` 覆寫單一檔案,並行部署互相覆蓋——kiro 寫入 9 秒後被 codex 蓋掉,`domains=` 與 `cli=` 不再代表 kiro。第二,`clear` 用 `rm -f` 刪整個檔案,任一支 CLI 重啟就解除全部五支的閘門,其餘四支沒重啟卻不再被擋。這台機器就裝了五支 CLI,閘門在多 CLI 環境等於半失效。閘門管的是「這一支 CLI 的行程還在跑舊版」,那是每支 CLI 各自的事實,狀態檔本來就不該共用。 How:寫入、判定、清除三件事都只碰自己那一份,別支那幾份一律不看,`clear` 的範圍也就收斂成呼叫端那一支。取不到 CLI 代號時比照既有的「查不到就放行」原則:hook 模式放行,`require` 回 exit 2 並講明這次沒有掛上閘門——寫到讀不到的檔名等於沒掛,不能讓部署以為掛上了。代號會直接拿去當檔名,所以含 `/`、以 `.` 開頭、或出現 `[A-Za-z0-9._-]` 以外字元的值一律當成取不到,狀態檔就寫不到目錄外面去。舊檔沒有 per-CLI 資訊,分不出是哪一支寫的,判定寧可擋多不擋少;`clear` 只在新工作階段被呼叫,呼叫到就代表確實有一支重新啟動過了,舊檔留著會讓五支一路被擋到有人手動刪,所以一併刪掉。這段相容邏輯與可移除的時機(所有機器都跑過一次寫狀態目錄的部署與重啟之後)寫在檔頭「舊檔相容」與 README。`session-timer.sh` 不必跟著改:狀態檔的路徑、範圍與格式只留在 `restart-gate.sh`,那裡只負責判斷新舊工作階段。 Who:`jsc-hooks` 的部署後強制重啟閘門(R15),以及它與 `session-timer.sh` 的清除分工。
What:`tools/wire-cli.sh` 的 `smoke` 重啟閘門段從 9 條情境擴為 16 條,新增 `smoke_rs_file()` 比對狀態檔在不在。新增的情境是:只有別支 CLI 有狀態檔時放行、`require` 寫出的是當前 CLI 那一份、新工作階段開始後只清掉自己那一份、清除不動別支那一份、清除後放行、舊格式單一狀態檔存在時擋下、`clear` 一併刪掉舊檔、舊檔清除後放行。原有的豁免技能、逃生門與取不到技能名三類情境照舊,情境名稱從「狀態檔存在」改寫為「當前 CLI 那份存在」。檔頭的判定路徑清單同步改寫。 Why:只比對結束碼看不出「一支 CLI 一份」有沒有真的成立——別支那一份要留著、自己那一份要刪掉,兩件事都反映在檔案在不在,不反映在結束碼。舊版的冒煙只驗得到「狀態檔不存在」與「狀態檔存在」兩條,正好漏掉這次修掉的兩個缺陷所在的範圍:並行部署的覆寫與清除的全域解除,兩者在單一檔案的年代都跑得出 exit 0 與 exit 2 的正確值。 How:情境沿用暫時的 `$JSC_HOME`,不動使用者真正的狀態目錄——冒煙測試不該把別人的閘門拆掉。別支 CLI 的代號取 `smoke-other` 這個不在五支之列的固定值,才不會跟這一輪的 `$cli` 撞在一起。`smoke_rs_file()` 只比 `exist` 與 `absent` 兩種預期,失敗訊息直接講明「一支 CLI 一份的範圍壞了」,看訊息就知道是範圍出錯,不是腳本跑不動。舊檔那兩條情境走 `session-timer.sh restart` 觸發清除,跟真實路徑一致。 Who:`jsc-hooks` 接線工具的冒煙測試,覆蓋部署後重啟閘門的判定與清除範圍。
What:`README.md` 改三處——Hooks 表的 `restart-gate.sh` 與 `session-timer.sh` 兩列、覆蓋範圍的引言、以及「部署後重啟狀態檔」整節。整節改寫後寫明狀態檔路徑是 `$JSC_HOME/restart-required.d/{CLI 代號}`、一支 CLI 一份的理由、`report` 的輸出格式,以及舊檔相容與它可移除的時機。`skills/hooks-install/SKILL.md` 第 45 項改為 `restart-required.d/{cli}`,補上「每支 CLI 只讀自己那一份」、「重啟只清自己那一份」、「`report` 一支 CLI 一行」與舊檔過渡說明。 Why:這兩份是接線與判讀閘門時唯一會被讀到的說明。路徑改了而文件沒改,看文件的人會去看一個不存在的檔案,也會沿用「重啟一支就全清」的舊認知去判斷閘門有沒有生效。一支 CLI 一份的理由(並行覆寫與全域解除兩個實測缺陷)要留在文件裡,下一個人才不會為了少一個目錄又改回單一檔案。 How:只改說明,不動任何判定邏輯。路徑與格式的唯一來源仍是 `hooks/restart-gate.sh`,文件只轉述,舊檔相容的取捨與可移除清單也指回檔頭那一節,兩邊不各自維護一份。覆蓋範圍的引言照舊據實寫:非 claude 的四支 CLI 沒有 pre-tool hook,這道閘門一次都擋不下來,改成一支一份也不會變。 Who:`jsc-hooks` 的存取庫說明與 `jsc-hooks:hooks-install` 技能文件,對齊部署後重啟閘門的實作。
What:`plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` 的 `version` 從 0.2.5 升到 0.2.6,三份同步,description 不動。 Why:這一輪改掉重啟閘門的狀態檔路徑與範圍,是使用者裝上去就會拿到的行為變更。版本不升,`version-guard.sh` 的版本前置檢查與 `jsc-cli:deploy` 的落後判定都看不出本機還是舊版,機器上就不會被提示更新。 How:只改版號一個欄位。三份必須一致:`plugin.json` 給 marketplace、`.claude-plugin` 給 claude、`.codex-plugin` 給 codex,任一份落後都會讓那一路的版本比對抓錯。修的是既有行為的缺陷、沒有新增子命令也沒有改變呼叫介面,所以走修訂號。 Who:`jsc-hooks` 的三份 plugin manifest,配合這一輪重啟閘門的修正發佈。
Reviewed-on: #37
No dependencies set.
The note is not visible to the blocked user.
PR 描述
摘要
develop上的 v0.2.6 放行到預設分支master,共 5 個 commit。內容是 R15 部署後重啟閘門的狀態檔設計修正:狀態檔從全機器單一檔案改成一支 CLI 一份,落在$JSC_HOME/restart-required.d/{cli}。這一段是必要的:marketplace 與hooks/version-guard.sh都讀存取庫的預設分支master(version-guard.sh取 rawplugin.json時刻意不指定 ref,Gitea 就回預設分支那一份),develop併了不等於生效,要放行到master,已安裝的五支 CLI 才抓得到這次修正。修的兩個缺陷都是 2026-08-27 這道閘門上線當日部署時實測抓到的:一、require用>覆寫同一個檔案,並行部署互相覆蓋,實測 kiro 寫入 9 秒後被 codex 蓋掉;二、clear用rm -f刪整個檔案,任一支 CLI 重啟就解除全部五支的閘門,其餘四支沒重啟卻不再被擋——這台機器裝了五支 CLI,等於這道閘門半失效。變更內容
hooks/restart-gate.sh$JSC_HOME/restart-required.d/{cli},一支 CLI 一份,檔名就是 CLI 代號。新增cli_code取當前 CLI 代號並做髒值防護;require只寫自己那份、取不到代號就 exit 2 並講明沒掛上閘門;hook 判定只讀自己那份、取不到代號一律放行;clear只刪自己那份,順手刪掉舊格式單一檔案;report改成一支 CLI 一行,看得出還有哪幾支沒重啟。四行 key=value 的欄位格式不變。hooks/session-timer.shrestart-gate.sh,這裡不碰檔案,所以改成一支一份之後這支腳本的邏輯不用跟著改。tools/wire-cli.shsmoke_rs_file比狀態檔在不在,情境擴充為:當前 CLI 沒有狀態檔、只有別支 CLI 有狀態檔、當前 CLI 那份存在、四支豁免技能、逃生門、取不到技能名,再加清除只刪自己那份、別支那份留著、舊格式單一檔案照樣擋得下來也清得掉。README.mdreport的輸出格式與舊檔相容的取捨。非 claude 四支的覆蓋範圍說明也跟著改成「那一支自己那份」。skills/hooks-install/SKILL.mdreport的用途、舊檔一律擋且下次clear刪掉。豁免九支的清單不變。plugin.json、.claude-plugin/plugin.json、.codex-plugin/plugin.json設計重點
hooks/restart-gate.sh;新舊工作階段的判準仍只在hooks/session-timer.sh。兩邊都不抄對方那一半,這也是為什麼session-timer.sh這次只改註解。/、以.開頭、或帶[A-Za-z0-9._-]以外字元的值一律當成取不到,不寫檔也不判定。實測JSC_CLI=../evil、a/b、.、..四種都回 exit 2,狀態目錄外沒有多出任何檔案。$JSC_HOME/restart-required存在時一律擋,視為每一支 CLI 都有未重啟的部署——舊檔沒有 per-CLI 資訊,分不出是哪一支寫的,寧可擋多不擋少,擋人訊息會標明這是舊格式紀錄。clear除了刪自己那份也刪掉舊檔:clear只在新工作階段被呼叫,呼叫到就代表確實有一支重新啟動過了,舊檔留著會讓五支一路被擋到有人手動刪。可移除的時機(所有機器都跑過一次寫狀態目錄的部署與重啟之後)寫在檔頭與 README,連要刪哪幾段都列了。version-guard.sh,這次從三種放行擴充為四種——狀態檔讀不到、CLI 代號取不到、技能名取不到、工具名不是 Skill。restart-gate.sh report現在看得出哪幾支還沒重啟。仍然只有 claude 擋得住——codex、copilot、antigravity、kiro 沒有 pre-tool hook,狀態檔照樣寫、照樣清,但中間沒有判定點。測試結果
tools/ste100-lint.sh掃本存取庫全綠。plugin.json、.claude-plugin/plugin.json、.codex-plugin/plugin.json)版本一致為 0.2.6。sh -n對改動過的腳本(hooks/restart-gate.sh、hooks/session-timer.sh、tools/wire-cli.sh)全部通過。require,產生兩份獨立狀態檔、不互相覆蓋;三支 CLI 各只看得到自己那一份;kiroclear之後只剩 codex 那一份;重啟後 kiro 放行而 codex 仍被擋。最後這一列正是舊版壞掉的行為,主 agent 另外獨立複驗過一次,結果一致。clear之後舊檔被刪;再判定一次放行。JSC_RESTART_GATE=off放行、取不到技能名放行、取不到 CLI 代號放行。JSC_CLI=../evil、a/b、.、..四種全部 exit 2,狀態目錄外沒有多出檔案。</dev/null加timeout之下都不卡住。tools/wire-cli.sh smoke claude回status=ok,重啟閘門 16 條專屬情境加通用執行共 17 行全部與預期相同。develop內容搬到master,沒有新的程式碼變更。前置 Push Request
develop。What:`hooks/restart-gate.sh` 的狀態檔從全機器單一檔案 `$JSC_HOME/restart-required` 改為狀態目錄 `$JSC_HOME/restart-required.d/{CLI 代號}`,一支 CLI 一份,檔名就是 CLI 代號,四行 key=value 的格式不變。新增 `cli_code()` 取當前 CLI 代號(含不能當檔名的髒值防護)與 `state_line()` 統一 `report` 的輸出格式;`require` 只寫自己那一份、hook 判定只讀自己那一份、`clear` 只刪自己那一份、`report` 一支 CLI 一行印出還沒重啟的是哪幾支。舊格式的單一檔案存在時一律擋,訊息標明是舊格式紀錄,`clear` 會一併刪掉它。檔頭註解同步改寫,新增「report 輸出格式」與「舊檔相容(過渡用)」兩節。`hooks/session-timer.sh` 程式碼不動,兩處註解補上「清除範圍只有跑到這支腳本的那一支 CLI」。 Why:這道閘門 2026-08-27 才上線,部署現場實測抓到兩個缺陷。第一,`require` 用 `>` 覆寫單一檔案,並行部署互相覆蓋——kiro 寫入 9 秒後被 codex 蓋掉,`domains=` 與 `cli=` 不再代表 kiro。第二,`clear` 用 `rm -f` 刪整個檔案,任一支 CLI 重啟就解除全部五支的閘門,其餘四支沒重啟卻不再被擋。這台機器就裝了五支 CLI,閘門在多 CLI 環境等於半失效。閘門管的是「這一支 CLI 的行程還在跑舊版」,那是每支 CLI 各自的事實,狀態檔本來就不該共用。 How:寫入、判定、清除三件事都只碰自己那一份,別支那幾份一律不看,`clear` 的範圍也就收斂成呼叫端那一支。取不到 CLI 代號時比照既有的「查不到就放行」原則:hook 模式放行,`require` 回 exit 2 並講明這次沒有掛上閘門——寫到讀不到的檔名等於沒掛,不能讓部署以為掛上了。代號會直接拿去當檔名,所以含 `/`、以 `.` 開頭、或出現 `[A-Za-z0-9._-]` 以外字元的值一律當成取不到,狀態檔就寫不到目錄外面去。舊檔沒有 per-CLI 資訊,分不出是哪一支寫的,判定寧可擋多不擋少;`clear` 只在新工作階段被呼叫,呼叫到就代表確實有一支重新啟動過了,舊檔留著會讓五支一路被擋到有人手動刪,所以一併刪掉。這段相容邏輯與可移除的時機(所有機器都跑過一次寫狀態目錄的部署與重啟之後)寫在檔頭「舊檔相容」與 README。`session-timer.sh` 不必跟著改:狀態檔的路徑、範圍與格式只留在 `restart-gate.sh`,那裡只負責判斷新舊工作階段。 Who:`jsc-hooks` 的部署後強制重啟閘門(R15),以及它與 `session-timer.sh` 的清除分工。What:`README.md` 改三處——Hooks 表的 `restart-gate.sh` 與 `session-timer.sh` 兩列、覆蓋範圍的引言、以及「部署後重啟狀態檔」整節。整節改寫後寫明狀態檔路徑是 `$JSC_HOME/restart-required.d/{CLI 代號}`、一支 CLI 一份的理由、`report` 的輸出格式,以及舊檔相容與它可移除的時機。`skills/hooks-install/SKILL.md` 第 45 項改為 `restart-required.d/{cli}`,補上「每支 CLI 只讀自己那一份」、「重啟只清自己那一份」、「`report` 一支 CLI 一行」與舊檔過渡說明。 Why:這兩份是接線與判讀閘門時唯一會被讀到的說明。路徑改了而文件沒改,看文件的人會去看一個不存在的檔案,也會沿用「重啟一支就全清」的舊認知去判斷閘門有沒有生效。一支 CLI 一份的理由(並行覆寫與全域解除兩個實測缺陷)要留在文件裡,下一個人才不會為了少一個目錄又改回單一檔案。 How:只改說明,不動任何判定邏輯。路徑與格式的唯一來源仍是 `hooks/restart-gate.sh`,文件只轉述,舊檔相容的取捨與可移除清單也指回檔頭那一節,兩邊不各自維護一份。覆蓋範圍的引言照舊據實寫:非 claude 的四支 CLI 沒有 pre-tool hook,這道閘門一次都擋不下來,改成一支一份也不會變。 Who:`jsc-hooks` 的存取庫說明與 `jsc-hooks:hooks-install` 技能文件,對齊部署後重啟閘門的實作。