fix(hooks): 重啟閘門狀態檔改為一支 CLI 一份,修並行覆寫與全域解除 #37

Merged
admin merged 4 commits from fix/restart-gate-per-cli-state into develop 2026-08-27 10:51:21 +00:00
Member

摘要

  • 需求描述:修掉部署後重啟閘門(R15)的狀態檔設計缺陷。狀態檔原本是全機器單一檔案 $JSC_HOME/restart-required,部署現場實測抓到兩個後果:require 用 > 覆寫,並行部署互相覆蓋(kiro 寫入 9 秒後被 codex 蓋掉,domains= 與 cli= 不再代表 kiro);clear 用 rm -f 刪整個檔案,任一支 CLI 重啟就解除全部五支的閘門,其餘四支沒重啟卻不再被擋。這台機器裝了五支 CLI,閘門在多 CLI 環境等於半失效。改為一支 CLI 一份的狀態檔 $JSC_HOME/restart-required.d/{CLI 代號},require、判定、clear 三件事都只碰自己那一份。
  • 計畫名稱:無
  • 計畫頁:無
  • 分析頁:無

變更內容

檔案 為什麼改
hooks/restart-gate.sh 主體。狀態檔改為狀態目錄底下一支 CLI 一份;新增 cli_code()(含不能當檔名的髒值防護)與 state_line();require 只寫自己那一份、hook 判定只讀自己那一份、clear 只刪自己那一份、report 一支 CLI 一行;舊格式單一檔案存在時一律擋並標明是舊格式紀錄,clear 一併刪掉它;檔頭註解同步
hooks/session-timer.sh 程式碼不動。兩處註解補上「清除範圍只有跑到這支腳本的那一支 CLI」,說明為什麼改成一支一份之後這裡不必跟著改
tools/wire-cli.sh smoke 的重啟閘門段從 9 條情境擴為 16 條,新增 smoke_rs_file() 比對狀態檔在不在——一支一份的範圍反映在檔案在不在,不反映在結束碼;檔頭的判定路徑清單同步
README.md Hooks 表的 restart-gate.sh 與 session-timer.sh 兩列、覆蓋範圍引言、「部署後重啟狀態檔」整節改寫,加上 report 輸出格式與舊檔相容說明
skills/hooks-install/SKILL.md 第 45 項改為 restart-required.d/{cli},補上只讀自己那一份、清除只清自己那一份、report 一支 CLI 一行、舊檔過渡說明
plugin.json、.claude-plugin/plugin.json、.codex-plugin/plugin.json 版本 0.2.5 → 0.2.6,三份同步;版本不升,版本前置檢查與部署的落後判定都看不出本機是舊版

設計重點

  • 狀態檔一支 CLI 一份。 目錄 $JSC_HOME/restart-required.d/,檔名就是 CLI 代號,四行 key=value 的格式不變。閘門管的是「這一支 CLI 的行程還在跑舊版」,那是每支 CLI 各自的事實,狀態檔不該共用。
  • 範圍收斂在三個動作上。 require 寫自己那份、hook 判定只看自己那份、clear 只刪自己那份;別支那幾份一律不看。report 因此列得出「還有哪幾支沒重啟」,一支一行。
  • 取不到 CLI 代號時的兩種降級。 hook 模式放行,比照既有的「查不到就放行」原則(同 version-guard.sh);require 回 exit 2 並講明沒有掛上閘門——寫到讀不到的檔名等於沒掛,不能讓部署以為掛上了。
  • 代號當檔名的髒值防護。 含 /、以 . 開頭、或出現 [A-Za-z0-9._-] 以外字元的值一律當成取不到,狀態檔就寫不到狀態目錄外面去。
  • 舊檔相容是過渡用的。 舊格式單一檔案存在就一律擋(沒有 per-CLI 資訊,寧可擋多不擋少),訊息標明是舊格式紀錄;clear 除了刪自己那份也刪舊檔——clear 只在新工作階段被呼叫,呼叫到就代表確實有一支重啟過了,舊檔留著會讓五支一路被擋到有人手動刪。可移除的時機(所有機器都跑過一次寫狀態目錄的部署與重啟之後)與要移除哪幾段寫在 restart-gate.sh 檔頭與 README。
  • 分工不變。 新舊工作階段的判準留在 session-timer.sh,狀態檔的路徑、範圍與格式留在 restart-gate.sh,兩邊都不抄對方那一半。
  • 豁免清單與逃生門不動。 九支豁免技能與 JSC_RESTART_GATE=off 都照舊,這一輪沒有動到豁免範圍。

測試結果

  • ste100-lint.sh:本存取庫全綠。三份 manifest 版本一致為 0.2.6。sh -n 對這次改動過的 hooks/restart-gate.sh、hooks/session-timer.sh、tools/wire-cli.sh 全數通過。
  • 核心隔離(主要修的就是這件事):kiro 與 codex 各跑一次 require,產生兩份獨立狀態檔、不互相覆蓋;三支 CLI 各只看自己那一份(kiro 擋、codex 擋、claude 放行);kiro clear 之後只剩 codex 那一份;kiro 重啟後放行而 codex 仍被擋——這一列正是舊版壞掉的行為。
  • 舊檔相容:只有舊檔時三支 CLI 全擋,訊息標明是舊格式紀錄;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 條專屬情境,加上每支 hook 都跑一次的那一次通用執行,共 17 行全部與預期相同。
  • 無法測試的部分:codex、copilot、antigravity、kiro 四支沒有 pre-tool hook,這道閘門在那四支上一次技能呼叫都擋不下來。那是覆蓋範圍的先天限制,不是還沒測。
  • 這批修正尚未部署到任何 CLI。

前置 Push Request

  • 無
## 摘要 - 需求描述:修掉部署後重啟閘門(R15)的狀態檔設計缺陷。狀態檔原本是全機器單一檔案 `$JSC_HOME/restart-required`,部署現場實測抓到兩個後果:`require` 用 `>` 覆寫,並行部署互相覆蓋(kiro 寫入 9 秒後被 codex 蓋掉,`domains=` 與 `cli=` 不再代表 kiro);`clear` 用 `rm -f` 刪整個檔案,任一支 CLI 重啟就解除全部五支的閘門,其餘四支沒重啟卻不再被擋。這台機器裝了五支 CLI,閘門在多 CLI 環境等於半失效。改為一支 CLI 一份的狀態檔 `$JSC_HOME/restart-required.d/{CLI 代號}`,`require`、判定、`clear` 三件事都只碰自己那一份。 - 計畫名稱:無 - 計畫頁:無 - 分析頁:無 ## 變更內容 | 檔案 | 為什麼改 | | --- | --- | | `hooks/restart-gate.sh` | 主體。狀態檔改為狀態目錄底下一支 CLI 一份;新增 `cli_code()`(含不能當檔名的髒值防護)與 `state_line()`;`require` 只寫自己那一份、hook 判定只讀自己那一份、`clear` 只刪自己那一份、`report` 一支 CLI 一行;舊格式單一檔案存在時一律擋並標明是舊格式紀錄,`clear` 一併刪掉它;檔頭註解同步 | | `hooks/session-timer.sh` | 程式碼不動。兩處註解補上「清除範圍只有跑到這支腳本的那一支 CLI」,說明為什麼改成一支一份之後這裡不必跟著改 | | `tools/wire-cli.sh` | `smoke` 的重啟閘門段從 9 條情境擴為 16 條,新增 `smoke_rs_file()` 比對狀態檔在不在——一支一份的範圍反映在檔案在不在,不反映在結束碼;檔頭的判定路徑清單同步 | | `README.md` | Hooks 表的 `restart-gate.sh` 與 `session-timer.sh` 兩列、覆蓋範圍引言、「部署後重啟狀態檔」整節改寫,加上 `report` 輸出格式與舊檔相容說明 | | `skills/hooks-install/SKILL.md` | 第 45 項改為 `restart-required.d/{cli}`,補上只讀自己那一份、清除只清自己那一份、`report` 一支 CLI 一行、舊檔過渡說明 | | `plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` | 版本 0.2.5 → 0.2.6,三份同步;版本不升,版本前置檢查與部署的落後判定都看不出本機是舊版 | ## 設計重點 - **狀態檔一支 CLI 一份。** 目錄 `$JSC_HOME/restart-required.d/`,檔名就是 CLI 代號,四行 key=value 的格式不變。閘門管的是「這一支 CLI 的行程還在跑舊版」,那是每支 CLI 各自的事實,狀態檔不該共用。 - **範圍收斂在三個動作上。** `require` 寫自己那份、hook 判定只看自己那份、`clear` 只刪自己那份;別支那幾份一律不看。`report` 因此列得出「還有哪幾支沒重啟」,一支一行。 - **取不到 CLI 代號時的兩種降級。** hook 模式放行,比照既有的「查不到就放行」原則(同 `version-guard.sh`);`require` 回 exit 2 並講明沒有掛上閘門——寫到讀不到的檔名等於沒掛,不能讓部署以為掛上了。 - **代號當檔名的髒值防護。** 含 `/`、以 `.` 開頭、或出現 `[A-Za-z0-9._-]` 以外字元的值一律當成取不到,狀態檔就寫不到狀態目錄外面去。 - **舊檔相容是過渡用的。** 舊格式單一檔案存在就一律擋(沒有 per-CLI 資訊,寧可擋多不擋少),訊息標明是舊格式紀錄;`clear` 除了刪自己那份也刪舊檔——`clear` 只在新工作階段被呼叫,呼叫到就代表確實有一支重啟過了,舊檔留著會讓五支一路被擋到有人手動刪。可移除的時機(所有機器都跑過一次寫狀態目錄的部署與重啟之後)與要移除哪幾段寫在 `restart-gate.sh` 檔頭與 README。 - **分工不變。** 新舊工作階段的判準留在 `session-timer.sh`,狀態檔的路徑、範圍與格式留在 `restart-gate.sh`,兩邊都不抄對方那一半。 - **豁免清單與逃生門不動。** 九支豁免技能與 `JSC_RESTART_GATE=off` 都照舊,這一輪沒有動到豁免範圍。 ## 測試結果 - `ste100-lint.sh`:本存取庫全綠。三份 manifest 版本一致為 0.2.6。`sh -n` 對這次改動過的 `hooks/restart-gate.sh`、`hooks/session-timer.sh`、`tools/wire-cli.sh` 全數通過。 - **核心隔離**(主要修的就是這件事):kiro 與 codex 各跑一次 `require`,產生兩份獨立狀態檔、不互相覆蓋;三支 CLI 各只看自己那一份(kiro 擋、codex 擋、claude 放行);kiro `clear` 之後只剩 codex 那一份;kiro 重啟後放行而 **codex 仍被擋**——這一列正是舊版壞掉的行為。 - 舊檔相容:只有舊檔時三支 CLI 全擋,訊息標明是舊格式紀錄;`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 條專屬情境,加上每支 hook 都跑一次的那一次通用執行,共 17 行全部與預期相同。 - **無法測試的部分**:codex、copilot、antigravity、kiro 四支沒有 pre-tool hook,這道閘門在那四支上一次技能呼叫都擋不下來。那是覆蓋範圍的先天限制,不是還沒測。 - 這批修正尚未部署到任何 CLI。 ## 前置 Push Request - 無
jiantw83 added 4 commits 2026-08-27 10:50:11 +00:00
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,配合這一輪重啟閘門的修正發佈。
admin merged commit 852d8553b4 into develop 2026-08-27 10:51:21 +00:00
admin deleted branch fix/restart-gate-per-cli-state 2026-08-27 10:51:21 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Reference: plugins/hooks#37