fix(restart-gate): 閘門改問行程還在不在,不問代號見過沒有
判準原本是「這個工作階段代號我沒見過=行程是新起的」。那個等式不 成立:還沒重啟的工作階段自己生出來的子行程,拿到的也是沒見過的代 號,於是替人把閘門放下了,而人一次都沒重啟。 這台機器上真的發生過。部署掛上的閘門兩分鐘後就不見了,那段時間有 三個子工作階段冒出來,收尾那句「請重新啟動」於是只剩人自己記得。 排程那條路碰巧沒踩到,因為 cron 條目帶著 JSC_CLI=cron,清的是別 一份——巧合擋下來的,不是判準擋下來的。 反方向也會答錯:續接原代號的 resume,行程確實換過了,舊寫法卻連 問都不會問。 所以 require 一併記下掛上閘門時的工作階段代號與那一支 CLI 的行程 代號,清除只認行程存活:還活著就不清,走了就清。追不到行程代號時 退回結束記號,要求代號換了而且舊階段寫出過 .end。核對命令名不只 看行程還在,因為行程代號會被回收。 判定整段搬到 restart-gate.sh,session-timer.sh 只負責問。冒煙那 一組原本把舊語意寫成斷言,改成逐條驗三條清除路徑,並加一條驗 require 真的把欄位寫下來——少了那個欄位會無聲退回相容路徑,而每 一條行為斷言照樣全綠。
This commit is contained in:
@@ -23,7 +23,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
| 腳本 | 事件 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) |
|
||||
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間。`start` 與 `restart` 判定為新工作階段時,另外呼叫 `restart-gate.sh clear` 放下部署後的重啟閘門——新工作階段代表 CLI 行程是新起的,新版一定已經載入。清除的範圍只有跑到這支腳本的那一支 CLI 自己那一份狀態檔,別支沒重啟就繼續被擋 |
|
||||
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間。`start` 與 `restart` 一律呼叫一次 `restart-gate.sh clear` 問要不要放下部署後的重啟閘門,**只是問,清不清由那一邊看行程存活決定**:這裡曾經自己判過,用的是「起始檔不存在=行程是新起的」,而還沒重啟的工作階段生出來的子行程拿到的也是沒見過的代號,於是替人把閘門放下了。清除的範圍只有跑到這支腳本的那一支 CLI 自己那一份狀態檔,別支沒重啟就繼續被擋 |
|
||||
| `hooks/session-reminder.sh` | SessionStart | 把助理算好的未讀提醒帶到前景。只讀 `$JSC_HOME/assistant/reminders.tsv`(`jsc-assist` 的巡檢每一輪重寫),逾期的排前面、使用者自己登錄的到期提醒在後,最多列 8 筆;委派清單種入的內建項只印一行總數(那幾筆等的是接線不是人,每一輪都到期、每一輪都一樣,逐筆吐出來就是噪音),另加一行「有幾筆待辦連續失敗」。**這一支一個判定都不做**:自己拿 `due` 欄與 `next_run` 去跟現在比就是第二套到期判定,跟助理那一套遲早對不上。一個工作階段只提一次,記號是 `$JSC_HOME/sessions/{代號}.reminded`,接不到 session id 的 CLI 由 `session-timer.sh restart` 清掉那個記號。佇列檔頭帶那一輪的時間戳與 epoch,超過心跳門檻或心跳不新鮮就明說「這批提醒是多久以前算的、助理現在的心跳是什麼狀態」——一份沒有人更新的佇列讀起來跟新的一模一樣,而「沒有提醒」與「沒有人算提醒」不可以長得一樣。助理狀態目錄不存在時一個字都不印:那台機器從沒啟動過助理,每個工作階段催一次不是提醒是噪音。子指令 `peek` 只印不記號,給人重看與檢核用。永遠 exit 0 |
|
||||
| `hooks/skill-name.sh` | 不直接接線,由 `version-guard.sh` 與 `restart-gate.sh` 呼叫 | 從各 CLI 的 hook 負載解析出這一次要用哪一支 jsc 技能,一支 CLI 一個子命令,印一行「{domain}<TAB>{技能名}」,解析不出來就印空字串。取值來源:claude 讀 stdin JSON 的 `skill` 欄位、codex 讀 `tool_input.command` 裡那條 `SKILL.md` 路徑(Codex 沒有 Skill 工具,技能是模型自己用 Bash 讀 `SKILL.md` 載入的)、copilot 讀 `toolArgs`(字串化的 JSON,要先剝一層跳脫)、antigravity 讀 `toolCall.args.AbsolutePath` 另收提示字串(斜線指令不產生工具呼叫)、kiro 讀 `prompt` 開頭那個斜線指令;五支都先看環境變數 `JSC_SKILL`、`SKILL`。永遠 exit 0:閘門那一端一律 fail-open,而且 copilot 的 command hook 是 fail-closed 的,回非零等於拒絕。規則只有這一份,兩支閘門都不重寫第二套 |
|
||||
| `hooks/deny.sh` | 不直接接線,由 `version-guard.sh` 與 `restart-gate.sh` 呼叫 | 產出各 CLI 認得的阻擋輸出,訊息從參數或標準輸入進。claude、codex、copilot 訊息寫 stderr 並回 exit 2;antigravity 印 stdout 的 `{"decision":"deny","reason":"..."}` 並固定回 0——那支 CLI 的結束碼語意兩邊文件都沒寫,靠結束碼會變成「判定擋下、CLI 照樣放行」的無聲失效,所以 stdout 只准有那一行;kiro 擋不下技能叫用,改印警告後回 0;認不得的代號走 stderr 加 2 這個保守預設 |
|
||||
@@ -121,7 +121,11 @@ Claude 由 `hooks/hooks.json` 自動接線十支 hook;其他 CLI 用 `hooks-in
|
||||
|
||||
一支 CLI 一份是為了修兩個實測抓到的洞:一台機器上五支 CLI 各自是獨立行程,各自載入自己記憶體裡的那一版。早先的單一檔案設計裡,並行部署會互相覆寫(後寫的把 `domains` 與 `cli` 蓋掉,欄位不再代表先寫的那一支),而且任一支 CLI 重啟就把五支的閘門一起解除,其餘四支沒重啟卻不再被擋,閘門在多 CLI 環境等於半失效。拆成一支一份之後,寫入、判定、清除三件事都只碰自己那一份。
|
||||
|
||||
寫檔的一律是 `jsc-cli:deploy`,經 `restart-gate.sh require {install|update} [{domain}...]` 落地,寫的是當前 CLI 那一份;取不到 CLI 代號或寫不進去都會 exit 2 並講明「這次部署沒有掛上重啟閘門」——沒寫成就沒有閘門,不能讓部署以為掛上了。清除的一律是 `session-timer.sh`:`start` 判定起始檔不存在(這個 session id 第一次開始)、或 `restart`(接不到 session id 的 CLI,每次工作階段開始都算新的)時,呼叫 `restart-gate.sh clear`,只刪呼叫端那一支自己那一份。判準留在 `session-timer.sh`、狀態檔留在 `restart-gate.sh`,兩邊都不抄對方那一半。
|
||||
寫檔的一律是 `jsc-cli:deploy`,經 `restart-gate.sh require {install|update} [{domain}...]` 落地,寫的是當前 CLI 那一份;取不到 CLI 代號或寫不進去都會 exit 2 並講明「這次部署沒有掛上重啟閘門」——沒寫成就沒有閘門,不能讓部署以為掛上了。問清除的一律是 `session-timer.sh`:`start` 與 `restart` 都在工作階段開始時呼叫一次 `restart-gate.sh clear`,並把手上那個工作階段代號帶進去,只刪呼叫端那一支自己那一份。
|
||||
|
||||
**判準是「載入舊程式碼那個行程還在不在」,寫在 `restart-gate.sh`。** `require` 落地時一併記下 `session=` 與 `pid=`——掛上閘門那一刻的工作階段代號,與那一支 CLI 自己的行程代號(往上追祖先,比對命令名等於 CLI 代號)。清除時記到行程代號就只認它:還活著就不清,不管代號換沒換,因為舊程式碼還在它的記憶體裡;它走了就清,續接原代號的 resume 也算。追不到行程代號時退回結束記號,要求代號換了而且舊的那個工作階段寫出過 `.end`。舊版寫的閘門沒有這兩個欄位,一律清,維持改版前的行為。核對命令名不只看行程還在:行程代號會被回收,回收後那個號碼照樣「活著」。
|
||||
|
||||
判準原本寫在 `session-timer.sh`,用的是「起始檔不存在=行程是新起的」。實測打掉了那個等式:還沒重啟的工作階段生出來的子行程(`claude -p`、外掛子命令、子代理)拿到的是一個沒見過的代號,一觸發就把閘門清掉;反過來,續接原代號的 resume 行程確實換過了卻連問都不會問。那個條件兩頭都會答錯,所以判定整段搬到 `restart-gate.sh`,`session-timer.sh` 只負責問。
|
||||
|
||||
欄位只用在擋人訊息上。判定看的是「當前 CLI 那份檔案在不在」——檔案存在就是這一支還沒重啟過的證據,欄位缺了只讓訊息少幾個字。別支 CLI 那幾份一律不看。狀態檔讀不到、CLI 代號取不到、技能名取不到一律放行,理由與 `version-guard.sh` 相同。
|
||||
|
||||
@@ -133,7 +137,7 @@ Claude 由 `hooks/hooks.json` 自動接線十支 hook;其他 CLI 用 `hooks-in
|
||||
|
||||
有幾行就代表有幾支 CLI 還沒重啟;一份都沒有就不印。欄位缺值時只留鍵名(例如 `domains=`)。第一欄印 `legacy` 的那一行代表下面說的舊格式單一檔案,它不屬於任何一支 CLI。
|
||||
|
||||
**舊檔相容(過渡用)。** 舊版把狀態寫進 `$JSC_HOME/restart-required` 單一檔案。改用狀態目錄的第一輪部署,機器上可能還留著那份舊檔,所以判定與清除都認它:舊檔存在就一律擋,視為「每一支 CLI 都有未重啟的部署」,擋人訊息會標明這是舊格式紀錄;`clear` 除了刪當前 CLI 那一份,也一併刪掉舊檔。取捨講白:`clear` 只在新工作階段被呼叫,呼叫到就代表確實有一支 CLI 重新啟動過了;舊檔沒有 per-CLI 資訊,留著會讓五支 CLI 一路被擋到有人手動刪,刪掉是唯一收斂的做法,代價是同一輪部署的其他 CLI 少擋一次,只影響改用狀態目錄的那一輪。這一段相容邏輯在所有機器都跑過一次寫狀態目錄的部署與重啟之後就可以整段移除,屆時舊檔不會再被寫出來。
|
||||
**舊檔相容(過渡用)。** 舊版把狀態寫進 `$JSC_HOME/restart-required` 單一檔案。改用狀態目錄的第一輪部署,機器上可能還留著那份舊檔,所以判定與清除都認它:舊檔存在就一律擋,視為「每一支 CLI 都有未重啟的部署」,擋人訊息會標明這是舊格式紀錄;`clear` 除了刪當前 CLI 那一份,也一併刪掉舊檔。取捨講白:舊檔沒有 per-CLI 資訊,也沒有行程代號可以問,留著會讓五支 CLI 一路被擋到有人手動刪,刪掉是唯一收斂的做法。所以它不走上面那道行程存活判定,一被問到就刪,代價是同一輪部署的其他 CLI 少擋一次,只影響改用狀態目錄的那一輪。這一段相容邏輯在所有機器都跑過一次寫狀態目錄的部署與重啟之後就可以整段移除,屆時舊檔不會再被寫出來。
|
||||
|
||||
> `version-guard.sh report` 是非 hook 的子指令:印出每個已安裝 jsc plugin 的
|
||||
> 「{domain} {本機} {遠端} {落後|最新|超前|查詢失敗}」,最後一行 `behind {落後個數}`。
|
||||
|
||||
Reference in New Issue
Block a user