Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d105f4c1da | ||
|
|
49c9009090 | ||
|
|
730f21a0e8 | ||
|
|
800a899239 | ||
|
|
b5107563dd | ||
|
|
b0e352ae15 | ||
|
|
9de64ead7e | ||
|
|
59844c0a18 | ||
|
|
6f8afb8ac8 | ||
|
|
ea50b7d915 | ||
|
|
3cf8d82ffd | ||
|
|
113df37808 | ||
|
|
417fe1a413 | ||
|
|
fbf0aec9f7 | ||
|
|
462264a253 |
@@ -13,6 +13,14 @@
|
|||||||
},
|
},
|
||||||
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jsc-assist",
|
||||||
|
"source": {
|
||||||
|
"source": "url",
|
||||||
|
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
|
||||||
|
},
|
||||||
|
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-cli",
|
"name": "jsc-cli",
|
||||||
"source": {
|
"source": {
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
},
|
},
|
||||||
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jsc-assist",
|
||||||
|
"source": {
|
||||||
|
"source": "url",
|
||||||
|
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
|
||||||
|
},
|
||||||
|
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-cli",
|
"name": "jsc-cli",
|
||||||
"source": {
|
"source": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.3.4",
|
"version": "0.3.8",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"hooks": "./hooks/codex-hooks.json",
|
"hooks": "./hooks/codex-hooks.json",
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.3.4",
|
"version": "0.3.8",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| `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 這個保守預設 |
|
| `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 這個保守預設 |
|
||||||
| `hooks/version-guard.sh` | PreToolUse:claude matcher `Skill`、codex matcher `Bash`、copilot matcher `skill`、antigravity matcher `^view_file$` 加 `PreInvocation`;kiro `userPromptSubmit`(只注入警告) | 技能使用前的版本前置檢查,擋兩種情況,兩種都擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給;技能名解析交給 `hooks/skill-name.sh`、阻擋輸出形態交給 `hooks/deny.sh`,兩支的規則見上面兩列,這裡不重寫第二套):一是本機**實際載入**版本落後遠端發佈版本,二是技能所屬 plugin 的 manifest 在 `jsc.requires` 宣告的相依 plugin 版本落後——相依那一項讀 `installPath` 底下那份 `plugin.json`,逐項比對相依 plugin 的本機實際載入版本,訊息講明哪一個 plugin、需要哪一版、目前哪一版、怎麼補。相依檢查排在遠端比對之前,全部讀本機檔案,離線也判得動;判定邏輯自己實作,不呼叫 `jsc-cli/tools/check-requires.sh`,免得 hook 散落到別的 domain,也免得跟已宣告相依 `jsc-hooks` 的 `jsc-cli` 做出循環相依。部署那端照樣更新、只回報,阻擋落在這支 hook。兩種都只擋確定落後:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本、解不出安裝路徑、讀不到 manifest、manifest 沒有 `jsc.requires`、讀不到相依 plugin 的本機載入版本也一律放行。遠端版本快取在 `$JSC_HOME/version-cache/{CLI 代號}/{domain}`,一支 CLI 一份;舊路徑 `$JSC_HOME/version-cache/{domain}` 會在第一次讀取時複製到新路徑。逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-hooks:repair`、`jsc-cli:models`、`jsc-meta:*`、`jsc-ask:ask`、`jsc-gitea:wiki`——共 7 項,兩種擋人情況共用同一份,相依落後不另立短清單;清單的唯一來源是 `hooks/version-guard.sh` 的檔頭,那裡一項一個理由 |
|
| `hooks/version-guard.sh` | PreToolUse:claude matcher `Skill`、codex matcher `Bash`、copilot matcher `skill`、antigravity matcher `^view_file$` 加 `PreInvocation`;kiro `userPromptSubmit`(只注入警告) | 技能使用前的版本前置檢查,擋兩種情況,兩種都擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給;技能名解析交給 `hooks/skill-name.sh`、阻擋輸出形態交給 `hooks/deny.sh`,兩支的規則見上面兩列,這裡不重寫第二套):一是本機**實際載入**版本落後遠端發佈版本,二是技能所屬 plugin 的 manifest 在 `jsc.requires` 宣告的相依 plugin 版本落後——相依那一項讀 `installPath` 底下那份 `plugin.json`,逐項比對相依 plugin 的本機實際載入版本,訊息講明哪一個 plugin、需要哪一版、目前哪一版、怎麼補。相依檢查排在遠端比對之前,全部讀本機檔案,離線也判得動;判定邏輯自己實作,不呼叫 `jsc-cli/tools/check-requires.sh`,免得 hook 散落到別的 domain,也免得跟已宣告相依 `jsc-hooks` 的 `jsc-cli` 做出循環相依。部署那端照樣更新、只回報,阻擋落在這支 hook。兩種都只擋確定落後:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本、解不出安裝路徑、讀不到 manifest、manifest 沒有 `jsc.requires`、讀不到相依 plugin 的本機載入版本也一律放行。遠端版本快取在 `$JSC_HOME/version-cache/{CLI 代號}/{domain}`,一支 CLI 一份;舊路徑 `$JSC_HOME/version-cache/{domain}` 會在第一次讀取時複製到新路徑。逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-hooks:repair`、`jsc-cli:models`、`jsc-meta:*`、`jsc-ask:ask`、`jsc-gitea:wiki`——共 7 項,兩種擋人情況共用同一份,相依落後不另立短清單;清單的唯一來源是 `hooks/version-guard.sh` 的檔頭,那裡一項一個理由 |
|
||||||
| `hooks/restart-gate.sh` | PreToolUse:claude matcher `Skill`、codex matcher `Bash`、copilot matcher `skill`、antigravity matcher `^view_file$` 加 `PreInvocation`;kiro `userPromptSubmit`(只注入警告) | 部署後強制重啟閘門:`$JSC_HOME/restart-required.d/{CLI 代號}` 一支 CLI 一份,當前 CLI 那份存在時擋下 jsc 技能呼叫,並印出要重新啟動哪一支 CLI(技能名解析交給 `hooks/skill-name.sh`、阻擋輸出形態交給 `hooks/deny.sh`,兩支的規則見上面兩列);別支 CLI 那幾份不影響這一支。狀態檔由 `jsc-cli:deploy` 在 install 或 update 收尾時經 `restart-gate.sh require {install|update} [{domain}...]` 寫入當前 CLI 那一份,在下一個工作階段開始時由 `session-timer.sh` 呼叫 `restart-gate.sh clear` 只清除那一份。判定看檔案在不在:狀態檔讀不到、CLI 代號取不到、技能名取不到都放行(理由與 `version-guard.sh` 一致,只擋確定違規)。舊格式的單一檔案 `$JSC_HOME/restart-required` 存在時一律擋,`clear` 會一併刪掉它(過渡相容,詳見下面「部署後重啟狀態檔」)。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-hooks:repair`、`jsc-gitea:wiki`、`jsc-log:worklog`、`jsc-log:learn`、`jsc-meta:*`、`jsc-ask:ask`、`jsc-git:pr`、`jsc-git:commit`——部署後還要寫得完技能組異動報告與工作日誌,hook 壞掉也要修得回來,整批擋下去這些規則會互相打死。清單認技能名不認呼叫鏈,後三支是為了讓前七支走得完才補進來的:`deploy` 要問模式、報告寫完要開 PR。另有唯讀子指令 `report`,一支 CLI 一行印出每一份狀態檔的內容,看得出還有哪幾支沒重啟。逃生門 `JSC_RESTART_GATE=off` |
|
| `hooks/restart-gate.sh` | PreToolUse:claude matcher `Skill`、codex matcher `Bash`、copilot matcher `skill`、antigravity matcher `^view_file$` 加 `PreInvocation`;kiro `userPromptSubmit`(只注入警告) | 部署後強制重啟閘門:`$JSC_HOME/restart-required.d/{CLI 代號}` 一支 CLI 一份,當前 CLI 那份存在時擋下 jsc 技能呼叫,並印出要重新啟動哪一支 CLI(技能名解析交給 `hooks/skill-name.sh`、阻擋輸出形態交給 `hooks/deny.sh`,兩支的規則見上面兩列);別支 CLI 那幾份不影響這一支。狀態檔由 `jsc-cli:deploy` 在 install 或 update 收尾時經 `restart-gate.sh require {install|update} [{domain}...]` 寫入當前 CLI 那一份,在下一個工作階段開始時由 `session-timer.sh` 呼叫 `restart-gate.sh clear` 只清除那一份。判定看檔案在不在:狀態檔讀不到、CLI 代號取不到、技能名取不到都放行(理由與 `version-guard.sh` 一致,只擋確定違規)。舊格式的單一檔案 `$JSC_HOME/restart-required` 存在時一律擋,`clear` 會一併刪掉它(過渡相容,詳見下面「部署後重啟狀態檔」)。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-hooks:repair`、`jsc-gitea:wiki`、`jsc-log:worklog`、`jsc-log:learn`、`jsc-meta:*`、`jsc-ask:ask`、`jsc-git:pr`、`jsc-git:commit`——部署後還要寫得完技能組異動報告與工作日誌,hook 壞掉也要修得回來,整批擋下去這些規則會互相打死。清單認技能名不認呼叫鏈,後三支是為了讓前七支走得完才補進來的:`deploy` 要問模式、報告寫完要開 PR。另有唯讀子指令 `report`,一支 CLI 一行印出每一份狀態檔的內容,看得出還有哪幾支沒重啟。逃生門 `JSC_RESTART_GATE=off` |
|
||||||
|
| `hooks/heartbeat.sh` | 不接線,由 `jsc-assist` 的助理主體、系統排程與 `status` 技能呼叫 | 助理心跳檔 `$JSC_HOME/assistant/heartbeat` 的讀寫工具,純文字 key=value,欄位 `ts`、`pid`、`cli`、`session`。四個子命令:`write` 寫入四個欄位(目錄不存在就建,先寫暫存檔再改名,讀的那一端永遠讀到完整的一份)、`check` 判定新不新鮮(什麼都不印,結果只在結束碼)、`report` 印一行現況供 `status` 技能與擋人訊息取用、`clear` 刪除心跳檔(由助理的 `stop` 呼叫,檔案不存在也算成功)。新鮮的判準只有一條:心跳檔存在、而且 `ts` 距現在小於門檻秒數,門檻預設 300(心跳週期 60 秒的五倍,一次卡頓不會誤判),可用 `JSC_ASSISTANT_HEARTBEAT_TTL` 覆寫。**絕不看 pid 存活**:五支 CLI 與容器裡的行程互相看不到彼此的 pid,問了會把活著的判成停了,pid 又會被回收,反過來把停掉的判成還在跑,兩種誤判都不報錯;pid 只當擋人訊息的線索。`check` 的結束碼分四種讓呼叫端各自處置:0 新鮮、1 過期(跑過但停了)、3 心跳檔不存在(從沒啟動過)、4 檔案在但 `ts` 讀不出來(檔案壞了);`write` 與 `clear` 的檔案系統失敗回 5,用法錯誤回 6。四個子命令都不讀標準輸入——這支不是 hook,是被工具端呼叫的腳本,讀了會在管線沒人關閉時整支卡死。逐碼意義與 `report` 的欄位順序見腳本檔頭 |
|
||||||
|
| `hooks/assistant-gate.sh` | **這一版尚未接線。** 接線位置比照 `restart-gate.sh`(PreToolUse,claude matcher `Skill`、codex matcher `Bash`、copilot matcher `skill`、antigravity matcher `^view_file$` 加 `PreInvocation`;kiro `userPromptSubmit` 只注入警告) | 助理運行閘門:助理沒在跑就擋下 jsc 技能呼叫。判定整段交給 `heartbeat.sh check`,本檔不自己讀心跳檔;訊息細節取自 `heartbeat.sh report`(技能名解析交給 `hooks/skill-name.sh`、阻擋輸出形態交給 `hooks/deny.sh`)。心跳新鮮放行;心跳不存在、過期、`ts` 讀不出來三種都擋,三種的訊息各寫一份——沒啟動過的要去啟動、跑過停了的要去查為什麼停、檔案壞了的要先 `stop` 再 `start` 重建,併成一句就會叫錯人做錯事。`heartbeat.sh` 回 2(腳本沒跑起來)、5(檔案系統失敗)、6(用法錯誤)一律放行:那三碼是判定機制自己壞了,不是「助理沒在跑」的證據,而且 5 正是磁碟滿或權限壞的訊號,擋下去會把全機器整組技能鎖死、連豁免那幾支也修不動。**這是整組技能唯一一道 fail-closed 閘門**(其餘 hook 一律資料不足就放行),所以逃生門與豁免清單是它能上線的前提,不是選配。豁免 `jsc-assist:*`(啟動助理本身就是一次技能呼叫,少了它整組鎖死)、`jsc-hooks:repair`、`jsc-hooks:hooks-install`、`jsc-cli:doctor`、`jsc-cli:setup`、`jsc-cli:deploy`、`jsc-gitea:wiki`、`jsc-ask:ask`、`jsc-git:commit`、`jsc-git:pr`、`jsc-cli:models`。清單認技能名不認呼叫鏈,後五支是為了讓前六支走得完才補進來的,其中 `jsc-gitea:wiki` 最容易漏:巡檢一輪要先把結果寫進 `MONITOR_{HASH}` 才寫心跳,擋了它就變成「沒心跳 → 擋 wiki → 巡檢不完 → 還是沒心跳」自己咬住自己。逃生門 `JSC_ASSISTANT_GATE=off`,判斷擺在載入 `lib.sh` 之前——`lib.sh` 讀不到時 sh 回 2 等於無聲擋下每一次呼叫,逃生門也會跟著跑不到 |
|
||||||
| `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 |
|
| `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 |
|
||||||
| `hooks/comment-scope.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 程式碼註解不得夾帶文件相關資訊與審查流程痕跡,共三種模式。`prompt`:在每次提示注入規則摘要(禁止項與白名單各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。掃描時機每個 CLI 不同——claude 逐檔即時(PostToolUse)、codex 每輪結束(`notify`)、kiro 每輪提示送出時(`userPromptSubmit`,掃的是上一輪寫的檔)、copilot 與 antigravity 只有工作階段結束時由 `tools/jsc-wrap.sh` 收尾掃一次。兩種掃描模式都只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入,檔案已經寫好了)。markdown、純文字、資料檔與二進位檔一律跳過。只實作可用樣式判定的項目,專案代號、客戶名稱這類判不出來的交給 `/jsc-review:code-review`。規則正文的唯一來源在 `jsc-review` 的 `references/comment-scope.md`,本存取庫不留副本。逃生門 `JSC_COMMENT_SCOPE=off` |
|
| `hooks/comment-scope.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 程式碼註解不得夾帶文件相關資訊與審查流程痕跡,共三種模式。`prompt`:在每次提示注入規則摘要(禁止項與白名單各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。掃描時機每個 CLI 不同——claude 逐檔即時(PostToolUse)、codex 每輪結束(`notify`)、kiro 每輪提示送出時(`userPromptSubmit`,掃的是上一輪寫的檔)、copilot 與 antigravity 只有工作階段結束時由 `tools/jsc-wrap.sh` 收尾掃一次。兩種掃描模式都只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入,檔案已經寫好了)。markdown、純文字、資料檔與二進位檔一律跳過。只實作可用樣式判定的項目,專案代號、客戶名稱這類判不出來的交給 `/jsc-review:code-review`。規則正文的唯一來源在 `jsc-review` 的 `references/comment-scope.md`,本存取庫不留副本。逃生門 `JSC_COMMENT_SCOPE=off` |
|
||||||
| `hooks/lang-guard.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 所有非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字,共三種模式。`prompt`:在每次提示注入規則摘要(適用範圍與自我檢查各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。接線位置與掃描時機跟 `comment-scope.sh` 完全一樣,見下面那張表。偵測三項:簡體字(字表在 `hooks/simplified.txt`,讀不到就安靜跳過這一項)、亂碼(U+FFFD 替代字元與雙重編碼殘骸)、非 UTF-8 編碼(用 `iconv` 判定,沒有 `iconv` 就跳過)。三項都掃整個檔案、不只掃註解行,`.md` 與純文字檔照掃——那些正是「非程式碼輸出」的主場,這兩點跟 `comment-scope.sh` 刻意不同。掃描深度仍只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入)。二進位檔(只認 NUL 位元組)與 `*.lock`、`*.min.js`、`*.map` 這類產生檔跳過;`hooks/simplified.txt`、`hooks/ste100-guard.sh`、`hooks/lang-guard.sh` 也跳過,那三份檔案裡的簡體字與亂碼樣本是被討論的對象,不是被使用。規則正文的唯一來源在 `jsc-meta` 的 `references/ste100.md`。逃生門 `JSC_LANG_GUARD=off` |
|
| `hooks/lang-guard.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 所有非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字,共三種模式。`prompt`:在每次提示注入規則摘要(適用範圍與自我檢查各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。接線位置與掃描時機跟 `comment-scope.sh` 完全一樣,見下面那張表。偵測三項:簡體字(字表在 `hooks/simplified.txt`,讀不到就安靜跳過這一項)、亂碼(U+FFFD 替代字元與雙重編碼殘骸)、非 UTF-8 編碼(用 `iconv` 判定,沒有 `iconv` 就跳過)。三項都掃整個檔案、不只掃註解行,`.md` 與純文字檔照掃——那些正是「非程式碼輸出」的主場,這兩點跟 `comment-scope.sh` 刻意不同。掃描深度仍只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入)。二進位檔(只認 NUL 位元組)與 `*.lock`、`*.min.js`、`*.map` 這類產生檔跳過;`hooks/simplified.txt`、`hooks/ste100-guard.sh`、`hooks/lang-guard.sh` 也跳過,那三份檔案裡的簡體字與亂碼樣本是被討論的對象,不是被使用。規則正文的唯一來源在 `jsc-meta` 的 `references/ste100.md`。逃生門 `JSC_LANG_GUARD=off` |
|
||||||
@@ -166,7 +168,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `tools/jsc-wrap.sh` | 沒有完整 hook 系統的 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填,再依序跑一次 `comment-scope.sh sweep` 與 `lang-guard.sh sweep` 掃整個 git 工作區的註解範圍與繁中編碼(copilot 與 antigravity 沒有任何逐輪事件,整個工作階段只有這裡掃得到)。兩次收尾掃描一律不影響結束碼:包裝器原樣回傳 CLI 自己的結束碼,`sweep` 命中只把警告印到 stderr。`JSC_CLI` 存 CLI 代號,實際執行的是對應的執行檔(antigravity 是 agy、kiro 是 kiro-cli) |
|
| `tools/jsc-wrap.sh` | 沒有完整 hook 系統的 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填,再依序跑一次 `comment-scope.sh sweep` 與 `lang-guard.sh sweep` 掃整個 git 工作區的註解範圍與繁中編碼(copilot 與 antigravity 沒有任何逐輪事件,整個工作階段只有這裡掃得到)。兩次收尾掃描一律不影響結束碼:包裝器原樣回傳 CLI 自己的結束碼,`sweep` 命中只把警告印到 stderr。`JSC_CLI` 存 CLI 代號,實際執行的是對應的執行檔(antigravity 是 agy、kiro 是 kiro-cli) |
|
||||||
| `tools/scan-logs.sh` | 離線回填:解析 copilot、antigravity、codex 的原生日誌,把技能用量與階段界線補進 `$JSC_HOME`,重掃不重複 |
|
| `tools/scan-logs.sh` | 離線回填:解析 copilot、antigravity、codex 的原生日誌,把技能用量與階段界線補進 `$JSC_HOME`,重掃不重複 |
|
||||||
| `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。目錄頁一律先讀回舊頁再附加新列、整頁寫回,不整頁覆蓋:只有 `wiki-get` 回 4(頁面真的不存在)才用範本建新頁,回 7(金鑰失效)或 8(其他 API 失敗)代表舊內容未知,放棄目錄頁寫入並以 exit 4 回報,免得拿範本蓋掉所有既有列。wiki 位置由 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR` 解析,解析不出來就安靜降級。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) |
|
| `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。目錄頁一律先讀回舊頁再附加新列、整頁寫回,不整頁覆蓋:只有 `wiki-get` 回 4(頁面真的不存在)才用範本建新頁,回 7(金鑰失效)或 8(其他 API 失敗)代表舊內容未知,放棄目錄頁寫入並以 exit 4 回報,免得拿範本蓋掉所有既有列。目錄頁指向異常頁的連結用 `gitea.sh wiki-url` 的絕對網址,跨存取庫的 `[[頁名]]` 解不開。網址在異常頁寫成功之後才取:頁名的 hash 帶時間戳,每次回報都是全新的頁,寫進去之前查一定是 404,先查就只拿得到空字串。取不到網址時只印頁名,原因走 stderr,結束碼照舊回 0。wiki 位置分兩次解析:異常頁走 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR`,目錄頁走 `gitea.sh wiki-repo CONTENTS`,兩者是兩個不同的存取庫。異常頁的存取庫解不出來就整支安靜降級;只有目錄頁的存取庫解不出來,就只寫異常頁、跳過目錄頁更新,仍回 exit 0。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) |
|
||||||
| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共四個用法。`{cli}` 是接線:先建立或更新 `$JSC_HOME/current/jsc-hooks` 指向目前這版 plugin,接著把對應的設定編輯、包裝別名安裝、hook 檔建立成穩定路徑,皆以 `<!-- jsc-hooks -->`(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、`.codex-plugin/plugin.json` 的 matcher 必須是 `Bash`、copilot 必須是小寫 `skill` 且沒有第二種大小寫的事件名、antigravity 的 matcher 必須帶錨點 `^view_file$` 且有 `PreInvocation`、kiro 的 agent JSON 必須成對且 `hooks`、`resources`、`tools` 在最上層並含兩層 `skill://` glob),也會確認寫入路徑能解到既有腳本。matcher 本身要單獨驗:鍵在、matcher 卻錯的形態最難查,回報會說接好了,實際一次都不會被叫用。檔案系統不能建立 symlink 時,會明確回報並退回目前根目錄,不會靜默寫出壞路徑。`status=wired\|degraded\|skipped\|failed` 回報接線結果。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除;移除標記段落時會先去掉標記行前後空白,所以縮排或尾端補空白的 jsc 區塊一樣會移除;移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:九支 hook 的每個接線模式各跑一次,非零退出即為錯誤,另外把五支 CLI 的真實負載各餵進 `skill-name.sh` 一次驗技能名解析、四種阻擋形態各驗一次 `deny.sh`,再把那些負載直接餵進 `restart-gate.sh` 驗「解析→判定→輸出形態」整條串得起來(含 fail-open、豁免放行與 kiro 的注入路徑)——前兩組分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前三支 CLI 失效的樣子;另外用一份暫時的 `$JSC_HOME` 狀態檔把工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼,再用一份暫時的 `HOME`(假的 `installed_plugins.json` 與各 plugin 的 manifest)把 `version-guard.sh` 相依版本檢查的每條路徑跑一次——相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、逃生門,另加一條回歸:多行縮排的 manifest,`jsc.requires` 的最後一個鍵也要解得到。驗的是判定結果本身,不只是腳本跑得完(例外有四個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh`、`lang-guard.sh` 掃描模式與 `write-guard.sh` 三種模式的 exit 2 是命中違規的設計行為——`sweep` 在髒工作區本來就會回 2,`write-guard.sh` 在機器剛好鎖在 `plan` 階段時也會回 2,都不算 hook 壞掉),以 `status=ok\|failed` 回報。**結果行數由腳本自己數、自己斷言**:`status=` 之後緊接一行 `lines<TAB>{數量}`,那是其後 `[jsc]` 結果行的實際條數,與腳本內逐類宣告的預期條數比對,不符就回非零。判定路徑增減時只改腳本裡的預期值,散文一律引用這一行,不另外抄一份數字。`status {cli}` 是唯讀盤點:只讀設定檔判斷段落與 matcher 對不對,不寫檔也不執行 hook,claude、codex、copilot、antigravity 回 `wired`,kiro 回 `degraded` 並在 `reason` 講明那是 CLI 限制;每個接線點印一行 `item<TAB>{項目}<TAB>{路徑}<TAB>{present\|missing\|unverified}`,也會把帶版號快取路徑、開發存取庫路徑與不存在的腳本列為缺項。狀態有三格不是兩格:`unverified` 是「這一項驗不了」,只有 `missing` 才算缺項——`kiro-cli agent validate` 在沒登入時印的是環境問題,不是這個檔案的問題,報 `present` 會讓沒驗到的東西看起來像通過,報 `missing` 會把沒登入算成接線缺漏;`status claude` 讀 Claude Code 實際載入的 `installed_plugins.json`,不再檢查目前腳本旁邊那份 `hooks.json`。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境;那道限制另有程式層把關,`JSC_READONLY=1` 之下只准 `status` 與 `smoke`,`purge` 與接線一律以 exit 6 拒絕並回報 `status=readonly`,環境不會被動到 |
|
| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共四個用法。`{cli}` 是接線:先建立或更新 `$JSC_HOME/current/jsc-hooks` 指向目前這版 plugin,接著把對應的設定編輯、包裝別名安裝、hook 檔建立成穩定路徑,皆以 `<!-- jsc-hooks -->`(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、`.codex-plugin/plugin.json` 的 matcher 必須是 `Bash`、copilot 必須是小寫 `skill` 且沒有第二種大小寫的事件名、antigravity 的 matcher 必須帶錨點 `^view_file$` 且有 `PreInvocation`、kiro 的 agent JSON 必須成對且 `hooks`、`resources`、`tools` 在最上層並含兩層 `skill://` glob),也會確認寫入路徑能解到既有腳本。matcher 本身要單獨驗:鍵在、matcher 卻錯的形態最難查,回報會說接好了,實際一次都不會被叫用。檔案系統不能建立 symlink 時,會明確回報並退回目前根目錄,不會靜默寫出壞路徑。`status=wired\|degraded\|skipped\|failed` 回報接線結果。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除;移除標記段落時會先去掉標記行前後空白,所以縮排或尾端補空白的 jsc 區塊一樣會移除;移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:九支 hook 的每個接線模式各跑一次,非零退出即為錯誤,另外把五支 CLI 的真實負載各餵進 `skill-name.sh` 一次驗技能名解析、四種阻擋形態各驗一次 `deny.sh`,再把那些負載直接餵進 `restart-gate.sh` 驗「解析→判定→輸出形態」整條串得起來(含 fail-open、豁免放行與 kiro 的注入路徑)——前兩組分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前三支 CLI 失效的樣子;另外用一份暫時的 `$JSC_HOME` 狀態檔把工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼,再用一份暫時的 `HOME`(假的 `installed_plugins.json` 與各 plugin 的 manifest)把 `version-guard.sh` 相依版本檢查的每條路徑跑一次——相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、逃生門,另加一條回歸:多行縮排的 manifest,`jsc.requires` 的最後一個鍵也要解得到。驗的是判定結果本身,不只是腳本跑得完(例外有四個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh`、`lang-guard.sh` 掃描模式與 `write-guard.sh` 三種模式的 exit 2 是命中違規的設計行為——`sweep` 在髒工作區本來就會回 2,`write-guard.sh` 在機器剛好鎖在 `plan` 階段時也會回 2,都不算 hook 壞掉),以 `status=ok\|failed` 回報。**結果行數由腳本自己數、自己斷言**:`status=` 之後緊接一行 `lines<TAB>{數量}`,那是其後 `[jsc]` 結果行的實際條數,與腳本內逐類宣告的預期條數比對,不符就回非零。判定路徑增減時只改腳本裡的預期值,散文一律引用這一行,不另外抄一份數字。`status {cli}` 是唯讀盤點:只讀設定檔判斷段落與 matcher 對不對,不寫檔也不執行 hook,claude、codex、copilot、antigravity 回 `wired`,kiro 回 `degraded` 並在 `reason` 講明那是 CLI 限制;每個接線點印一行 `item<TAB>{項目}<TAB>{路徑}<TAB>{present\|missing\|unverified}`,也會把帶版號快取路徑、開發存取庫路徑與不存在的腳本列為缺項。狀態有三格不是兩格:`unverified` 是「這一項驗不了」,只有 `missing` 才算缺項——`kiro-cli agent validate` 在沒登入時印的是環境問題,不是這個檔案的問題,報 `present` 會讓沒驗到的東西看起來像通過,報 `missing` 會把沒登入算成接線缺漏;`status claude` 讀 Claude Code 實際載入的 `installed_plugins.json`,不再檢查目前腳本旁邊那份 `hooks.json`。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境;那道限制另有程式層把關,`JSC_READONLY=1` 之下只准 `status` 與 `smoke`,`purge` 與接線一律以 exit 6 拒絕並回報 `status=readonly`,環境不會被動到 |
|
||||||
| `tools/scan-hook-errors.sh` | 掃 CLI 原生紀錄找 hook 的執行期錯誤(接線寫對、跑起來出錯)。只有 claude 有 hook 結果紀錄,掃 `~/.claude/projects/**/*.jsonl` 的 `hook_non_blocking_error` 與非空 `hookErrors`;codex、copilot、antigravity、kiro 沒有等價紀錄,一律回報 `unavailable` 並指向 `wire-cli.sh smoke {cli}`。每筆錯誤附加一行 JSON 到 `$JSC_HOME/errors/hooks.jsonl`,`jsc` 欄位標明是不是 jsc 自己的 hook(第三方 hook 的錯誤只回報,不由 jsc 修正);去重與 `scan-logs.sh` 同法,重掃只讀新增段落,以 `status=clean\|errors\|unavailable` 回報 |
|
| `tools/scan-hook-errors.sh` | 掃 CLI 原生紀錄找 hook 的執行期錯誤(接線寫對、跑起來出錯)。只有 claude 有 hook 結果紀錄,掃 `~/.claude/projects/**/*.jsonl` 的 `hook_non_blocking_error` 與非空 `hookErrors`;codex、copilot、antigravity、kiro 沒有等價紀錄,一律回報 `unavailable` 並指向 `wire-cli.sh smoke {cli}`。每筆錯誤附加一行 JSON 到 `$JSC_HOME/errors/hooks.jsonl`,`jsc` 欄位標明是不是 jsc 自己的 hook(第三方 hook 的錯誤只回報,不由 jsc 修正);去重與 `scan-logs.sh` 同法,重掃只讀新增段落,以 `status=clean\|errors\|unavailable` 回報 |
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
|||||||
| 範本 | 用途 |
|
| 範本 | 用途 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `templates/error-page.md` | 單筆 hook 異常頁 `ERROR_{HASH}`,記錄當次失敗的觸發條件、錯誤摘要與處理結果。 |
|
| `templates/error-page.md` | 單筆 hook 異常頁 `ERROR_{HASH}`,記錄當次失敗的觸發條件、錯誤摘要與處理結果。 |
|
||||||
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。 |
|
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。落在目錄專用存取庫,一律 upsert 附加,連結用絕對網址。 |
|
||||||
|
|
||||||
## Skills 目錄
|
## Skills 目錄
|
||||||
|
|
||||||
@@ -201,7 +203,8 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
|||||||
| 變數 | 用途 | 未設定時 |
|
| 變數 | 用途 | 未設定時 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
||||||
| `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
|
| `JSC_WIKI_REPO_ERROR` | 異常內容頁 `ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO`;還是解不出來就整支 `tools/report-error.sh` 安靜降級,不寫 wiki |
|
||||||
|
| `JSC_WIKI_REPO_CONTENTS` | 目錄頁 `ERROR_CONTENTS` 所在的 `{owner}/{repo}`。目錄頁與內容頁分屬兩個不同的存取庫,各解各的 | 退回 `JSC_WIKI_REPO`;還是解不出來就只寫內容頁、跳過目錄頁更新,`tools/report-error.sh` 仍回 exit 0 |
|
||||||
| `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | `tools/report-error.sh` 安靜降級,不寫 wiki |
|
| `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | `tools/report-error.sh` 安靜降級,不寫 wiki |
|
||||||
| `COPILOT_HOME` | copilot 的設定根目錄,`hooks/jsc-hooks.json` 與指引檔都寫在它底下 | 預設 `~/.copilot` |
|
| `COPILOT_HOME` | copilot 的設定根目錄,`hooks/jsc-hooks.json` 與指引檔都寫在它底下 | 預設 `~/.copilot` |
|
||||||
| `KIRO_HOME` | kiro 的設定根目錄,`agents/jsc.json`、`settings/cli.json` 與 `resources` 的 `skill://` glob 都由它推導 | 預設 `~/.kiro` |
|
| `KIRO_HOME` | kiro 的設定根目錄,`agents/jsc.json`、`settings/cli.json` 與 `resources` 的 `skill://` glob 都由它推導 | 預設 `~/.kiro` |
|
||||||
@@ -217,6 +220,8 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
|||||||
| `JSC_LANG_GUARD` | 設 `off` 完全略過繁中與編碼檢查(`lang-guard.sh` 三種模式都直接結束) | 啟用檢查 |
|
| `JSC_LANG_GUARD` | 設 `off` 完全略過繁中與編碼檢查(`lang-guard.sh` 三種模式都直接結束) | 啟用檢查 |
|
||||||
| `JSC_WRITE_GUARD` | 設 `off` 完全略過寫入與提交閘門(`write-guard.sh` 三種模式都直接結束) | 啟用閘門 |
|
| `JSC_WRITE_GUARD` | 設 `off` 完全略過寫入與提交閘門(`write-guard.sh` 三種模式都直接結束) | 啟用閘門 |
|
||||||
| `JSC_WRITE_GUARD_TTL` | `write-guard.sh review` 判定「稽核技能還在跑」的時效秒數 | 預設 900 |
|
| `JSC_WRITE_GUARD_TTL` | `write-guard.sh review` 判定「稽核技能還在跑」的時效秒數 | 預設 900 |
|
||||||
|
| `JSC_ASSISTANT_HEARTBEAT_TTL` | `hooks/heartbeat.sh check` 判定心跳新鮮的門檻秒數。值不是正整數就退回預設值 | 預設 300(心跳週期 60 秒的五倍) |
|
||||||
|
| `JSC_ASSISTANT_GATE` | 設 `off` 完全略過助理運行閘門(`assistant-gate.sh` 一律放行)。判斷擺在載入 `lib.sh` 之前,那支函式庫讀不到時逃生門照樣有效 | 啟用閘門 |
|
||||||
| `JSC_READONLY` | 設 `1` 時 `tools/wire-cli.sh` 只准 `status` 與 `smoke`,`purge` 與接線一律拒絕並回 exit 6 | 四個用法都可執行 |
|
| `JSC_READONLY` | 設 `1` 時 `tools/wire-cli.sh` 只准 `status` 與 `smoke`,`purge` 與接線一律拒絕並回 exit 6 | 四個用法都可執行 |
|
||||||
| `JSC_CHANGED_FILE` | 非 Claude CLI 要掃描的檔案路徑,代替 stdin JSON 的 `file_path`,供 `comment-scope.sh` 與 `lang-guard.sh` 使用 | 安靜降級,不掃描 |
|
| `JSC_CHANGED_FILE` | 非 Claude CLI 要掃描的檔案路徑,代替 stdin JSON 的 `file_path`,供 `comment-scope.sh` 與 `lang-guard.sh` 使用 | 安靜降級,不掃描 |
|
||||||
| `JSC_TOOL_COMMAND` | 非 Claude CLI 要判定的 Bash 指令字串,代替 stdin JSON 的 `command`,供 `write-guard.sh commit` 使用 | 安靜降級,不判定 |
|
| `JSC_TOOL_COMMAND` | 非 Claude CLI 要判定的 Bash 指令字串,代替 stdin JSON 的 `command`,供 `write-guard.sh commit` 使用 | 安靜降級,不判定 |
|
||||||
|
|||||||
Executable
+204
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# assistant-gate.sh — 助理運行閘門(PreToolUse,matcher: Skill)。
|
||||||
|
#
|
||||||
|
# 助理在背景跑,前景會話看不到它。技能組有一批規則要靠它落地:巡檢、監控頁、待辦簿。助理停著
|
||||||
|
# 的時候那些規則沒有人執行,可是技能照樣叫得起來,看起來一切正常。這道閘門負責讓「助理沒在跑
|
||||||
|
# 就繼續用技能」擋在門外。
|
||||||
|
#
|
||||||
|
# --- 這一版尚未接線 ---
|
||||||
|
#
|
||||||
|
# 本檔沒有寫進 hooks/hooks.json、hooks/codex-hooks.json 與 tools/wire-cli.sh,五支 CLI 一支都不
|
||||||
|
# 會叫到它。要驗證請直接跑 `sh hooks/assistant-gate.sh`,餵環境變數與標準輸入。
|
||||||
|
#
|
||||||
|
# 接線的前提有兩條,兩條都成立才可以接:
|
||||||
|
# 1. 助理已經在跑——`jsc-assist:assistant` 的 start 跑過,排程項目確實裝上了。
|
||||||
|
# 2. 心跳穩定——`heartbeat.sh check` 連續多輪都回 0。排程寫進 crontab 不等於 cron 在跑,
|
||||||
|
# WSL 預設不啟動 cron,那種機器上心跳一拍都不會有。
|
||||||
|
# 這兩條沒確認就接線,下一次技能呼叫就會被擋,而且擋的是整台機器的五支 CLI。
|
||||||
|
#
|
||||||
|
# 結束碼(hook 模式,本檔只有這一個模式):
|
||||||
|
# 0 放行,或已經以不靠結束碼的形態擋下。所以「exit 0」在這支腳本有兩種意思。
|
||||||
|
# 放行的情況:逃生門 JSC_ASSISTANT_GATE=off、負載裡解不出技能名、解出來的不是 jsc 技能、
|
||||||
|
# 命中下方豁免清單、`heartbeat.sh check` 回 0(心跳新鮮)、`heartbeat.sh check` 回 2、5、6
|
||||||
|
# (判不出來,理由見下方「心跳結束碼怎麼處置」)。
|
||||||
|
# 已擋下但不靠結束碼的情況:antigravity 的 stdout deny JSON、kiro 的注入警告。
|
||||||
|
# 2 擋下該次技能呼叫(claude、codex、copilot,以及認不得的 CLI 代號)。
|
||||||
|
# 擋下時的輸出形態由 deny.sh 依當前 CLI 決定,本檔只負責判定與訊息內容:
|
||||||
|
# claude、codex、copilot 走 stderr 加 exit 2;antigravity 走 stdout 的 deny JSON,結束碼
|
||||||
|
# 固定 0(那支 CLI 的結束碼語意沒有文件,不可靠);kiro 擋不下來,改印警告後 exit 0。
|
||||||
|
# 本檔沒有其他結束碼,也沒有子命令。帶進來的參數一律忽略。
|
||||||
|
#
|
||||||
|
# 輸入:技能名一律由 skill-name.sh 從當前 CLI 的負載解析,環境變數 JSC_SKILL、SKILL 優先,
|
||||||
|
# 規則與 version-guard.sh、restart-gate.sh 共用同一份。不另外篩工具名:工具名每支 CLI 都不
|
||||||
|
# 一樣(Skill、Bash、skill、view_file),拿 Claude 的那一個當通用條件會把另外四支整批擋在判定
|
||||||
|
# 之外。
|
||||||
|
#
|
||||||
|
# --- 這是整組技能唯一一道 fail-closed 閘門 ---
|
||||||
|
#
|
||||||
|
# 其餘 hook 的原則都是「資料不足就放行」:version-guard.sh 查不到版本放行,restart-gate.sh 讀不
|
||||||
|
# 到狀態檔放行。這一道相反——心跳不存在就是助理沒在跑,照要求要擋。心跳檔不存在本身就是證據,
|
||||||
|
# 不是「資料不足」。
|
||||||
|
#
|
||||||
|
# 代價講白:$JSC_HOME 寫不進去的時候(磁碟滿、權限壞、掛載掉了)助理寫不出心跳,這道閘門就把
|
||||||
|
# 全機器五支 CLI 的整組技能一起停掉。所以逃生門與豁免清單不是選配,是這道閘門能上線的前提:
|
||||||
|
# 逃生門讓人在閘門判錯時當場繞過去,不必先修好環境才動得了技能。
|
||||||
|
# 豁免清單讓「啟動助理」與「修環境」這兩條路徑永遠走得通,閘門才不會把解除自己的路徑鎖掉。
|
||||||
|
# 這兩樣任何一樣被拿掉或改窄,這道閘門就不可以接線。
|
||||||
|
#
|
||||||
|
# --- 心跳結束碼怎麼處置 ---
|
||||||
|
#
|
||||||
|
# 判定一律交給 `heartbeat.sh check`,本檔不自己讀心跳檔——判定寫兩份就會漂移,狀態跟訊息對不上。
|
||||||
|
# 那支腳本的六個結束碼逐碼處置如下:
|
||||||
|
# 0 新鮮。放行。
|
||||||
|
# 1 過期:心跳檔在、ts 也讀得到,但距現在已達門檻。助理跑過、現在停了。擋,訊息講「跑過但
|
||||||
|
# 停了」,並講出超過門檻幾秒。
|
||||||
|
# 2 腳本沒跑起來(`. lib.sh` 載入失敗時 sh 自己回這一碼)。放行——這是判定機制自己壞了,
|
||||||
|
# 不是「助理沒在跑」的證據。
|
||||||
|
# 3 心跳檔不存在。助理從沒啟動過。擋,訊息講「從沒啟動過」,要人去啟動。與 1 的處置不同:
|
||||||
|
# 使用者要做的事不一樣,併成同一句話會叫錯人去做錯事。
|
||||||
|
# 4 心跳檔在、ts 卻讀不出來(缺鍵、空值或不是數字)。**擋。** heartbeat.sh 檔頭寫明呼叫端
|
||||||
|
# 一律當成不新鮮處置,絕不可以退回當成新鮮。訊息與 1、3 都不同:那是檔案壞了,不是助理
|
||||||
|
# 停了,修法是先 stop 再 start 把心跳檔重建起來。
|
||||||
|
# 5 檔案系統操作失敗。**放行。** 理由見下一段。
|
||||||
|
# 6 用法錯誤(不認得的子命令,或一個都沒給)。放行——本檔固定送 check,收到 6 就代表
|
||||||
|
# heartbeat.sh 換了介面、或這支閘門叫錯了。那是這一邊的缺陷,不是助理的狀態。
|
||||||
|
#
|
||||||
|
# 5 為什麼選放行,不選擋:
|
||||||
|
# 一、`check` 這條路徑根本不產生 5。5 只由 `write` 與 `clear` 產出。從 check 收到 5,意思是
|
||||||
|
# 判定機制本身壞了,跟 2 與 6 同一類,不是「助理沒在跑」。
|
||||||
|
# 二、fail-closed 管的是「助理狀態」這一件事實:確定沒有新鮮心跳才擋。5 的意思是連事實都問
|
||||||
|
# 不出來,那不在這道閘門的職權裡。
|
||||||
|
# 三、最要緊的實務理由:5 正是磁碟滿或權限壞的訊號,而那一刻助理自己也寫不出心跳。擋下去的
|
||||||
|
# 結果是全機器整組技能鎖死,出路只剩豁免清單那幾支——可是那幾支同樣要寫 $JSC_HOME
|
||||||
|
# (接線狀態、用量、工作階段),環境壞著它們也修不動。磁碟壞掉要人去清磁碟,不是把技能
|
||||||
|
# 組鎖起來。
|
||||||
|
# 四、和 4 的差別在有沒有出路:4 是「檔案在、內容壞」,那是確定沒有可信心跳的證據,而且修法
|
||||||
|
# 就在豁免清單裡(stop 再 start),擋得起;5 是「檔案系統問不出來」,擋了沒有出路。
|
||||||
|
# 代價據實寫:磁碟壞掉時這道閘門會安靜放行,助理沒在跑也擋不到。那是刻意的取捨——這道閘門
|
||||||
|
# 不是磁碟監控,環境壞掉由 /jsc-cli:doctor 抓。
|
||||||
|
#
|
||||||
|
# 豁免(這些技能永遠放行,改動前想清楚後果):
|
||||||
|
# jsc-assist:* 啟動助理本身就是一次技能呼叫。少了這一條,助理永遠啟動不了,整組
|
||||||
|
# 技能鎖死。這是雞生蛋,清單裡最要緊的一條
|
||||||
|
# jsc-hooks:repair 修 hook 的唯一路徑。修 hook 的技能被 hook 擋下,就沒有任何方法把
|
||||||
|
# hook 修回來,閘門等於把解除自己的路徑一起鎖掉
|
||||||
|
# jsc-hooks:hooks-install 重新接線的唯一路徑。這道閘門接錯了要靠它拆掉
|
||||||
|
# jsc-cli:doctor 環境健檢。心跳寫不出來多半是環境問題,查不了就修不了
|
||||||
|
# jsc-cli:setup 修設定的唯一路徑,doctor 找到的東西要靠它落地
|
||||||
|
# jsc-cli:deploy 部署技能組。助理主體本身也是技能,裝不上就啟動不了
|
||||||
|
# jsc-gitea:wiki 助理巡檢一輪要先把結果寫進 MONITOR_{HASH},寫不成那一輪就不寫心跳
|
||||||
|
# (no record, no heartbeat)。擋了它,巡檢永遠跑不完、心跳永遠不出現,
|
||||||
|
# 助理再也啟動不了。jsc-hooks:repair 的第一步也是讀 ERROR_{HASH},讀
|
||||||
|
# 不到就中止
|
||||||
|
# jsc-ask:ask 上面幾支都要問使用者:assistant 要問做哪一個操作,setup 與 deploy
|
||||||
|
# 要問模式。擋了它,start 連要不要跑都問不出來
|
||||||
|
# jsc-git:commit jsc-hooks:repair 收尾要提交,擋了修好的東西進不了版本控制
|
||||||
|
# jsc-git:pr 同上,repair 規定收尾要對 develop 開 PR,擋了修復做一半
|
||||||
|
# jsc-cli:models jsc-cli:setup 遇到 model-tags.tsv 不見時要靠它補回來,擋了那一項修不完
|
||||||
|
#
|
||||||
|
# 清單認的是技能名,不是呼叫鏈:豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。後五支
|
||||||
|
# (wiki、ask、commit、pr、models)就是為了這件事補進來的——它們自己不是啟動助理的主體,但前
|
||||||
|
# 六支少了它們就走不完。jsc-gitea:wiki 是這裡面最容易漏的一支:只豁免 jsc-assist:* 看起來就夠
|
||||||
|
# 了,可是巡檢那一輪會轉呼叫 wiki 去寫監控頁,寫不成就不寫心跳,於是「沒心跳 → 擋 wiki →
|
||||||
|
# 巡檢不完 → 還是沒心跳」自己咬住自己,永遠解不開。
|
||||||
|
#
|
||||||
|
# 清單刻意不收 jsc-log:worklog 與 jsc-log:learn:那兩支是部署收尾的規則,跟「把助理啟動起來」
|
||||||
|
# 這條路徑無關。fail-closed 閘門的豁免清單只收解鎖路徑,收寬了這道閘門就等於沒有。
|
||||||
|
#
|
||||||
|
# 逃生門:JSC_ASSISTANT_GATE=off 完全略過這道閘門。
|
||||||
|
#
|
||||||
|
# 註:逃生門的判斷擺在載入 lib.sh 之前,這一點與 restart-gate.sh 不同。lib.sh 讀不到時 sh 會就地
|
||||||
|
# 結束並回 2,接在 PreToolUse 上就是無聲擋下每一次技能呼叫;這道閘門是 fail-closed 的,那個
|
||||||
|
# 結果方向上不算錯,但逃生門也跟著跑不到,人就沒有辦法自己繞過去。所以先看逃生門,再載入。
|
||||||
|
# hooks/skill-name.sh、hooks/deny.sh 與 hooks/heartbeat.sh 都以子行程呼叫,讀不到只會讓判定
|
||||||
|
# 降級成放行,不會反過來擋人。
|
||||||
|
|
||||||
|
# 逃生門先看。結束前把標準輸入讀乾淨:不讀就結束,宿主 CLI 會寫進斷掉的管線。
|
||||||
|
if [ "${JSC_ASSISTANT_GATE:-}" = "off" ]; then
|
||||||
|
[ -t 0 ] || cat >/dev/null 2>&1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||||
|
|
||||||
|
read_stdin
|
||||||
|
|
||||||
|
# 技能名解析:交給 skill-name.sh。輸出固定是「{domain}<TAB>{技能名}」;用 awk 判 NF==2 才取值,
|
||||||
|
# 少一欄就當成解析不出來,免得沒有定位字元時 cut -f2 把整行當成技能名,拼出一個不存在的技能名
|
||||||
|
# 去比對豁免清單。
|
||||||
|
sn=$(printf '%s' "$STDIN_JSON" | sh "$HERE/skill-name.sh" "$(cli_name)" 2>/dev/null)
|
||||||
|
sn_domain=$(printf '%s\n' "$sn" | awk -F'\t' 'NF == 2 { print $1; exit }')
|
||||||
|
sn_name=$(printf '%s\n' "$sn" | awk -F'\t' 'NF == 2 { print $2; exit }')
|
||||||
|
[ -n "$sn_domain" ] && [ -n "$sn_name" ] || exit 0
|
||||||
|
skill="jsc-$sn_domain:$sn_name"
|
||||||
|
|
||||||
|
# 豁免清單(理由見檔頭)
|
||||||
|
case "$skill" in
|
||||||
|
jsc-assist:*|jsc-hooks:repair|jsc-hooks:hooks-install|jsc-cli:doctor|jsc-cli:setup|jsc-cli:deploy|jsc-gitea:wiki|jsc-ask:ask|jsc-git:commit|jsc-git:pr|jsc-cli:models)
|
||||||
|
exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 心跳判定。補 </dev/null:heartbeat.sh 不讀標準輸入,但這裡的標準輸入已經被 read_stdin 收乾,
|
||||||
|
# 留著空管線給子行程沒有意義,明確關掉才不會有人往回接一條會等的路。
|
||||||
|
sh "$HERE/heartbeat.sh" check </dev/null 2>/dev/null
|
||||||
|
hb=$?
|
||||||
|
case "$hb" in
|
||||||
|
0) exit 0 ;; # 新鮮
|
||||||
|
1|3|4) ;; # 過期、不存在、時間戳壞掉:落到下面組訊息並擋下
|
||||||
|
*) exit 0 ;; # 2、5、6:判不出來就放行,逐碼理由見檔頭
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 訊息細節一律取自 `heartbeat.sh report`,本檔不自己解析心跳檔:判定與訊息共用同一份探測結果,
|
||||||
|
# 兩邊各讀一次會出現「擋的理由」與「印的數字」對不上。
|
||||||
|
REPORT=$(sh "$HERE/heartbeat.sh" report </dev/null 2>/dev/null)
|
||||||
|
|
||||||
|
# report 是一行、欄位以空白分隔。除了 file 以外每一欄都不含空白,換行切開再取最穩。
|
||||||
|
rep_field() { # $1=鍵名(file 除外)
|
||||||
|
printf '%s' "$REPORT" | tr ' ' '\n' | sed -n "s/^$1=//p" | head -n1
|
||||||
|
}
|
||||||
|
# file 擺在最後,路徑可能含空白,所以取「file= 之後的全部」。
|
||||||
|
rep_file() {
|
||||||
|
printf '%s' "$REPORT" | sed -n 's/.*[[:space:]]file=//p'
|
||||||
|
}
|
||||||
|
|
||||||
|
hb_age=$(rep_field age)
|
||||||
|
hb_ttl=$(rep_field ttl)
|
||||||
|
hb_pid=$(rep_field pid)
|
||||||
|
hb_file=$(rep_file)
|
||||||
|
[ -n "$hb_file" ] || hb_file="$JSC_HOME/assistant/heartbeat"
|
||||||
|
|
||||||
|
# 超過門檻幾秒。兩個值都是純數字才算,算不出來就不印那一段——寧可少講一個數字,也不要印出
|
||||||
|
# 算壞的值。
|
||||||
|
hb_over=""
|
||||||
|
case "$hb_age$hb_ttl" in
|
||||||
|
''|*[!0-9]*) ;;
|
||||||
|
*) hb_over=$(( hb_age - hb_ttl )) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 三種狀態的第一句話各寫一份。使用者要做的事不一樣:沒啟動過的要去啟動,跑過停了的要去查為
|
||||||
|
# 什麼停,檔案壞了的要去重建。併成同一句就會叫錯人做錯事。
|
||||||
|
case "$hb" in
|
||||||
|
3) first=$(printf '[jsc][助理閘門][ERR]:助理沒有在跑。心跳檔 %s 不存在,助理從沒啟動過。技能 /%s 這一次呼叫已擋下。' \
|
||||||
|
"$hb_file" "$skill") ;;
|
||||||
|
1) first=$(printf '[jsc][助理閘門][ERR]:助理跑過,現在停了。上次心跳是 %s 秒前,門檻 %s 秒,已經超過門檻 %s 秒(寫入者 pid=%s,心跳檔 %s)。技能 /%s 這一次呼叫已擋下。' \
|
||||||
|
"${hb_age:-不明}" "${hb_ttl:-不明}" "${hb_over:-不明}" "${hb_pid:-不明}" "$hb_file" "$skill") ;;
|
||||||
|
*) first=$(printf '[jsc][助理閘門][ERR]:助理狀態判不出來。心跳檔 %s 在,但 ts 欄位缺了、是空的、或不是數字——檔案壞了,不是助理停了。一律當成沒有心跳處置。技能 /%s 這一次呼叫已擋下。' \
|
||||||
|
"$hb_file" "$skill") ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 第二句:怎麼把助理弄回來。三種狀態的做法也不同。
|
||||||
|
case "$hb" in
|
||||||
|
3) second='啟動助理:/jsc-assist:assistant,操作選 start。它會先跑一輪巡檢,把結果寫上監控頁,再把排程項目裝起來;心跳是那一輪跑完才寫的。' ;;
|
||||||
|
1) second='重新啟動助理:/jsc-assist:assistant,操作選 start;先用 status 看排程項目還在不在。排程寫進 crontab 不等於 cron 在跑,WSL 預設不啟動 cron,那種機器要先 sudo service cron start,而且每次重開機都要再跑一次。' ;;
|
||||||
|
*) second='重建心跳:/jsc-assist:assistant,操作先選 stop 再選 start。stop 會把壞掉的心跳檔刪掉,start 跑完一輪巡檢才寫出新的一份。' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 擋人輸出交給 deny.sh:形態依 CLI 而定,本檔只組訊息。四段訊息整段走同一條管線送過去,
|
||||||
|
# antigravity 那一支才有辦法把它們壓成同一個 reason 字串;分次呼叫會做出好幾份 deny JSON,
|
||||||
|
# 那支 CLI 只認第一份,後面三段使用者永遠看不到。
|
||||||
|
{ printf '%s\n' "$first"
|
||||||
|
printf '%s\n' "$second"
|
||||||
|
printf '仍可使用:/jsc-assist:*、/jsc-hooks:repair、/jsc-hooks:hooks-install、/jsc-cli:doctor、/jsc-cli:setup、/jsc-cli:deploy、/jsc-cli:models、/jsc-gitea:wiki、/jsc-ask:ask、/jsc-git:commit、/jsc-git:pr(啟動助理與修環境這兩條路徑要永遠走得通,包括它們轉呼叫的下一層)\n'
|
||||||
|
printf '確定要略過閘門:JSC_ASSISTANT_GATE=off\n'
|
||||||
|
} | sh "$HERE/deny.sh" "$(cli_name)"
|
||||||
|
exit $?
|
||||||
@@ -81,7 +81,13 @@ scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,
|
|||||||
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
|
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
|
||||||
hit '(^|[^[:alnum:]_])![0-9]+' 'PR、MR 編號(!45)'
|
hit '(^|[^[:alnum:]_])![0-9]+' 'PR、MR 編號(!45)'
|
||||||
hit '[A-Z]{2,6}-[0-9]{1,6}' '工作包、故事、驗收、測試案例、變更單、議題編號(前綴加流水號)'
|
hit '[A-Z]{2,6}-[0-9]{1,6}' '工作包、故事、驗收、測試案例、變更單、議題編號(前綴加流水號)'
|
||||||
hit '(QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR|CHECK|REPORT)_([A-Z0-9]{8}|CONTENTS)' 'jsc wiki 頁面編號'
|
# 頁名樣式:十五種頁型,尾段是 CONTENTS(目錄頁)、40 碼大寫十六進位(內容頁),
|
||||||
|
# 或尚未遷移的舊頁編號。舊頁那兩條要留著,不然舊頁編號會漏偵測。
|
||||||
|
# 舊頁為什麼有 H 開頭這一條:舊的短碼演算法只要首碼落在 0-9ABC 就改寫成 H 加原前 7 碼,
|
||||||
|
# 十六個十六進位首碼有十三個會命中,所以既有舊頁名大多是 H 開頭,只收 [0-9A-F]{8} 會漏掉。
|
||||||
|
# 這條式子在別處另有兩份各自獨立的定義,稽核時才比對一致。刻意不共用函式:hook 要能
|
||||||
|
# 自足執行,執行期相依別的 plugin 路徑,那條路徑一缺,整支 hook 就掃不動了。
|
||||||
|
hit '(QUESTION|PLAN|ANALYZE|DELIVER|MAINTAIN|REPO|LOG|LEARN|ERROR|CHECK|REPORT|SKILLSET|TOOLING|MONITOR|CONTENTS)_(CONTENTS|[0-9A-F]{8}|H[0-9A-F]{7}|[0-9A-F]{40})' 'jsc wiki 頁面編號'
|
||||||
hit '(todo|TODO|待辦)[[:space:]]*#?[0-9]+' 'TDD 待辦編號'
|
hit '(todo|TODO|待辦)[[:space:]]*#?[0-9]+' 'TDD 待辦編號'
|
||||||
hit '([Ss]print|里程碑|[Mm]ilestone)[[:space:]]*[0-9]+' 'Sprint、里程碑編號'
|
hit '([Ss]print|里程碑|[Mm]ilestone)[[:space:]]*[0-9]+' 'Sprint、里程碑編號'
|
||||||
hit '(^|[^[:alnum:].])v[0-9]+\.[0-9]+|版本[[:space:]]*v?[0-9]+\.[0-9]+' '版本號'
|
hit '(^|[^[:alnum:].])v[0-9]+\.[0-9]+|版本[[:space:]]*v?[0-9]+\.[0-9]+' '版本號'
|
||||||
|
|||||||
Executable
+176
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# heartbeat.sh — 助理心跳檔的讀寫工具。
|
||||||
|
#
|
||||||
|
# 助理在背景跑,前景會話看不到它。心跳檔就是它還在跑的唯一證據:助理主體與系統排程每 60 秒
|
||||||
|
# 寫一次,`jsc-assist:assistant` 與擋人訊息讀這一份,判斷助理在不在。
|
||||||
|
#
|
||||||
|
# 這支不是 hook,不接在任何事件上,只被工具端呼叫,所以它擋不到任何人。它只做判定,擋不擋
|
||||||
|
# 由呼叫端自己決定——助理不參與閘門判定,只負責維持心跳。
|
||||||
|
#
|
||||||
|
# 用法(四個子命令都不讀標準輸入,理由見下方「不讀標準輸入」):
|
||||||
|
# heartbeat.sh write 寫入心跳檔,四個欄位一次寫齊,目錄不存在就建。由助理主體與系統
|
||||||
|
# 排程呼叫。
|
||||||
|
# heartbeat.sh check 判定心跳新不新鮮。什麼都不印,結果只在結束碼;要細節請跑 report。
|
||||||
|
# heartbeat.sh report 印一行心跳現況,格式見下方「report 輸出格式」。
|
||||||
|
# heartbeat.sh clear 刪除心跳檔。由助理的 stop 呼叫。檔案不存在也算成功。
|
||||||
|
#
|
||||||
|
# 結束碼:
|
||||||
|
# 0 check 判定新鮮(state=fresh);write 寫成功;clear 清完,檔案已經不在;report 印完
|
||||||
|
# 1 check:心跳檔在、ts 也讀得到,但距現在已達門檻(state=stale)。助理跑過,現在停了。
|
||||||
|
# 訊息要叫人去查助理為什麼停
|
||||||
|
# 2 保留給「腳本沒跑起來」:`. lib.sh` 載入失敗時 sh 自己回這一碼(見最下方註)。判定路徑
|
||||||
|
# 刻意不用 2,兩者才分得開
|
||||||
|
# 3 check:心跳檔不存在(state=absent)。助理從沒啟動過。處置與 1 不同,訊息要叫人去啟動
|
||||||
|
# 4 check:心跳檔在、ts 卻讀不出來(state=invalid,缺鍵、空值或不是數字)。檔案壞了,不是
|
||||||
|
# 助理停了。呼叫端一律當成不新鮮處置,絕不可以退回當成新鮮
|
||||||
|
# 5 檔案系統操作失敗:write 寫不進去(磁碟滿、權限壞、目錄建不起來),或 clear 刪不掉、
|
||||||
|
# 檔案還在。這是嚴重狀況——助理沒有心跳就會被自己那道閘門擋掉,所以一定要吵出來,
|
||||||
|
# 不能安靜當成成功
|
||||||
|
# 6 用法錯誤:不認得的子命令,或一個子命令都沒給。刻意不與上面任何一種正常狀態共用碼,
|
||||||
|
# 共用了呼叫端就分不出「助理沒在跑」與「這支腳本被叫錯」
|
||||||
|
#
|
||||||
|
# --- 只看 ts,絕不看 pid 存活 ---
|
||||||
|
#
|
||||||
|
# 新鮮的判準只有一條:心跳檔存在,而且 ts 距現在小於門檻秒數。pid 一律不拿來判定。
|
||||||
|
# 一台機器上五支 CLI 各自是獨立行程,助理也可能跑在容器裡,彼此看不到對方的 pid:拿
|
||||||
|
# `kill -0` 去問,看不到的行程一律回失敗,活著的助理會被判成停了;pid 還會被回收,別人的
|
||||||
|
# 行程剛好接到同一個號碼,就反過來把停掉的助理判成還在跑。兩種誤判都不會報錯,查起來也沒有
|
||||||
|
# 線索。pid 只寫進檔案當擋人訊息的線索,讓人自己去查那個行程。
|
||||||
|
#
|
||||||
|
# --- 門檻為什麼是 300 ---
|
||||||
|
#
|
||||||
|
# 心跳週期是 60 秒,門檻取五倍。一次網路或磁碟卡頓讓某一拍沒寫成,後面還有四拍補得回來,
|
||||||
|
# 不會誤判成助理停了。門檻用環境變數 JSC_ASSISTANT_HEARTBEAT_TTL 覆寫,單位是秒;值不是
|
||||||
|
# 正整數就退回 300——環境變數打錯字不該讓判定整個歪掉。
|
||||||
|
#
|
||||||
|
# --- 不讀標準輸入 ---
|
||||||
|
#
|
||||||
|
# 這支是被工具端呼叫的腳本,四個子命令一律不讀 stdin。理由與 restart-gate.sh 的子命令相同:
|
||||||
|
# read_stdin 在標準輸入是管線又沒人關閉時會一直等,工具端呼叫就整支卡死。
|
||||||
|
#
|
||||||
|
# --- 狀態檔格式 ---
|
||||||
|
#
|
||||||
|
# $JSC_HOME/assistant/heartbeat(JSC_HOME 未設定時為 ~/.jsc),純文字 key=value,一行一欄位,
|
||||||
|
# 順序不拘,不認得的鍵一律忽略:
|
||||||
|
# ts={epoch 秒數} 寫入當下的時間。判定只看這一欄
|
||||||
|
# pid={行程 id} 寫入者的行程 id。只當擋人訊息的線索
|
||||||
|
# cli={CLI 代號} 寫入者是哪一支 CLI,取自 cli_name()
|
||||||
|
# session={id} 寫入者的工作階段 id,取自 session_id()
|
||||||
|
# 寫入走「先寫暫存檔、再改名」:改名是原子的,讀的那一端永遠讀到完整的一份。直接覆寫的話,
|
||||||
|
# 剛好讀到寫一半的檔案會少掉 ts,判定就從 fresh 掉成 invalid,助理明明活著卻被說成壞了。
|
||||||
|
#
|
||||||
|
# --- report 輸出格式 ---
|
||||||
|
#
|
||||||
|
# 固定一行,鍵的順序固定,欄位以空白分隔。鍵一個都不會少,缺值就只留鍵名,呼叫端不必判斷
|
||||||
|
# 有沒有這一欄。路徑擺最後,路徑含空白時才不會把後面的欄位吃掉:
|
||||||
|
# state={fresh|stale|invalid|absent} ts={epoch} age={秒} ttl={秒} pid={} cli={} session={} file={路徑}
|
||||||
|
# state 的四種值與 check 的結束碼一一對應:fresh=0、stale=1、absent=3、invalid=4。
|
||||||
|
# 例(心跳新鮮):
|
||||||
|
# state=fresh ts=1756684800 age=42 ttl=300 pid=31415 cli=claude session=a1b2c3 file=/root/.jsc/assistant/heartbeat
|
||||||
|
# 例(心跳檔不存在):
|
||||||
|
# state=absent ts= age= ttl=300 pid= cli= session= file=/root/.jsc/assistant/heartbeat
|
||||||
|
# ts 不是數字時 ts 與 age 兩欄都印空的:那個值是垃圾,原樣印出來會夾帶空白把欄位切歪。
|
||||||
|
#
|
||||||
|
# 註:本檔以 `. "$HERE/lib.sh"` 載入共用函式,沒有接 `|| true`。載入失敗時 sh 會就地結束並回
|
||||||
|
# 2。這一點的後果與 restart-gate.sh 不同:那支接在 PreToolUse 上,回 2 等於無聲擋下每一次
|
||||||
|
# 技能呼叫;這支沒接任何 hook,回 2 只會讓呼叫端收到「心跳判不出來」,擋不到任何人。
|
||||||
|
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||||
|
|
||||||
|
# 這支永遠不讀標準輸入,但 session_id() 會去看 STDIN_JSON。先設成空字串,讓它直接走環境
|
||||||
|
# 變數那條路,不會因為變數沒定義而拿到不確定的值。
|
||||||
|
STDIN_JSON=""
|
||||||
|
|
||||||
|
STATE_DIR="$JSC_HOME/assistant"
|
||||||
|
STATE="$STATE_DIR/heartbeat"
|
||||||
|
|
||||||
|
DEFAULT_TTL=300
|
||||||
|
|
||||||
|
# 門檻秒數。環境變數不是正整數就退回預設值,理由見檔頭「門檻為什麼是 300」。
|
||||||
|
ttl() {
|
||||||
|
_t="${JSC_ASSISTANT_HEARTBEAT_TTL:-}"
|
||||||
|
case "$_t" in
|
||||||
|
''|*[!0-9]*) printf '%s' "$DEFAULT_TTL"; return 0 ;;
|
||||||
|
esac
|
||||||
|
if [ "$_t" -gt 0 ] 2>/dev/null; then printf '%s' "$_t"; else printf '%s' "$DEFAULT_TTL"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 從心跳檔取一個欄位;檔案讀不到或欄位不存在就不輸出。
|
||||||
|
field() { # $1=鍵名
|
||||||
|
[ -f "$STATE" ] && [ -r "$STATE" ] || return 0
|
||||||
|
sed -n "s/^$1=//p" "$STATE" 2>/dev/null | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 判定心跳狀態,印出「{state}<TAB>{ts}<TAB>{age}」,後兩欄在 absent 與 invalid 時留空。
|
||||||
|
# check 與 report 共用這一份:兩邊各判一次就會漂移,狀態與訊息對不上。
|
||||||
|
probe() {
|
||||||
|
if [ ! -f "$STATE" ] || [ ! -r "$STATE" ]; then
|
||||||
|
printf 'absent\t\t\n'; return 0
|
||||||
|
fi
|
||||||
|
_ts=$(field ts)
|
||||||
|
case "$_ts" in
|
||||||
|
''|*[!0-9]*) printf 'invalid\t\t\n'; return 0 ;;
|
||||||
|
esac
|
||||||
|
# 去掉開頭的 0:POSIX 算術把 08 當八進位,會直接報錯,錯完 age 是空的,判定就整條歪掉。
|
||||||
|
while :; do
|
||||||
|
case "$_ts" in 0?*) _ts=${_ts#0} ;; *) break ;; esac
|
||||||
|
done
|
||||||
|
_age=$(( $(now_epoch) - _ts ))
|
||||||
|
# age 是負的代表 ts 在未來,那是時鐘偏移,不是助理停了,照樣算新鮮。
|
||||||
|
if [ "$_age" -lt "$(ttl)" ]; then _st=fresh; else _st=stale; fi
|
||||||
|
printf '%s\t%s\t%s\n' "$_st" "$_ts" "$_age"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf 'usage: heartbeat.sh {write|check|report|clear}\n' >&2
|
||||||
|
exit 6
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
write)
|
||||||
|
# 心跳檔的位置被目錄或別的東西佔住時要當場失敗。`mv` 遇到目標是目錄會把暫存檔搬進去,
|
||||||
|
# 搬得成功、心跳檔卻永遠不存在,寫的那一端拿到 0,讀的那一端說助理沒啟動過。
|
||||||
|
if [ -e "$STATE" ] && [ ! -f "$STATE" ]; then
|
||||||
|
printf '[jsc][助理心跳][ERR]:%s 不是一般檔案,心跳寫不進去。\n' "$STATE" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||||
|
_tmp="$STATE.tmp.$$"
|
||||||
|
# stderr 先轉走再開檔:順序反過來的話,開檔失敗的訊息是 sh 自己印的,那時 stderr 還沒
|
||||||
|
# 轉走,會漏到呼叫端的畫面上,蓋掉下面那句講得清楚的錯誤訊息。
|
||||||
|
if ! printf 'ts=%s\npid=%s\ncli=%s\nsession=%s\n' \
|
||||||
|
"$(now_epoch)" "$$" "$(cli_name)" "$(session_id)" 2>/dev/null > "$_tmp"; then
|
||||||
|
rm -f "$_tmp" 2>/dev/null
|
||||||
|
printf '[jsc][助理心跳][ERR]:寫不進 %s,助理這一拍沒有心跳。\n' "$STATE" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
if ! mv -f "$_tmp" "$STATE" 2>/dev/null; then
|
||||||
|
rm -f "$_tmp" 2>/dev/null
|
||||||
|
printf '[jsc][助理心跳][ERR]:換不上 %s,助理這一拍沒有心跳。\n' "$STATE" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
check)
|
||||||
|
case "$(probe | cut -f1)" in
|
||||||
|
fresh) exit 0 ;;
|
||||||
|
stale) exit 1 ;;
|
||||||
|
absent) exit 3 ;;
|
||||||
|
*) exit 4 ;;
|
||||||
|
esac ;;
|
||||||
|
report)
|
||||||
|
_p=$(probe)
|
||||||
|
printf 'state=%s ts=%s age=%s ttl=%s pid=%s cli=%s session=%s file=%s\n' \
|
||||||
|
"$(printf '%s' "$_p" | cut -f1)" \
|
||||||
|
"$(printf '%s' "$_p" | cut -f2)" \
|
||||||
|
"$(printf '%s' "$_p" | cut -f3)" \
|
||||||
|
"$(ttl)" "$(field pid)" "$(field cli)" "$(field session)" "$STATE"
|
||||||
|
exit 0 ;;
|
||||||
|
clear)
|
||||||
|
rm -f "$STATE" 2>/dev/null
|
||||||
|
# 刪不掉就要講出來:檔案還在,別人讀到的心跳會說助理還在跑。
|
||||||
|
if [ -e "$STATE" ]; then
|
||||||
|
printf '[jsc][助理心跳][ERR]:刪不掉 %s,心跳檔還在。\n' "$STATE" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.3.4",
|
"version": "0.3.8",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills/",
|
"skills": "./skills/",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 觸發時機 | 裝好或更新完 jsc 技能組之後,要把九支 hook 接線到每一支已安裝的 CLI 時用;`jsc-cli:deploy` 收尾會把偵測到的 CLI 清單交給它。不用於撰寫新的 hook,也不用於單獨修一支壞掉的 hook,那是 `jsc-hooks:repair` 的事 |
|
| 觸發時機 | 裝好或更新完 jsc 技能組之後,要把九支 hook 接線到每一支已安裝的 CLI 時用;`jsc-cli:deploy` 收尾會把偵測到的 CLI 清單交給它。不用於撰寫新的 hook,也不用於單獨修一支壞掉的 hook,那是 `jsc-hooks:repair` 的事 |
|
||||||
| 關鍵步驟 | 取得 CLI 清單(呼叫端交來的優先,沒有才自己跑 `detect-clis.sh`)、第一支 CLI 單獨跑完整條管線(它負責更新共用的 `$JSC_HOME/current/jsc-hooks` 連結)、其餘 CLI 一支一個 sub agent 並行、每支 CLI 依序走 purge、接線、status、smoke、scan 五道關卡、讀每道關卡自己印的第一行判定、任一關卡出錯就寫 `ERROR_{HASH}` 並轉給 `jsc-hooks:repair`、逐 CLI 回報五道關卡的結果 |
|
| 關鍵步驟 | 取得 CLI 清單(呼叫端交來的優先,沒有才自己跑 `detect-clis.sh`)、第一支 CLI 單獨跑完整條管線(它負責更新共用的 `$JSC_HOME/current/jsc-hooks` 連結)、其餘 CLI 一支一個 sub agent 並行、每支 CLI 依序走 purge、接線、status、smoke、scan 五道關卡、讀每道關卡自己印的第一行判定、任一關卡出錯就寫 `ERROR_{HASH}` 並轉給 `jsc-hooks:repair`(異常頁與索引目錄頁分屬兩個存取庫,各自解析;只解不出目錄頁的存取庫時異常頁照寫、索引跳過,回報要講明那一頁沒被索引)、逐 CLI 回報五道關卡的結果 |
|
||||||
| 外部呼叫 | `tools/wire-cli.sh purge`、`tools/wire-cli.sh {cli}`、`tools/wire-cli.sh status`、`tools/wire-cli.sh smoke`、`tools/scan-hook-errors.sh`、`tools/report-error.sh`、`jsc-cli/tools/detect-clis.sh`、`jsc-hooks:repair` 技能、`jsc-gitea:wiki`(寫 `ERROR_{HASH}` 時經 `report-error.sh`);接線腳本內部另呼叫 `hooks/skill-name.sh` 與 `hooks/deny.sh` 做冒煙斷言 |
|
| 外部呼叫 | `tools/wire-cli.sh purge`、`tools/wire-cli.sh {cli}`、`tools/wire-cli.sh status`、`tools/wire-cli.sh smoke`、`tools/scan-hook-errors.sh`、`tools/report-error.sh`、`jsc-cli/tools/detect-clis.sh`、`jsc-hooks:repair` 技能、`jsc-gitea:wiki`(寫 `ERROR_{HASH}` 時經 `report-error.sh`);接線腳本內部另呼叫 `hooks/skill-name.sh` 與 `hooks/deny.sh` 做冒煙斷言 |
|
||||||
| 完成條件 | 每一支偵測到的 CLI 都有五道關卡各一行判定,沒有任何一道回結束碼 2,smoke 的 `lines` 條數與它自己的斷言相符,claude、codex、copilot、antigravity 回 `wired` 而 kiro 回 `degraded`(CLI 擋不下技能叫用),四支非 claude 的執行期錯誤掃描一律據實回 `unavailable`,各 CLI 的形狀與觸發驗證等級分開寫進回報(codex、antigravity、kiro 形狀實證,copilot 形狀未證;kiro 觸發部分實證,其餘未驗證),而且每一筆錯誤都帶一個 `ERROR_{HASH}` 結果與一條對 `develop` 的修正 PR 連結 |
|
| 完成條件 | 每一支偵測到的 CLI 都有五道關卡各一行判定,沒有任何一道回結束碼 2,smoke 的 `lines` 條數與它自己的斷言相符,claude、codex、copilot、antigravity 回 `wired` 而 kiro 回 `degraded`(CLI 擋不下技能叫用),四支非 claude 的執行期錯誤掃描一律據實回 `unavailable`,各 CLI 的形狀與觸發驗證等級分開寫進回報(codex、antigravity、kiro 形狀實證,copilot 形狀未證;kiro 觸發部分實證,其餘未驗證),而且每一筆錯誤都帶一個 `ERROR_{HASH}` 結果與一條對 `develop` 的修正 PR 連結 |
|
||||||
| 可驗證跡象 | 各 CLI 的設定檔多出 jsc 段落:codex 的 `config.toml` 標記段落、`hooks/codex-hooks.json`(從 `hooks/hooks.json` 推導,matcher `Skill` 換成 `Bash`)與 `.codex-plugin/plugin.json` 指過去的 `hooks` 路徑字串、copilot 的 `~/.copilot/settings.json` 頂層 `hooks` 鍵(matcher `skill`,合併不覆寫,`enabledPlugins` 與第三方條目原樣保留)與 `$COPILOT_HOME` 底下的指引檔、antigravity 的 `~/.gemini/config/hooks.json` 的 `jsc` 段落(`PreToolUse` 為 Grouped、matcher `^view_file$`,`PreInvocation` 維持 Flat)、kiro 的 `~/.kiro/agents/jsc.json`(`hooks` 為 `agentSpawn`、`userPromptSubmit`、`stop` 三個合法事件加 `timeout_ms`、兩層 `skill://` glob 的 `resources`、明列的 `tools`,並通過 `kiro-cli agent validate`)與 `~/.kiro/settings/cli.json` 的 `chat.defaultAgent=jsc`;四支非 claude 的接線命令都以 `JSC_CLI={代號}` 前綴自帶 CLI 代號,缺了它兩道閘門解不出技能名、一律安靜放行,所以 `status` 把它列成單獨一項;另有 `$JSC_HOME/current/jsc-hooks` 符號連結建立或更新、`$JSC_HOME/backup/hooks/{cli}/{時間戳}/` 留下 purge 前的備份、出錯時 wiki 多一頁 `ERROR_{HASH}` 並在 `ERROR_CONTENTS` 補一列、修正路徑留下一條對 `develop` 的 PR |
|
| 可驗證跡象 | 各 CLI 的設定檔多出 jsc 段落:codex 的 `config.toml` 標記段落、`hooks/codex-hooks.json`(從 `hooks/hooks.json` 推導,matcher `Skill` 換成 `Bash`)與 `.codex-plugin/plugin.json` 指過去的 `hooks` 路徑字串、copilot 的 `~/.copilot/settings.json` 頂層 `hooks` 鍵(matcher `skill`,合併不覆寫,`enabledPlugins` 與第三方條目原樣保留)與 `$COPILOT_HOME` 底下的指引檔、antigravity 的 `~/.gemini/config/hooks.json` 的 `jsc` 段落(`PreToolUse` 為 Grouped、matcher `^view_file$`,`PreInvocation` 維持 Flat)、kiro 的 `~/.kiro/agents/jsc.json`(`hooks` 為 `agentSpawn`、`userPromptSubmit`、`stop` 三個合法事件加 `timeout_ms`、兩層 `skill://` glob 的 `resources`、明列的 `tools`,並通過 `kiro-cli agent validate`)與 `~/.kiro/settings/cli.json` 的 `chat.defaultAgent=jsc`;四支非 claude 的接線命令都以 `JSC_CLI={代號}` 前綴自帶 CLI 代號,缺了它兩道閘門解不出技能名、一律安靜放行,所以 `status` 把它列成單獨一項;另有 `$JSC_HOME/current/jsc-hooks` 符號連結建立或更新、`$JSC_HOME/backup/hooks/{cli}/{時間戳}/` 留下 purge 前的備份、出錯時 wiki 多一頁 `ERROR_{HASH}`(落在 `JSC_WIKI_REPO_ERROR` 解出的存取庫)並在索引目錄頁補一列(落在 `JSC_WIKI_REPO_CONTENTS` 解出的另一個存取庫,那一列指向異常頁的連結是絕對網址)、修正路徑留下一條對 `develop` 的 PR |
|
||||||
|
|
||||||
## repair
|
## repair
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ The detailed flow **MUST run as a sub agent**; the main agent only reports the s
|
|||||||
5. `tools/scan-hook-errors.sh --cli {cli}` — only claude keeps hook results in its native records and can answer `clean` or `errors`; codex, copilot, antigravity and kiro answer `unavailable`, and their runtime evidence comes from the smoke stage alone. Exit 0 covers both `clean` and `unavailable`, exit 1 is `errors` and every entry with `jsc=true` goes to step 3, exit 2 is a bad CLI name.
|
5. `tools/scan-hook-errors.sh --cli {cli}` — only claude keeps hook results in its native records and can answer `clean` or `errors`; codex, copilot, antigravity and kiro answer `unavailable`, and their runtime evidence comes from the smoke stage alone. Exit 0 covers both `clean` and `unavailable`, exit 1 is `errors` and every entry with `jsc=true` goes to step 3, exit 2 is a bad CLI name.
|
||||||
|
|
||||||
Done when every detected CLI has exactly one verdict line per stage, no stage exited 2, the smoke stage's `lines` count matches its own assertion, the four non-claude CLIs are reported as `unavailable` rather than clean on the scan stage, and antigravity and kiro carry the note that their hook firing is unverified.
|
Done when every detected CLI has exactly one verdict line per stage, no stage exited 2, the smoke stage's `lines` count matches its own assertion, the four non-claude CLIs are reported as `unavailable` rather than clean on the scan stage, and antigravity and kiro carry the note that their hook firing is unverified.
|
||||||
3. For each error — a failed purge, a failed wiring, an `unwired` status, a failed smoke, or a scanned error with `jsc=true` — run `tools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}` with the script's `[jsc]` output on stdin, then hand the failure to `jsc-hooks:repair`, which **MUST run as a sub agent** and must finish by opening a PR against `develop`. Aborting the remaining installs here is allowed as long as the repair starts. Exit 0 with an `ERROR_{HASH}` page name and URL on stdout means the page was written; exit 0 with empty output means `JSC_WIKI_REPO_ERROR` and `JSC_WIKI_REPO` are both unset, so carry that reason into step 4 instead; exit 2 means the call itself was malformed — `--hook` or `--summary` is missing — so fix the arguments and rerun the same call; exit 4 means the wiki record did not land, so report the failure text and still start the repair — a page that could not be written is no reason to leave a broken hook wired. Exit 4 covers two cases, and the report has to say which: a failed write, or the script refusing to write the error directory page because it could not read the old one back. That directory is appended to, never overwritten: every row on it is somebody else's error report, so the script reads the page, adds this run's row, and writes the whole page. Only a genuine 404 (`wiki-get` exit 4) means the page is not there yet and lets it build one from the template. An invalid key (exit 7) or any other API failure (exit 8) leaves the old rows unknown, so it skips the directory write and names the code instead — writing a fresh template over a directory it never read would erase every earlier report, with no merge and no backup behind it. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Skip this step when every CLI passed all five stages. Done when every error carries one `ERROR_{HASH}` result — a page name and URL, or the recorded reason no page was written — and one repair PR URL against `develop`.
|
3. For each error — a failed purge, a failed wiring, an `unwired` status, a failed smoke, or a scanned error with `jsc=true` — run `tools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}` with the script's `[jsc]` output on stdin, then hand the failure to `jsc-hooks:repair`, which **MUST run as a sub agent** and must finish by opening a PR against `develop`. Aborting the remaining installs here is allowed as long as the repair starts. The error page and the error directory page live in two different wiki repos, resolved separately: the page through `wiki-repo ERROR`, the directory through `wiki-repo CONTENTS`. Exit 0 with an `ERROR_{HASH}` page name and URL on stdout means the page was written; the same exit 0 with a `[jsc]` line on stderr still means the page landed, and that line says what is missing — either the directory repo would not resolve, so nothing indexes the page, or the page URL could not be read back, so the page name comes out on its own — carry that note into step 4. Exit 0 with no output at all means the run ended on one of the quiet-degradation reasons listed in the script's own header — no `gitea.sh` on the path, the error page's wiki repo unresolved, the hash not computed, or a temp file not created — so no page was written at all and that reason goes into step 4 instead; exit 2 means the call itself was malformed — `--hook` or `--summary` is missing — so fix the arguments and rerun the same call; exit 4 means the wiki record did not land, so report the failure text and still start the repair — a page that could not be written is no reason to leave a broken hook wired. Exit 4 covers two cases, and the report has to say which: a failed write, or the script refusing to write the error directory page because it could not read the old one back. That directory is appended to, never overwritten: every row on it is somebody else's error report, so the script reads the page, adds this run's row, and writes the whole page. Only a genuine 404 (`wiki-get` exit 4) means the page is not there yet and lets it build one from the template. An invalid key (exit 7) or any other API failure (exit 8) leaves the old rows unknown, so it skips the directory write and names the code instead — writing a fresh template over a directory it never read would erase every earlier report, with no merge and no backup behind it. A scanned error with `jsc=false` belongs to a third-party hook: report it and leave it alone. Skip this step when every CLI passed all five stages. Done when every error carries one `ERROR_{HASH}` result — a page name with its URL, a page name plus the reason the URL is missing, or the recorded reason no page was written — and one repair PR URL against `develop`.
|
||||||
4. Report five results per CLI — purge, wiring, status, smoke, scan — each with the reason its script printed, plus the smoke `lines` count, any `ERROR_{HASH}` page name and every repair PR URL. Done when every detected CLI appears with one verdict per stage and every repair has a PR against `develop`.
|
4. Report five results per CLI — purge, wiring, status, smoke, scan — each with the reason its script printed, plus the smoke `lines` count, any `ERROR_{HASH}` page name and every repair PR URL. Done when every detected CLI appears with one verdict per stage and every repair has a PR against `develop`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
> 由 `jsc-hooks` 的失敗回報流程維護。新異常附加在文末,查問題時先看最新一筆。
|
> 由 `jsc-hooks` 的失敗回報流程維護。新異常附加在文末,查問題時先看最新一筆。
|
||||||
>
|
>
|
||||||
> 寫入語意:一列代表一次 hook 異常回報。寫入前先讀回整頁,同一筆異常已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。禁止整頁覆蓋,也不得改動別人的列。
|
> 存放位置:本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的目錄專用存取庫,與異常頁的存取庫是兩個不同的存取庫。
|
||||||
|
>
|
||||||
|
> 寫入語意:一列代表一次 hook 異常回報。寫入前先讀回整頁,同一筆異常已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。一律 upsert 附加,禁止整頁覆蓋,也不得改動別人的列。
|
||||||
|
>
|
||||||
|
> 連結寫法:指向異常頁的連結一律用 `gitea.sh wiki-url` 產出的絕對網址。跨存取庫的 `[[頁名]]` 解不開,只會留下死連結。
|
||||||
|
|
||||||
## 異常清單
|
## 異常清單
|
||||||
|
|
||||||
| 時間 | 頁名 | 存取庫名稱 | 觸發 hook | 退出碼 | 摘要 |
|
| 時間 | 頁名 | 存取庫名稱 | 觸發 hook | 退出碼 | 摘要 |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| {yyyy-MM-dd HH:mm:ss} | [[{error title}|ERROR_{HASH}]] | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
| {yyyy-MM-dd HH:mm:ss} | [{error title}]({error url}) | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
||||||
|
|||||||
+67
-24
@@ -12,13 +12,20 @@
|
|||||||
# 相關輸出(stdout/stderr 摘要)由標準輸入讀入,可省略。
|
# 相關輸出(stdout/stderr 摘要)由標準輸入讀入,可省略。
|
||||||
#
|
#
|
||||||
# 輸出:
|
# 輸出:
|
||||||
# 成功印出「{頁名} {網址}」一行。
|
# 成功印出「{頁名} {網址}」一行。網址在異常頁寫成功之後才取,頁名的 hash 帶時間戳,
|
||||||
# wiki 位置解析不出來(JSC_WIKI_REPO_ERROR 與 JSC_WIKI_REPO 都沒設,或找不到
|
# 每次都是全新的頁,寫之前查一定是 404。取不到網址時只印頁名,原因走 stderr,仍然 exit 0。
|
||||||
# gitea.sh)時安靜降級:不輸出、exit 0。回報失敗不該再變成一次失敗。
|
# 下列四種情形安靜降級:不寫任何頁、不輸出任何內容、exit 0。回報失敗不該再變成一次失敗。
|
||||||
|
# 1. 找不到 gitea.sh
|
||||||
|
# 2. 解析不出異常頁的 wiki 存取庫(JSC_WIKI_REPO_ERROR 與 JSC_WIKI_REPO 都沒設)
|
||||||
|
# 3. 算不出 HASH(hash-id 失敗或回空字串)
|
||||||
|
# 4. 建不出暫存檔(mktemp 失敗)
|
||||||
|
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
|
||||||
|
# 異常頁、只解不出目錄頁的存取庫,就只寫異常頁、跳過目錄頁更新,印出頁名,仍然 exit 0。
|
||||||
|
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
||||||
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
||||||
#
|
#
|
||||||
# 結束碼: 0=已寫入 wiki 並印出「{頁名} {網址}」,或安靜降級(找不到 gitea.sh、解析不出
|
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
|
||||||
# wiki 存取庫、算不出 HASH、建不出暫存檔)——回報失敗不該再變成一次失敗
|
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
|
||||||
# 2=用法錯誤(缺 --hook 或 --summary)
|
# 2=用法錯誤(缺 --hook 或 --summary)
|
||||||
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
|
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
|
||||||
# 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。
|
# 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。
|
||||||
@@ -27,8 +34,10 @@
|
|||||||
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
|
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
|
||||||
#
|
#
|
||||||
# 頁名:
|
# 頁名:
|
||||||
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 前 8 碼(共用 hash 規則)。
|
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 完整 40 碼大寫十六進位
|
||||||
|
# (共用 hash 規則)。三段以空白相連當 hash 輸入。
|
||||||
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
|
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
|
||||||
|
# 索引目錄頁的頁名固定,不帶 HASH。
|
||||||
#
|
#
|
||||||
# 誰來呼叫:
|
# 誰來呼叫:
|
||||||
# 由操作者手動執行,或由技能步驟執行(`jsc-hooks:hooks-install` 在 wire-cli.sh 回報
|
# 由操作者手動執行,或由技能步驟執行(`jsc-hooks:hooks-install` 在 wire-cli.sh 回報
|
||||||
@@ -63,12 +72,19 @@ if [ -z "$hook" ] || [ -z "$summary" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
gsh=$(jsc_gitea_sh) || exit 0
|
gsh=$(jsc_gitea_sh) || exit 0
|
||||||
|
# 異常頁與索引目錄頁落在兩個不同的存取庫,各解各的:異常頁走自己的型別,目錄頁走目錄專用
|
||||||
|
# 型別。異常頁的存取庫解不出來就整支降級,連異常都沒地方寫,做下去也沒意義。
|
||||||
wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0
|
wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0
|
||||||
[ -n "$wrepo" ] || exit 0
|
[ -n "$wrepo" ] || exit 0
|
||||||
|
# 目錄頁的存取庫解不出來不算失敗:異常頁照寫,只跳過目錄頁更新,仍然 exit 0。
|
||||||
|
# 一份寫得成的異常紀錄,不該因為索引沒地方放就整份丟掉。
|
||||||
|
crepo=$(sh "$gsh" wiki-repo CONTENTS 2>/dev/null || true)
|
||||||
|
|
||||||
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
||||||
if [ -z "$repo" ]; then
|
if [ -z "$repo" ]; then
|
||||||
origin=$(git config --get remote.origin.url 2>/dev/null || true)
|
origin=$(git config --get remote.origin.url 2>/dev/null || true)
|
||||||
|
# 尾綴的 .git 在這裡剝掉:hash 工具刻意不做輸入正規化,同一份輸入要算出同一個 HASH,
|
||||||
|
# 正規化就是呼叫端的責任。帶不帶 .git 會算出兩個不同的頁,同一個存取庫就分裂成兩份紀錄。
|
||||||
repo=$(printf '%s' "$origin" \
|
repo=$(printf '%s' "$origin" \
|
||||||
| sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//')
|
| sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//')
|
||||||
fi
|
fi
|
||||||
@@ -116,12 +132,41 @@ fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
|
|||||||
| fill '{stdout / stderr 摘要}' "$detail" \
|
| fill '{stdout / stderr 摘要}' "$detail" \
|
||||||
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
|
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
|
||||||
|
|
||||||
row=$(printf '| %s | [[%s|%s]] | %s | %s | %s | %s |' \
|
# 異常頁先寫,網址後取。頁名的 hash 帶時間戳,每次回報都是一個全新的頁,寫進去之前查網址
|
||||||
"$ts" "$hook 異常 $ts" "$page" "$repo" "$hook" "$code" "$summary")
|
# 一定是 404,拿到的必然是空字串;目錄頁那一列會變成沒有連結的死字,stdout 也少一半。
|
||||||
|
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
||||||
|
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 目錄頁與異常頁分屬兩個存取庫,wiki 的 [[頁名]] 只在同一個存取庫內解得開,跨庫一律解成
|
||||||
|
# 死連結。所以目錄頁指向異常頁的連結一律用絕對網址。
|
||||||
|
# 網址取不到不算失敗:異常頁已經寫成功了,只是這一列少一條連結。這裡把原因記下來走 stderr,
|
||||||
|
# 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。
|
||||||
|
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null)
|
||||||
|
url_code=$?
|
||||||
|
url_note=''
|
||||||
|
if [ "$url_code" -ne 0 ]; then
|
||||||
|
url=''
|
||||||
|
case "$url_code" in
|
||||||
|
4) url_note='頁面查不到(wiki-url 回 4),寫入後尚未生效' ;;
|
||||||
|
5) url_note='回應裡沒有 html_url(wiki-url 回 5)' ;;
|
||||||
|
7) url_note='金鑰失效或權限不足(wiki-url 回 7)' ;;
|
||||||
|
*) url_note="wiki-url 結束碼 $url_code" ;;
|
||||||
|
esac
|
||||||
|
echo "[jsc] 取不到 $page 的網址:$url_note。目錄頁那一列與輸出只留頁名。" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只印頁名,不印一個空欄位
|
||||||
|
if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
row=$(printf '| %s | [%s](%s) | %s | %s | %s | %s |' \
|
||||||
|
"$ts" "$hook 異常 $ts" "$url" "$repo" "$hook" "$code" "$summary")
|
||||||
|
|
||||||
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
||||||
fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
|
fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
|
||||||
| fill '{HASH}' "$hash" \
|
| fill '{error url}' "$url" \
|
||||||
| fill '{error title}' "$hook 異常 $ts" \
|
| fill '{error title}' "$hook 異常 $ts" \
|
||||||
| fill '{owner}/{repo}' "$repo" \
|
| fill '{owner}/{repo}' "$repo" \
|
||||||
| fill '{hook_name}' "$hook" \
|
| fill '{hook_name}' "$hook" \
|
||||||
@@ -129,6 +174,12 @@ build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁
|
|||||||
| fill '{error_summary}' "$summary" > "$tmp_list"
|
| fill '{error_summary}' "$summary" > "$tmp_list"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [ -z "$crepo" ]; then
|
||||||
|
echo "[jsc] 目錄頁的 wiki 存取庫解不出來,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2
|
||||||
|
emit
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過
|
# 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過
|
||||||
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
|
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
|
||||||
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
|
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
|
||||||
@@ -138,27 +189,19 @@ build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁
|
|||||||
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
|
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
|
||||||
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
|
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
|
||||||
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
|
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
|
||||||
sh "$gsh" wiki-get "$wrepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null
|
sh "$gsh" wiki-get "$crepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null
|
||||||
get_code=$?
|
get_code=$?
|
||||||
contents_skip=''
|
|
||||||
case "$get_code" in
|
case "$get_code" in
|
||||||
0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;;
|
0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;;
|
||||||
4) build_contents ;;
|
4) build_contents ;;
|
||||||
*) contents_skip=$get_code ;;
|
*)
|
||||||
|
echo "[jsc] 讀取目錄頁失敗($crepo,wiki-get 結束碼 $get_code),舊內容未知,不寫目錄頁;$page 已建立。" >&2
|
||||||
|
exit 4 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
if ! sh "$gsh" wiki-put "$crepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
|
||||||
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
echo "[jsc] 寫入目錄頁失敗($crepo),$page 已建立。" >&2
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
if [ -n "$contents_skip" ]; then
|
|
||||||
echo "[jsc] 讀取 ERROR_CONTENTS 失敗($wrepo,wiki-get 結束碼 $contents_skip),舊內容未知,不寫目錄頁;$page 已建立。" >&2
|
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
if ! sh "$gsh" wiki-put "$wrepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
|
|
||||||
echo "[jsc] 寫入 ERROR_CONTENTS 失敗($wrepo),$page 已建立。" >&2
|
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null || true)
|
emit
|
||||||
printf '%s %s\n' "$page" "$url"
|
|
||||||
|
|||||||
+53
-11
@@ -1506,16 +1506,57 @@ if [ "$action" = smoke ]; then
|
|||||||
# 舊格式的單一狀態檔照樣擋得下來。用暫時目錄是為了不動到使用者真正的
|
# 舊格式的單一狀態檔照樣擋得下來。用暫時目錄是為了不動到使用者真正的
|
||||||
# $JSC_HOME/restart-required.d/——冒煙測試不該把別人的閘門拆掉。
|
# $JSC_HOME/restart-required.d/——冒煙測試不該把別人的閘門拆掉。
|
||||||
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
|
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
|
||||||
smoke_rs_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_RESTART_GATE 值(可省略)
|
#
|
||||||
|
# 「擋下」的形態依 CLI 而定,事實來源是 hooks/deny.sh 的 case:claude、codex、copilot 與認不得
|
||||||
|
# 的代號走 stderr 加結束碼 2;antigravity 改印一行 stdout 的 deny JSON,kiro 只能注入警告,
|
||||||
|
# 這兩支的結束碼都固定 0。所以預期擋下不能寫死 2。
|
||||||
|
# 也不能一律放寬成 0:那兩支上放行也是 0,放寬之後「該擋沒擋」與「正確擋下」完全同形,
|
||||||
|
# 這道斷言等於作廢。解法是結束碼與擋人標記一起比,而且預期放行的案例要反過來確認標記沒出現,
|
||||||
|
# 兩個方向都守得住。既有的 smoke_dn_case 與 smoke_cx_case 已經是這個做法,這裡沿用同一套。
|
||||||
|
smoke_deny_rc() { # 這支 CLI 擋下時的結束碼
|
||||||
|
case "$cli" in antigravity|kiro) printf 0 ;; *) printf 2 ;; esac
|
||||||
|
}
|
||||||
|
smoke_deny_mark() { # 結束碼分不出擋下與放行時,輸出必定出現的擋人標記;空字串代表結束碼已經夠用
|
||||||
|
case "$cli" in
|
||||||
|
antigravity) printf '"decision":"deny"' ;;
|
||||||
|
kiro) printf 'kiro 擋不下技能叫用' ;;
|
||||||
|
*) printf '' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
smoke_deny_ok() { # $1=預期(數字,或字面值 deny)$2=實際結束碼 $3=實際輸出;回 0 代表通過
|
||||||
|
_d_want=$1; _d_rc=$2; _d_out=$3
|
||||||
|
_d_mark=$(smoke_deny_mark)
|
||||||
|
if [ "$_d_want" = deny ]; then
|
||||||
|
[ "$_d_rc" -eq "$(smoke_deny_rc)" ] || return 1
|
||||||
|
[ -n "$_d_mark" ] || return 0
|
||||||
|
printf '%s' "$_d_out" | grep -qF "$_d_mark"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
[ "$_d_rc" -eq "$_d_want" ] || return 1
|
||||||
|
[ -n "$_d_mark" ] || return 0
|
||||||
|
! printf '%s' "$_d_out" | grep -qF "$_d_mark"
|
||||||
|
}
|
||||||
|
smoke_deny_desc() { # $1=預期;把預期講成失敗訊息看得懂的形態
|
||||||
|
_d_mark=$(smoke_deny_mark)
|
||||||
|
if [ "$1" = deny ]; then
|
||||||
|
if [ -n "$_d_mark" ]; then printf 'exit %s 且輸出含「%s」' "$(smoke_deny_rc)" "$_d_mark"
|
||||||
|
else printf 'exit %s' "$(smoke_deny_rc)"; fi
|
||||||
|
else
|
||||||
|
if [ -n "$_d_mark" ]; then printf 'exit %s 且輸出不含「%s」' "$1" "$_d_mark"
|
||||||
|
else printf 'exit %s' "$1"; fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
smoke_rs_case() { # $1=情境 $2=技能名 $3=預期(結束碼,或 deny)$4=JSC_RESTART_GATE 值(可省略)
|
||||||
_out=$(JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \
|
_out=$(JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \
|
||||||
JSC_RESTART_GATE="${4:-}" sh "$HOOKS/restart-gate.sh" </dev/null 2>&1); _rc=$?
|
JSC_RESTART_GATE="${4:-}" sh "$HOOKS/restart-gate.sh" </dev/null 2>&1); _rc=$?
|
||||||
smoke_n_rs=$((smoke_n_rs + 1))
|
smoke_n_rs=$((smoke_n_rs + 1))
|
||||||
if [ "$_rc" -eq "$3" ]; then
|
if smoke_deny_ok "$3" "$_rc" "$_out"; then
|
||||||
printf '[jsc] restart-gate.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
|
printf '[jsc] restart-gate.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
|
||||||
else
|
else
|
||||||
smoke_fails=$((smoke_fails + 1))
|
smoke_fails=$((smoke_fails + 1))
|
||||||
printf '[jsc] restart-gate.sh(%s):exit %s,預期 %s,重啟閘門判定壞了:%s\n' \
|
printf '[jsc] restart-gate.sh(%s):exit %s,預期 %s,重啟閘門判定壞了:%s\n' \
|
||||||
"$1" "$_rc" "$3" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
"$1" "$_rc" "$(smoke_deny_desc "$3")" \
|
||||||
|
"$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
# 狀態檔在不在也要比:一支 CLI 一份的重點就在「該留的留、該刪的刪」,只看結束碼看不出來。
|
# 狀態檔在不在也要比:一支 CLI 一份的重點就在「該留的留、該刪的刪」,只看結束碼看不出來。
|
||||||
@@ -1540,7 +1581,7 @@ if [ "$action" = smoke ]; then
|
|||||||
JSC_HOME="$rs_home" JSC_CLI="$cli" \
|
JSC_HOME="$rs_home" JSC_CLI="$cli" \
|
||||||
sh "$HOOKS/restart-gate.sh" require update hooks cli </dev/null 2>/dev/null
|
sh "$HOOKS/restart-gate.sh" require update hooks cli </dev/null 2>/dev/null
|
||||||
smoke_rs_file "require 寫出當前 CLI 那一份" "$rs_dir/$cli" exist
|
smoke_rs_file "require 寫出當前 CLI 那一份" "$rs_dir/$cli" exist
|
||||||
smoke_rs_case "當前 CLI 那份存在,技能 jsc-sdlc:implement" jsc-sdlc:implement 2
|
smoke_rs_case "當前 CLI 那份存在,技能 jsc-sdlc:implement" jsc-sdlc:implement deny
|
||||||
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-cli:deploy" jsc-cli:deploy 0
|
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-cli:deploy" jsc-cli:deploy 0
|
||||||
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-gitea:wiki" jsc-gitea:wiki 0
|
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-gitea:wiki" jsc-gitea:wiki 0
|
||||||
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-log:worklog" jsc-log:worklog 0
|
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-log:worklog" jsc-log:worklog 0
|
||||||
@@ -1557,7 +1598,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# 舊格式的單一狀態檔(過渡相容):沒有 per-CLI 資訊,所以一律擋,clear 一併刪掉。
|
# 舊格式的單一狀態檔(過渡相容):沒有 per-CLI 資訊,所以一律擋,clear 一併刪掉。
|
||||||
printf 'at=%s\nmode=update\ndomains=hooks\ncli=%s\n' "$(now_iso)" "$rs_other" \
|
printf 'at=%s\nmode=update\ndomains=hooks\ncli=%s\n' "$(now_iso)" "$rs_other" \
|
||||||
> "$rs_home/restart-required" 2>/dev/null
|
> "$rs_home/restart-required" 2>/dev/null
|
||||||
smoke_rs_case "舊格式單一狀態檔存在" jsc-sdlc:implement 2
|
smoke_rs_case "舊格式單一狀態檔存在" jsc-sdlc:implement deny
|
||||||
JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SESSION_ID=smoke-restart-legacy \
|
JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SESSION_ID=smoke-restart-legacy \
|
||||||
sh "$HOOKS/session-timer.sh" restart </dev/null 2>/dev/null
|
sh "$HOOKS/session-timer.sh" restart </dev/null 2>/dev/null
|
||||||
smoke_rs_file "清除一併刪掉舊格式狀態檔" "$rs_home/restart-required" absent
|
smoke_rs_file "清除一併刪掉舊格式狀態檔" "$rs_home/restart-required" absent
|
||||||
@@ -1658,7 +1699,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# GITEA_HOST 一律清空:放行的案例會繼續往下走到遠端比對,站台推導得出來就會真的連網,
|
# GITEA_HOST 一律清空:放行的案例會繼續往下走到遠端比對,站台推導得出來就會真的連網,
|
||||||
# 冒煙不該依賴網路,也不該讓遠端落後把預期放行的案例判成擋下。
|
# 冒煙不該依賴網路,也不該讓遠端落後把預期放行的案例判成擋下。
|
||||||
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
|
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
|
||||||
smoke_vg_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=額外環境設定(KEY=值,可省略)
|
smoke_vg_case() { # $1=情境 $2=技能名 $3=預期(結束碼,或 deny)$4=額外環境設定(KEY=值,可省略)
|
||||||
# 第四個參數省略時仍要餵一個合法的 KEY=值 給 env,否則它會把空字串當成要執行的指令。
|
# 第四個參數省略時仍要餵一個合法的 KEY=值 給 env,否則它會把空字串當成要執行的指令。
|
||||||
# 這個名字沒有任何 hook 讀它,只是佔位。
|
# 這個名字沒有任何 hook 讀它,只是佔位。
|
||||||
_extra="${4:-JSC_VERSION_GUARD_UNUSED=1}"
|
_extra="${4:-JSC_VERSION_GUARD_UNUSED=1}"
|
||||||
@@ -1668,12 +1709,13 @@ if [ "$action" = smoke ]; then
|
|||||||
JSC_TOOL_NAME=Skill JSC_SKILL="$2" SKILL="$2" "$_extra" \
|
JSC_TOOL_NAME=Skill JSC_SKILL="$2" SKILL="$2" "$_extra" \
|
||||||
sh "$HOOKS/version-guard.sh" </dev/null 2>&1); _rc=$?
|
sh "$HOOKS/version-guard.sh" </dev/null 2>&1); _rc=$?
|
||||||
smoke_n_vg=$((smoke_n_vg + 1))
|
smoke_n_vg=$((smoke_n_vg + 1))
|
||||||
if [ "$_rc" -eq "$3" ]; then
|
if smoke_deny_ok "$3" "$_rc" "$vg_out"; then
|
||||||
printf '[jsc] version-guard.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
|
printf '[jsc] version-guard.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
|
||||||
else
|
else
|
||||||
smoke_fails=$((smoke_fails + 1))
|
smoke_fails=$((smoke_fails + 1))
|
||||||
printf '[jsc] version-guard.sh(%s):exit %s,預期 %s,相依版本判定壞了:%s\n' \
|
printf '[jsc] version-guard.sh(%s):exit %s,預期 %s,相依版本判定壞了:%s\n' \
|
||||||
"$1" "$_rc" "$3" "$(printf '%s' "$vg_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
"$1" "$_rc" "$(smoke_deny_desc "$3")" \
|
||||||
|
"$(printf '%s' "$vg_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
# 訊息內容也要比:擋下來卻沒指名是哪一個相依落後,使用者看不出要更新哪一個 plugin,
|
# 訊息內容也要比:擋下來卻沒指名是哪一個相依落後,使用者看不出要更新哪一個 plugin,
|
||||||
@@ -1710,7 +1752,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# 一、相依確定落後:擋下,而且訊息要指名是哪一項、需要哪一版、目前哪一版。
|
# 一、相依確定落後:擋下,而且訊息要指名是哪一項、需要哪一版、目前哪一版。
|
||||||
printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=9.9.9"}}}\n' \
|
printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=9.9.9"}}}\n' \
|
||||||
> "$vg_manifest"
|
> "$vg_manifest"
|
||||||
smoke_vg_case "相依落後" jsc-sdlc:implement 2
|
smoke_vg_case "相依落後" jsc-sdlc:implement deny
|
||||||
smoke_vg_msg "相依落後時訊息指名那一項" "jsc-cli 需要 >=9.9.9,目前 0.1.0"
|
smoke_vg_msg "相依落後時訊息指名那一項" "jsc-cli 需要 >=9.9.9,目前 0.1.0"
|
||||||
|
|
||||||
# 二、相依相等或超前都放行:只擋落後,本機超前是開發技能組時的常態,擋了維護者自己動不了。
|
# 二、相依相等或超前都放行:只擋落後,本機超前是開發技能組時的常態,擋了維護者自己動不了。
|
||||||
@@ -1724,7 +1766,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# 三、豁免技能:這一條最要緊,它證明閘門不會把更新與修復的唯一路徑鎖死。同一個 jsc-cli
|
# 三、豁免技能:這一條最要緊,它證明閘門不會把更新與修復的唯一路徑鎖死。同一個 jsc-cli
|
||||||
# plugin 底下,非豁免的技能在相依落後時被擋、豁免的那一支照樣放行,差別只在豁免清單。
|
# plugin 底下,非豁免的技能在相依落後時被擋、豁免的那一支照樣放行,差別只在豁免清單。
|
||||||
smoke_vg_case "豁免技能 jsc-cli:deploy,相依落後照樣放行" jsc-cli:deploy 0
|
smoke_vg_case "豁免技能 jsc-cli:deploy,相依落後照樣放行" jsc-cli:deploy 0
|
||||||
smoke_vg_case "同 plugin 的非豁免技能 jsc-cli:doctor" jsc-cli:doctor 2
|
smoke_vg_case "同 plugin 的非豁免技能 jsc-cli:doctor" jsc-cli:doctor deny
|
||||||
|
|
||||||
# 四、fail-open 四條:沒有證據不等於落後。五支 CLI 只有 claude 讀得到本機載入版本,
|
# 四、fail-open 四條:沒有證據不等於落後。五支 CLI 只有 claude 讀得到本機載入版本,
|
||||||
# 這四條任何一條改成擋人,另外四支就會被整批鎖死。
|
# 這四條任何一條改成擋人,另外四支就會被整批鎖死。
|
||||||
@@ -1746,7 +1788,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# 前面那一項則是滿足的:漏掉最後一筆就會變成放行,當場被這一條抓出來。
|
# 前面那一項則是滿足的:漏掉最後一筆就會變成放行,當場被這一條抓出來。
|
||||||
printf '{\n "name": "jsc-sdlc",\n "version": "0.1.0",\n "jsc": {\n "requires": {\n "jsc-hooks": ">=0.0.1",\n "jsc-cli": ">=9.9.9"\n }\n }\n}\n' \
|
printf '{\n "name": "jsc-sdlc",\n "version": "0.1.0",\n "jsc": {\n "requires": {\n "jsc-hooks": ">=0.0.1",\n "jsc-cli": ">=9.9.9"\n }\n }\n}\n' \
|
||||||
> "$vg_manifest"
|
> "$vg_manifest"
|
||||||
smoke_vg_case "多行縮排 manifest,落後的相依擺在最後一個鍵" jsc-sdlc:implement 2
|
smoke_vg_case "多行縮排 manifest,落後的相依擺在最後一個鍵" jsc-sdlc:implement deny
|
||||||
smoke_vg_msg "最後一個鍵的落後項也列進訊息" "jsc-cli 需要 >=9.9.9,目前 0.1.0"
|
smoke_vg_msg "最後一個鍵的落後項也列進訊息" "jsc-cli 需要 >=9.9.9,目前 0.1.0"
|
||||||
rm -rf "$vg_home"
|
rm -rf "$vg_home"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user