Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5493539046 | ||
|
|
b7f9f1293e | ||
|
|
3f0e5df673 | ||
|
|
7f6e0d9299 | ||
|
|
f1255038f6 | ||
|
|
27a3865e87 | ||
|
|
d6f1e0e7ba | ||
|
|
8e37ab3191 | ||
|
|
1684e5636d | ||
|
|
858d3521bd | ||
|
|
2704a7fa8f | ||
|
|
e5a9b05164 | ||
|
|
95c7bb1eee | ||
|
|
f0fd92eb33 | ||
|
|
70526244c7 | ||
|
|
221dca70e6 | ||
|
|
358c30c7b8 | ||
|
|
bd1a705ea8 | ||
|
|
02caedf91e | ||
|
|
44d1d00c0c | ||
|
|
4afb282c78 | ||
|
|
9adb39d21a |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.4.2",
|
"version": "0.4.7",
|
||||||
"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.4.2",
|
"version": "0.4.7",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -169,7 +169,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 回報,免得拿範本蓋掉所有既有列。目錄頁那一列指向異常頁,連結一律寫成 `[{文字}]({連結})`,網址取 `jsc-gitea` 的 `gitea.sh wiki-url` 印出的那一個,不自己組路徑。寫進那一格之前,先把那個網址交給 `jsc-gitea` 的 `tools/link-check.sh` 驗一次,結束碼 0 才寫連結;`link-check.sh` 的路徑由已經解出來的 `gitea.sh` 推得,兩支同一個 tools 目錄。驗不過(含找不到 `link-check.sh`、`GITEA_HOST` 未設定回 3、金鑰失效回 7)就只在那一格留純文字頁名,那一列照寫、異常頁照寫、結束碼照舊,原因走 stderr——回報失敗不該再變成一次失敗。網址在異常頁寫成功之後才取:頁名的 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/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在異常目錄頁附上一個索引區塊。目錄頁一筆一個 H2 區塊,標題就是那一頁的頁名 `ERROR_{HASH}`,欄位是標題底下的一層條列(時間、頁名、存取庫名稱、觸發 hook、退出碼、摘要各一條,格式 `- {欄位名}:{值}`),頁上不留 markdown 表格。目錄頁的讀回、比對與整頁寫回交給 `jsc-gitea` 的 `tools/wiki-contents.sh upsert ERROR 2 {頁名} {區塊檔} {範本}`,本腳本只組自己那一個區塊:同一筆已經有區塊就整塊換掉,沒有才附加到頁尾,一律 upsert,不整頁覆蓋,也不動別人的區塊;那個 `2` 是舊表格版目錄頁裡持有身分的欄位序號(第 2 欄是頁名),舊頁自動轉條列時要靠它取標題。「讀得回舊內容才寫」的判斷由 `wiki-contents.sh` 一手包辦,只有 `wiki-get` 回 4(頁面真的不存在)才用範本建新頁。區塊裡「頁名」那一條指向異常頁,連結一律寫成 `[{文字}]({連結})`,網址取 `jsc-gitea` 的 `gitea.sh wiki-url` 印出的那一個,不自己組路徑。寫進那一條之前,先把那個網址交給 `jsc-gitea` 的 `tools/link-check.sh` 驗一次,結束碼 0 才寫連結;`link-check.sh` 與 `wiki-contents.sh` 的路徑都由已經解出來的 `gitea.sh` 推得,三支同一個 tools 目錄。驗不過(含找不到 `link-check.sh`、`GITEA_HOST` 未設定回 3、金鑰失效回 7)就只在那一條留純文字頁名,那一條照寫、異常頁照寫、結束碼照舊,原因走 stderr——回報失敗不該再變成一次失敗。網址在異常頁寫成功之後才取:頁名的 hash 帶時間戳,每次回報都是全新的頁,寫進去之前查一定是 404,先查就只拿得到空字串。取不到網址時只印頁名,原因走 stderr,結束碼照舊回 0。wiki 位置分兩處解析:異常頁走 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR`,目錄頁由 `wiki-contents.sh` 自己走 `wiki-repo CONTENTS`,兩者是兩個不同的存取庫。異常頁的存取庫解不出來就整支安靜降級;只有目錄頁的存取庫解不出來(`wiki-contents.sh` 回 3)或那支腳本不在磁碟上,就只寫異常頁、跳過目錄頁更新,仍回 exit 0;其餘結束碼(1 組不出內容或寫入失敗、2 用法錯誤、4 沒範本、7 金鑰失效、8 其他 API 失敗)都以 exit 4 回報。由操作者手動執行,或由 `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` 狀態檔把模型來源與階段鎖、工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼(模型來源的每個案例各自指定 CLI 代號,不跟著這一輪接線的 CLI 走——偵測鏈已依 CLI 分流;「不知道能力就擋下」的三種情形連訊息裡的逃生門一起驗,只比結束碼的話訊息漏掉逃生門也是綠燈),再用一份暫時的 `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` 狀態檔把模型來源與階段鎖、工作包歸屬、部署後重啟閘門與寫入提交閘門的每條判定路徑各跑一次並比對結束碼(模型來源的每個案例各自指定 CLI 代號,不跟著這一輪接線的 CLI 走——偵測鏈已依 CLI 分流;「不知道能力就擋下」的三種情形連訊息裡的逃生門一起驗,只比結束碼的話訊息漏掉逃生門也是綠燈),再用一份暫時的 `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/report-status.sh` | 技能與 hook 的執行狀態事件流,寫進 `$JSC_HOME/usage/events.jsonl`,一次一行。`skill-start`、`skill-end`、`hook-end` 三個記錄子命令;`drain` 印出上次排空之後的新事件(位移存在 `usage/scan-state/events.offset`,檔案比位移小就當作輪替過、從頭讀,不比對 inode——五支 CLI 與容器裡的行程看到的 inode 不保證一致);`rotate` 超過 5 MiB 就改名成 `.1` 並把位移歸零,只留一份舊的。`status` 是 `ok`、`blocked`、`failed`、`degraded`、`aborted` 五選一。**三個記錄子命令一律回 0,寫檔失敗也是 0**:回報機制自己壞掉,不可以讓被回報的東西跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。參數檢查是例外,那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以以 2 擋在記錄之前。本檔不讀 stdin:技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主,所有資訊一律走參數。輪替不放在每次寫入,那等於每次提示多一次系統呼叫;改由巡檢排空之後呼叫。為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI,而且失敗的 hook 自我回報會疊出迴圈,`report-error.sh` 因此刻意不接在失敗的 hook 上,這裡沿用同一條線 |
|
| `tools/report-status.sh` | 技能與 hook 的執行狀態事件流,寫進 `$JSC_HOME/usage/events.jsonl`,一次一行。`skill-start`、`skill-end`、`hook-end` 三個記錄子命令;`drain` 印出上次排空之後的新事件(位移存在 `usage/scan-state/events.offset`,檔案比位移小就當作輪替過、從頭讀,不比對 inode——五支 CLI 與容器裡的行程看到的 inode 不保證一致);`rotate` 超過 5 MiB 就改名成 `.1` 並把位移歸零,只留一份舊的。`status` 是 `ok`、`blocked`、`failed`、`degraded`、`aborted` 五選一。**三個記錄子命令一律回 0,寫檔失敗也是 0**:回報機制自己壞掉,不可以讓被回報的東西跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。參數檢查是例外,那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以以 2 擋在記錄之前。本檔不讀 stdin:技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主,所有資訊一律走參數。輪替不放在每次寫入,那等於每次提示多一次系統呼叫;改由巡檢排空之後呼叫。為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI,而且失敗的 hook 自我回報會疊出迴圈,`report-error.sh` 因此刻意不接在失敗的 hook 上,這裡沿用同一條線 |
|
||||||
| `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` 回報 |
|
||||||
@@ -182,7 +182,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`,彙整所有異常頁,方便先看最新問題再往下追。落在目錄專用存取庫,一律 upsert 附加,連結一律寫成 `[{文字}]({連結})` 且先過 `link-check.sh` 驗過才寫。 |
|
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。版面是 H1 頁名加 `>` 引言,之後一筆一個 H2 區塊,標題就是異常頁頁名,欄位一行一條;範本只留一個示範區塊,用 `{佔位符}` 寫。落在目錄專用存取庫,一律 upsert 附加,連結一律寫成 `[{文字}]({連結})` 且先過 `link-check.sh` 驗過才寫。 |
|
||||||
|
|
||||||
## Skills 目錄
|
## Skills 目錄
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-in
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
||||||
| `JSC_WIKI_REPO_ERROR` | 異常內容頁 `ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO`;還是解不出來就整支 `tools/report-error.sh` 安靜降級,不寫 wiki |
|
| `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_CONTENTS` | 目錄頁 `ERROR_CONTENTS` 所在的 `{owner}/{repo}`,由 `wiki-contents.sh` 解析。目錄頁與內容頁分屬兩個不同的存取庫,各解各的 | 退回 `JSC_WIKI_REPO`;還是解不出來時 `wiki-contents.sh` 回 3,只寫內容頁、跳過目錄頁更新,`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` |
|
||||||
|
|||||||
+55
-10
@@ -20,6 +20,17 @@ mkdir -p "$JSC_HOME/sessions" "$JSC_HOME/usage" 2>/dev/null || true
|
|||||||
JSC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
JSC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||||
JSC_SCRIPT_DIR="${JSC_SCRIPT_DIR:-.}"
|
JSC_SCRIPT_DIR="${JSC_SCRIPT_DIR:-.}"
|
||||||
|
|
||||||
|
# stdin JSON 的預設值。這一行要在任何讀取它的函式之前。
|
||||||
|
#
|
||||||
|
# 為什麼一定要有:hook_trace() 裝的 EXIT trap 會在腳本結束時經由 emit_event() 呼叫
|
||||||
|
# session_id(),而 session_id() 第一件事就是拿 json_str() 去讀這個變數。腳本在 read_stdin
|
||||||
|
# 之前就離開(只印規則的子命令、掃整個工作區的用法、逃生門關閉、模式不認得)時,變數還
|
||||||
|
# 沒人設過,開了 `set -u` 的腳本收尾就會往標準錯誤吐一行「參數未設定」。事件其實照樣寫得
|
||||||
|
# 進去,訊息卻讓呼叫端誤判本體失敗——而以「安靜回 0」為通過判準的流程,會因此整條失準。
|
||||||
|
# 補在這裡而不是各腳本各補一次:讀這個變數的是共用函式,補在共用處才涵蓋每一條離開路徑。
|
||||||
|
# 用 `${STDIN_JSON-}` 而不是直接指派空字串:呼叫端已經帶值進來時要原樣保留。
|
||||||
|
STDIN_JSON="${STDIN_JSON-}"
|
||||||
|
|
||||||
# 讀完 stdin(可能為空;非阻塞宿主)
|
# 讀完 stdin(可能為空;非阻塞宿主)
|
||||||
read_stdin() {
|
read_stdin() {
|
||||||
if [ -t 0 ]; then STDIN_JSON=""; else STDIN_JSON=$(cat 2>/dev/null || true); fi
|
if [ -t 0 ]; then STDIN_JSON=""; else STDIN_JSON=$(cat 2>/dev/null || true); fi
|
||||||
@@ -229,27 +240,61 @@ cli_name() {
|
|||||||
else printf 'unknown'; fi
|
else printf 'unknown'; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 把一個檔案路徑正規化成不含 `..` 的實體路徑。解不出來就回傳 1。
|
||||||
|
#
|
||||||
|
# 為什麼一定要正規化:路徑裡的 `..` 一旦要穿過符號連結,兩種解法會給出不同的答案。
|
||||||
|
# 核心與 `[ -f ]` 走實體解析:先跟著連結走到目標,再從目標往上退。
|
||||||
|
# shell 的 `cd` 走邏輯解析:把 `..` 當純文字消去,退回的是連結自己的上層目錄。
|
||||||
|
# 找別的 plugin 是靠自己的位置往上退幾層再往下找,而安裝版面的腳本目錄正是經由一條符號
|
||||||
|
# 連結被叫到的,退層數一超過連結目標底下的深度就會踩到這個差異:這裡的 `[ -f ]` 說檔案
|
||||||
|
# 在、把路徑交出去,被呼叫的腳本自己 `cd` 過去卻找不到那個目錄,回一個空輸出與非零結束
|
||||||
|
# 碼。呼叫端只看得到「查詢失敗」,看不出是路徑寫法的問題,於是整道閘門無聲失效。
|
||||||
|
#
|
||||||
|
# 為什麼用 `cd -P` 加 `pwd -P` 而不是 readlink:這兩個都是 shell 內建,不必在 PATH 上找
|
||||||
|
# 外部執行檔。這些函式會在 cron 那種只剩幾段 PATH 的環境下跑,少一個外部相依就少一個
|
||||||
|
# 解不出來的理由。`-P` 是逐段跟著連結走的那一種解法,跟核心的答案一致。
|
||||||
|
jsc_abs_path() { # $1=檔案路徑
|
||||||
|
[ -n "${1:-}" ] || return 1
|
||||||
|
_ap_dir=$(CDPATH= cd -P -- "$(dirname -- "$1")" 2>/dev/null && pwd -P) || return 1
|
||||||
|
[ -n "$_ap_dir" ] || return 1
|
||||||
|
case "$_ap_dir" in
|
||||||
|
*/) printf '%s%s\n' "$_ap_dir" "$(basename -- "$1")" ;;
|
||||||
|
*) printf '%s/%s\n' "$_ap_dir" "$(basename -- "$1")" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則),
|
# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則),
|
||||||
# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。
|
# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。
|
||||||
# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。
|
# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。
|
||||||
|
#
|
||||||
|
# 每一條候選路徑都先湊出來、最後統一過 jsc_abs_path 才交出去,理由見該函式的說明:
|
||||||
|
# 這裡的候選帶著 `..`,而那些 `..` 要穿過安裝版面的符號連結,交出去的原樣路徑
|
||||||
|
# 只有 `[ -f ]` 認得,被呼叫的腳本自己 `cd` 過去會失敗。正規化失敗時退回原樣路徑,
|
||||||
|
# 讓「找得到」這件事的判準不因為多了一道正規化而變嚴。
|
||||||
jsc_gitea_sh() {
|
jsc_gitea_sh() {
|
||||||
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
_c=""
|
||||||
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
|
|
||||||
fi
|
|
||||||
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
|
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
|
||||||
|
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
||||||
|
_c="$JSC_GITEA_TOOLS/gitea.sh"
|
||||||
|
fi
|
||||||
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
|
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
|
||||||
for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
|
if [ -z "$_c" ]; then
|
||||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
for _p in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
|
||||||
|
[ -f "$_p" ] && { _c="$_p"; break; }
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
|
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
|
||||||
_c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
|
if [ -z "$_c" ]; then
|
||||||
|
_p=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
|
||||||
"$_root"/../../gitea/*/tools/gitea.sh \
|
"$_root"/../../gitea/*/tools/gitea.sh \
|
||||||
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
|
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
|
||||||
| sort | tail -n1)
|
| sort | tail -n1)
|
||||||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
[ -n "$_p" ] && [ -f "$_p" ] && _c="$_p"
|
||||||
_c=$(command -v gitea.sh 2>/dev/null || true)
|
fi
|
||||||
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
[ -n "$_c" ] || _c=$(command -v gitea.sh 2>/dev/null || true)
|
||||||
return 1
|
[ -n "$_c" ] || return 1
|
||||||
|
_n=$(jsc_abs_path "$_c") && [ -n "$_n" ] && _c="$_n"
|
||||||
|
printf '%s\n' "$_c"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名)
|
# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.4.2",
|
"version": "0.4.7",
|
||||||
"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`(異常頁與索引目錄頁分屬兩個存取庫,各自解析;只解不出目錄頁的存取庫時異常頁照寫、索引跳過,回報要講明那一頁沒被索引)、目錄頁那一列指向異常頁的連結一律寫成 `[{文字}]({連結})`,網址取 `gitea.sh wiki-url`,寫進去之前先過 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫連結、驗不過那一格只留純文字頁名而那一列與異常頁照寫(`report-error.sh` 內部做完,結束碼不變)、逐 CLI 回報五道關卡的結果、最後由主代理呼叫一次 `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {結束碼} {detail}` 記下整輪怎麼結束。腳本在同一個存取庫,用 `tools/` 相對路徑;這一筆只由主代理寫一次,寫在並行的各 CLI sub agent 裡會變成五筆互相矛盾的結局。腳本不在就安靜跳過,回報失敗不得變成接線失敗 |
|
| 關鍵步驟 | 先跑前置步驟解出兩個字面絕對路徑:`readlink -f "$JSC_HOME/current"` 解出連結農場根目錄(跨 domain 呼叫用它),`readlink -f "$JSC_HOME/current/jsc-hooks"` 解出 jsc-hooks 的實體根目錄(只有 `wire-cli.sh` 從這裡跑。它會改寫自己正踩著的那條連結,但它自己已經把 `HERE` 與 `ROOT` 解成實體路徑,`ln -sfn` 不會再把連結指向自己,所以從實體根目錄跑現在是多一層保險、不是唯一防線;照做的理由是舊版腳本還在別的機器上跑,那些版本走連結跑仍會把連結寫成指向自己、全機器 hook 一起失效)、兩個路徑各解一次不重解、各自在同一步用 `[ -d ]` 查過印出來的目錄真的存在(`JSC_HOME` 沒設時第一條會印出 `/current`、結束碼 0,非空又是絕對路徑,只查前三項擋不下來),任何一條解不出來、不是絕對路徑、或目錄不存在就停手回報是哪一條沒解出來並叫人跑 `jsc-cli:deploy`,不接任何線也不猜路徑、不退回帶版本號的快取路徑、之後每一次腳本呼叫都用解出來的字面絕對路徑開頭、取得 CLI 清單(呼叫端交來的優先,沒有才自己跑 `detect-clis.sh`)、第一支 CLI 單獨跑完整條管線(它負責更新共用的 `{連結農場根}/jsc-hooks` 連結)、其餘 CLI 一支一個 sub agent 並行、每支 CLI 依序走 purge、接線、status、smoke、scan 五道關卡、讀每道關卡自己印的第一行判定、任一關卡出錯就寫 `ERROR_{HASH}` 並轉給 `jsc-hooks:repair`(異常頁與索引目錄頁分屬兩個存取庫,各自解析:異常頁由 `report-error.sh` 走 `wiki-repo ERROR`,目錄頁由 `wiki-contents.sh` 走 `wiki-repo CONTENTS`;只解不出目錄頁的存取庫時異常頁照寫、索引跳過,回報要講明那一頁沒被索引)、目錄頁上這一筆是一個 H2 區塊,標題就是異常頁頁名,時間、頁名、存取庫名稱、觸發 hook、退出碼、摘要各一條條列,寫成 `- {欄位名}:{值}`,頁上不留 markdown 表格、「頁名」那一條指向異常頁的連結一律寫成 `[{文字}]({連結})`,網址取 `gitea.sh wiki-url`,寫進去之前先過 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫連結、驗不過那一條只留純文字頁名而那一條與異常頁照寫(`report-error.sh` 內部做完,結束碼不變)、目錄頁的讀回、比對與整頁寫回一律交給 `jsc-gitea/tools/wiki-contents.sh upsert ERROR 2 {頁名} {區塊檔} {範本}`,`report-error.sh` 只組自己那一個區塊,找得到同名 H2 就整塊換掉、找不到就附加到頁尾、逐 CLI 回報五道關卡的結果、最後由主代理呼叫一次 `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {結束碼} {detail}` 記下整輪怎麼結束。腳本在同一個存取庫,用 `tools/` 相對路徑;這一筆只由主代理寫一次,寫在並行的各 CLI sub agent 裡會變成五筆互相矛盾的結局。腳本不在就安靜跳過,回報失敗不得變成接線失敗 |
|
||||||
| 外部呼叫 | `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`)、`jsc-gitea/tools/gitea.sh wiki-url` 與 `jsc-gitea/tools/link-check.sh`(同樣經 `report-error.sh`,取目錄頁那一列的網址並驗它連得到);接線腳本內部另呼叫 `hooks/skill-name.sh` 與 `hooks/deny.sh` 做冒煙斷言 |
|
| 外部呼叫 | `readlink -f`(前置步驟解兩個根目錄,各一次)、`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`)、`jsc-gitea/tools/gitea.sh wiki-url` 與 `jsc-gitea/tools/link-check.sh`(同樣經 `report-error.sh`,取目錄頁那一條的網址並驗它連得到)、`jsc-gitea/tools/wiki-contents.sh upsert`(同樣經 `report-error.sh`,把那一個 H2 區塊 upsert 進索引目錄頁);接線腳本內部另呼叫 `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 連結,而且目錄頁那一列的連結驗不過時,回報要講明那一列只有純文字頁名、沒有連結。以上收完之後還要走完最後一步:呼叫 `report-status.sh skill-end`,狀態五選一——每一支偵測到的 CLI 五道關卡全過而且一律回 `wired` 是 `ok`,實務上等於那台機器沒有 kiro;跑完整輪但有一部分沒到 `wired` 是 `degraded`,涵蓋 kiro 只能降級接線,以及某支 CLI 接線失敗、已經轉交 `jsc-hooks:repair` 而且拿到一條對 `develop` 的 PR,還有被跳過的 CLI;關卡失敗而沒人接手是 `failed`,也就是修正技能起不來或回不出 PR,壞掉的 hook 還掛在那裡;`JSC_READONLY=1` 讓 `wire-cli.sh` 回 exit 6、一支 CLI 都沒動是 `blocked`;偵測不到任何 CLI 而主動停止是 `aborted`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
| 完成條件 | 前置步驟解出的兩個根目錄都是一條存在的絕對路徑(各自用 `[ -d ]` 查過),而且整個流程沒有任何一次腳本呼叫帶著未展開的變數或波浪號,每一支偵測到的 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}`(落在 `JSC_WIKI_REPO_ERROR` 解出的存取庫)並在索引目錄頁補一列(落在 `JSC_WIKI_REPO_CONTENTS` 解出的另一個存取庫,那一列的第 2 格寫成 `[{頁名}]({絕對網址})`,網址取自 `gitea.sh wiki-url` 且已經過 `link-check.sh` 驗到結束碼 0;驗不過那一格只有純文字頁名,`report-error.sh` 在 stderr 留一行 `[jsc]` 講明是哪一種原因)、修正路徑留下一條對 `develop` 的 PR 。接線完成後 `$JSC_HOME/usage/events.jsonl` 會逐行長出 `{kind:hook}` 事件,每支 hook 每次執行一筆,欄位含 `status` 與實際結束碼;跑過技能之後另有 `{kind:skill,phase:start}`。這支技能自己收尾時,同一個 `$JSC_HOME/usage/events.jsonl` 尾端會多一筆 `{kind:skill,phase:end}`,`name` 是 `jsc-hooks:hooks-install`,整輪只有一筆,`status` 與那次結局相符,`exit` 是決定結局的那道關卡的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,接線結果一字不變。事件寫不進去不影響任何 hook 的結束碼,也不影響本技能的結局 |
|
| 可驗證跡象 | 各 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-hooks` 符號連結建立或更新,而且它指向 jsc-hooks 的實體根目錄、不是指向自己(`readlink -f` 解得出一個存在的目錄,裡面有 `hooks/session-timer.sh` 與 `tools/jsc-wrap.sh`)、各 CLI 設定裡存下來的接線命令也都是展開後的字面絕對路徑,只有存放庫自帶的 `hooks/hooks.json` 保留 `${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks` 這段變數寫法,由 hook 自己的 shell 在執行當下展開、`$JSC_HOME/backup/hooks/{cli}/{時間戳}/` 留下 purge 前的備份、出錯時 wiki 多一頁 `ERROR_{HASH}`(落在 `JSC_WIKI_REPO_ERROR` 解出的存取庫)並在索引目錄頁補一個 H2 區塊(落在 `JSC_WIKI_REPO_CONTENTS` 解出的另一個存取庫,由 `wiki-contents.sh` upsert 進去;H2 標題就是那一頁的頁名,底下六條條列依序是時間、頁名、存取庫名稱、觸發 hook、退出碼、摘要,「頁名」那一條寫成 `[{頁名}]({絕對網址})`,網址取自 `gitea.sh wiki-url` 且已經過 `link-check.sh` 驗到結束碼 0;驗不過那一條只有純文字頁名,`report-error.sh` 在 stderr 留一行 `[jsc]` 講明是哪一種原因;那一頁上不會有 markdown 表格)、修正路徑留下一條對 `develop` 的 PR 。接線完成後 `$JSC_HOME/usage/events.jsonl` 會逐行長出 `{kind:hook}` 事件,每支 hook 每次執行一筆,欄位含 `status` 與實際結束碼;跑過技能之後另有 `{kind:skill,phase:start}`。事件寫不進去不影響任何 hook 的結束碼這支技能自己收尾時,同一個 `$JSC_HOME/usage/events.jsonl` 尾端會多一筆 `{kind:skill,phase:end}`,`name` 是 `jsc-hooks:hooks-install`,整輪只有一筆,`status` 與那次結局相符,`exit` 是決定結局的那道關卡的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,接線結果一字不變。事件寫不進去不影響任何 hook 的結束碼,也不影響本技能的結局 |
|
||||||
|
|
||||||
## repair
|
## repair
|
||||||
|
|
||||||
@@ -20,4 +20,4 @@
|
|||||||
| 關鍵步驟 | 從 `ERROR_{HASH}` 讀失敗情境(沒有頁就讀失敗的 `status=` 那一行,讀不到就停下來問)、跑 `detect-clis.sh`、每一支偵測到的 CLI 各開一個唯讀 sub agent 診斷並交回根因、要改的檔案與驗證指令、挑最小的修正改進 hooks 存取庫(技能名解析改 `hooks/skill-name.sh`、阻擋形態改 `hooks/deny.sh`,兩支是唯一真實來源,不在閘門裡各補一份)、跑 `wire-cli.sh smoke {cli}` 驗到 exit 0、跑 `sync-skill-manifest.sh .` 同步版本、以 `jsc-git:pr` 對 `develop` 開 PR、收尾呼叫 `tools/report-status.sh skill-end jsc-hooks:repair {status} {結束碼} {detail}` 記下這次修正怎麼結束(腳本在同一個存取庫,用 `tools/` 相對路徑,比照 `tools/wire-cli.sh`;檔案不在就安靜跳過,回報失敗不得變成修正失敗) |
|
| 關鍵步驟 | 從 `ERROR_{HASH}` 讀失敗情境(沒有頁就讀失敗的 `status=` 那一行,讀不到就停下來問)、跑 `detect-clis.sh`、每一支偵測到的 CLI 各開一個唯讀 sub agent 診斷並交回根因、要改的檔案與驗證指令、挑最小的修正改進 hooks 存取庫(技能名解析改 `hooks/skill-name.sh`、阻擋形態改 `hooks/deny.sh`,兩支是唯一真實來源,不在閘門裡各補一份)、跑 `wire-cli.sh smoke {cli}` 驗到 exit 0、跑 `sync-skill-manifest.sh .` 同步版本、以 `jsc-git:pr` 對 `develop` 開 PR、收尾呼叫 `tools/report-status.sh skill-end jsc-hooks:repair {status} {結束碼} {detail}` 記下這次修正怎麼結束(腳本在同一個存取庫,用 `tools/` 相對路徑,比照 `tools/wire-cli.sh`;檔案不在就安靜跳過,回報失敗不得變成修正失敗) |
|
||||||
| 外部呼叫 | `jsc-gitea:wiki`、`jsc-cli/tools/detect-clis.sh`、`tools/wire-cli.sh smoke`、`jsc-meta/tools/sync-skill-manifest.sh`、`jsc-git:pr`;診斷階段另以 sub agent 叫用各支已安裝的 AI CLI |
|
| 外部呼叫 | `jsc-gitea:wiki`、`jsc-cli/tools/detect-clis.sh`、`tools/wire-cli.sh smoke`、`jsc-meta/tools/sync-skill-manifest.sh`、`jsc-git:pr`;診斷階段另以 sub agent 叫用各支已安裝的 AI CLI |
|
||||||
| 完成條件 | 修正已經落在磁碟上、`wire-cli.sh smoke` 對受影響的 CLI 回 exit 0、`sync-skill-manifest.sh` 回 exit 0 而且三份 manifest 版本一致,最後拿到一條對 `develop` 的 PR 連結;開不出 PR 時要講明修正已套用但尚未合併、帶上分支名與失敗原因。每一條路線都要走完最後一步:呼叫 `report-status.sh skill-end`,狀態五選一——修正落地、smoke 回 exit 0、三份 manifest 版本一致而且拿到 PR 連結是 `ok`;smoke 過了但東西沒送出去是 `degraded`,也就是開不出 PR 只剩分支,或 manifest 沒對齊;修不好是 `failed`,也就是診斷繞回去以後 smoke 還是回 exit 4,或同步版本踩到環境錯誤,壞掉的接線還是壞的;沒有可修的項目是 `aborted`,也就是讀不到任何失敗情境。本技能豁免版本閘門與部署後重啟閘門,沒有別的閘門擋得住它,所以不會用 `blocked`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
| 完成條件 | 修正已經落在磁碟上、`wire-cli.sh smoke` 對受影響的 CLI 回 exit 0、`sync-skill-manifest.sh` 回 exit 0 而且三份 manifest 版本一致,最後拿到一條對 `develop` 的 PR 連結;開不出 PR 時要講明修正已套用但尚未合併、帶上分支名與失敗原因。每一條路線都要走完最後一步:呼叫 `report-status.sh skill-end`,狀態五選一——修正落地、smoke 回 exit 0、三份 manifest 版本一致而且拿到 PR 連結是 `ok`;smoke 過了但東西沒送出去是 `degraded`,也就是開不出 PR 只剩分支,或 manifest 沒對齊;修不好是 `failed`,也就是診斷繞回去以後 smoke 還是回 exit 4,或同步版本踩到環境錯誤,壞掉的接線還是壞的;沒有可修的項目是 `aborted`,也就是讀不到任何失敗情境。本技能豁免版本閘門與部署後重啟閘門,沒有別的閘門擋得住它,所以不會用 `blocked`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
||||||
| 可驗證跡象 | hooks 存取庫多一個修正提交與一條推上去的分支、`develop` 上多一條 PR、三份 manifest 與 README 技能清單版本一致、`wire-cli.sh smoke` 由失敗轉為 exit 0;不論走哪一條路線,`$JSC_HOME/usage/events.jsonl` 尾端都會多一筆 `{kind:skill,phase:end}` 事件,`name` 是 `jsc-hooks:repair`,`status` 與那次結局相符,`exit` 是決定結局的那支工具的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,修正結果一字不變 |
|
| 可驗證跡象 | hooks 存取庫多一個修正提交與一條推上去的分支、`develop` 上多一條 PR、三份 manifest 與 README 技能清單版本一致、`wire-cli.sh smoke` 由失敗轉為 exit 0。收尾在 `$JSC_HOME/usage/events.jsonl` 留下這一輪的 `skill-end` 事件,`status` 取 `ok`、`blocked`、`failed`、`degraded` 或 `aborted`,中途停下的那幾輪也照寫——只有 start 沒有配對 end 會被讀成中斷;不論走哪一條路線,`$JSC_HOME/usage/events.jsonl` 尾端都會多一筆 `{kind:skill,phase:end}` 事件,`name` 是 `jsc-hooks:repair`,`status` 與那次結局相符,`exit` 是決定結局的那支工具的結束碼;`report-status.sh` 不在那台機器上就沒有這一筆,修正結果一字不變 |
|
||||||
|
|||||||
@@ -7,9 +7,30 @@ description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SD
|
|||||||
|
|
||||||
Goal: make the nine hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`, `restart-gate.sh`, `comment-scope.sh`, `lang-guard.sh`, `write-guard.sh`) effective in every CLI, with nothing else wired alongside them.
|
Goal: make the nine hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`, `version-guard.sh`, `restart-gate.sh`, `comment-scope.sh`, `lang-guard.sh`, `write-guard.sh`) effective in every CLI, with nothing else wired alongside them.
|
||||||
|
|
||||||
|
## Path rule
|
||||||
|
|
||||||
|
**Every script call in this skill is written as a literal absolute path.** A path that still carries `$JSC_HOME`, any other unexpanded variable, or a `~` cannot be resolved statically by the permission layer, so it is treated as unknown and always asks for approval. An unattended round has nobody to approve, so it stops at the first script and the whole install never starts.
|
||||||
|
|
||||||
|
Measured on this machine: `$JSC_HOME/current/jsc-assist/tools/patrol.sh` and `~/.jsc/current/...` were both blocked and the command never ran; the same script at `/root/.jsc/current/...` ran. Adding an allow rule that itself starts with `$JSC_HOME` changed nothing on a retest, because the rule is matched against the expanded command — widening the permission list is not the fix.
|
||||||
|
|
||||||
|
Do not trade this back for portability. A variable-form path in this file buys no portability; it buys a round that dies before its first stage. Portability lives in the prerequisite below, which resolves the roots once, on the machine, at run time.
|
||||||
|
|
||||||
|
## Prerequisite — resolve the roots once
|
||||||
|
|
||||||
|
Run these two before any other call in this skill, and only here:
|
||||||
|
|
||||||
|
1. `readlink -f "$JSC_HOME/current"` prints the link farm as a literal absolute path. Call it `{JSC_ROOT}`. Every cross-domain call is written `{JSC_ROOT}/jsc-{domain}/...` with that path substituted in.
|
||||||
|
2. `readlink -f "$JSC_HOME/current/jsc-hooks"` prints the physical root behind the `jsc-hooks` link. Call it `{HOOKS_ROOT}`. `tools/wire-cli.sh` is called from there and from nowhere else. That script rewrites the very `{JSC_ROOT}/jsc-hooks` link it would be running through and derives its own root from `$0`, so running it through the link makes `ln -sfn` point that link at itself. The loop takes every CLI's hooks down at once, and it has happened.
|
||||||
|
|
||||||
|
**Both results are checked before anything else runs: `[ -d "{JSC_ROOT}" ]` and `[ -d "{HOOKS_ROOT}" ]`, each in the same approved step as its own `readlink`.** A `readlink` that printed something is not a `readlink` that found something. With `JSC_HOME` unset the first call prints `/current` and exits 0 — non-empty, absolute, and wrong — and every literal path built from it then names a place that is not there; the second call has the same hole one level down. An empty result, a non-zero exit, a path that is not absolute, or a directory that does not exist stops the skill here: report which of the two roots did not resolve and what the command printed, say `jsc-cli:deploy` has to run to restore `current` and its `jsc-hooks` link, and wire nothing. Never guess a root, never fall back to a versioned plugin cache path, and never create either root here — a run that pushes on wires every CLI to scripts that are not there, and `purge` has already removed the hooks that worked.
|
||||||
|
|
||||||
|
Resolve both once, here. Do not re-resolve per call, and do not add a tool that prints these paths — two `readlink` runs and their two checks are the whole step. The main agent resolves them and hands both literal paths to every sub agent it starts, so a sub agent never resolves anything itself. Done when you hold two literal absolute paths, both naming directories that exist, and every later call starts with one of them.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. `tools/wire-cli.sh purge` backs up every file it touches before it removes anything, so the removal stays reversible.
|
Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. `tools/wire-cli.sh purge` backs up every file it touches before it removes anything, so the removal stays reversible.
|
||||||
|
|
||||||
The wiring commands stored in user config use `$JSC_HOME/current/jsc-hooks`, not the versioned plugin cache path and not the development checkout. `tools/wire-cli.sh {cli}` creates or refreshes that symlink before it writes `notify`, shell aliases or Kiro hook JSON, then verifies the linked scripts exist. If the filesystem cannot create the symlink, the script must say so and explicitly fall back to the current root; it must never write a silent broken path. The bundled `hooks/hooks.json` follows the same rule: use `${CLAUDE_PLUGIN_ROOT}` only where the host provides it, and fall back to `$JSC_HOME/current/jsc-hooks` for any other CLI reading the same manifest, so an unset Claude-only variable never expands into `/hooks/...`.
|
The wiring commands stored in user config use `{JSC_ROOT}/jsc-hooks`, not the versioned plugin cache path and not the development checkout. `wire-cli.sh` expands that root itself, so what lands in each CLI's config is already a literal absolute path. `{HOOKS_ROOT}/tools/wire-cli.sh {cli}` creates or refreshes that symlink before it writes `notify`, shell aliases or Kiro hook JSON, then verifies the linked scripts exist. If the filesystem cannot create the symlink, the script must say so and explicitly fall back to the current root; it must never write a silent broken path. The bundled `hooks/hooks.json` follows the same rule with one deliberate exception: use `${CLAUDE_PLUGIN_ROOT}` only where the host provides it, and fall back to the manifest's own text, `${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks`, for any other CLI reading the same manifest, so an unset Claude-only variable never expands into `/hooks/...`. That one stays in variable form on purpose: it is file content shipped with the repo, expanded by the hook's own shell on whatever machine reads it, and it never passes through the permission layer. It is not a path anyone types at a prompt, so the path rule above does not reach it.
|
||||||
|
|
||||||
Four of the five CLIs have a pre-tool hook that can block. The version guard and the restart gate reach codex, copilot and antigravity too — each at its own wiring point, with its own matcher and its own blocking shape. Never say a CLI "has no pre-tool hook"; that claim is wrong and it is what left three CLIs unguarded.
|
Four of the five CLIs have a pre-tool hook that can block. The version guard and the restart gate reach codex, copilot and antigravity too — each at its own wiring point, with its own matcher and its own blocking shape. Never say a CLI "has no pre-tool hook"; that claim is wrong and it is what left three CLIs unguarded.
|
||||||
|
|
||||||
@@ -66,16 +87,16 @@ The detailed flow **MUST run as a sub agent**; the main agent only reports the s
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Take the CLI list from the caller when it hands one over — `jsc-cli:deploy` passes the list it already detected, and probing the same five executables a second time buys nothing. Run `jsc-cli/tools/detect-clis.sh` yourself only when no list came in; that fallback is what keeps this skill usable when it is called on its own. The script always exits 0 and prints one `name<TAB>path<TAB>version` line per installed CLI. Done when you hold that list and have said which of the two ways produced it; when it is empty, report that no CLI was detected and stop.
|
1. Take the CLI list from the caller when it hands one over — `jsc-cli:deploy` passes the list it already detected, and probing the same five executables a second time buys nothing. Run `{JSC_ROOT}/jsc-cli/tools/detect-clis.sh` yourself only when no list came in; that fallback is what keeps this skill usable when it is called on its own. The script always exits 0 and prints one `name<TAB>path<TAB>version` line per installed CLI. Done when you hold that list and have said which of the two ways produced it; when it is empty, report that no CLI was detected and stop.
|
||||||
2. Run the five-stage pipeline **purge → wire → status → smoke → scan** once per detected CLI. Run the first CLI's pipeline on its own, because `tools/wire-cli.sh {cli}` is what refreshes the shared `$JSC_HOME/current/jsc-hooks` link and two CLIs must not rewrite it at the same time; once that first pipeline has finished, run every remaining CLI's pipeline in parallel, one sub agent per CLI — the five stages of one CLI stay in this order, but different CLIs touch different config files and share nothing else. Every stage prints its verdict on its first line, so read that line and never infer the outcome from the prose below it.
|
2. Run the five-stage pipeline **purge → wire → status → smoke → scan** once per detected CLI. Run the first CLI's pipeline on its own, because `{HOOKS_ROOT}/tools/wire-cli.sh {cli}` is what refreshes the shared `{JSC_ROOT}/jsc-hooks` link and two CLIs must not rewrite it at the same time; once that first pipeline has finished, run every remaining CLI's pipeline in parallel, one sub agent per CLI — the five stages of one CLI stay in this order, but different CLIs touch different config files and share nothing else. Every stage prints its verdict on its first line, so read that line and never infer the outcome from the prose below it.
|
||||||
1. `tools/wire-cli.sh purge {cli}` — backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Marker matching trims leading and trailing whitespace, so an indented or padded marker block is still removed as the same jsc-owned block. Exit 0 is `purged`, exit 3 is `skipped` (that CLI's executable is not on this machine, so skip its remaining stages too), exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name, not a purge outcome — fix the name and rerun the stage.
|
1. `{HOOKS_ROOT}/tools/wire-cli.sh purge {cli}` — backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Marker matching trims leading and trailing whitespace, so an indented or padded marker block is still removed as the same jsc-owned block. Exit 0 is `purged`, exit 3 is `skipped` (that CLI's executable is not on this machine, so skip its remaining stages too), exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name, not a purge outcome — fix the name and rerun the stage.
|
||||||
2. `tools/wire-cli.sh {cli}` — owns both the wiring and its verification: it refreshes the link, writes the config, alias or hook file inside a `<!-- jsc-hooks -->` (or `# jsc-hooks`) marker block, re-reads every file it wrote, confirms the block is present and correctly placed, and confirms the stored runtime paths resolve to existing scripts before it prints a success status. Exit 0 is `wired` and is the expected result on claude, codex, copilot and antigravity; exit 1 is `degraded` and is expected on kiro alone; exit 3 is `skipped`, exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name — fix the name and rerun. The matcher is verified on its own, not just the presence of a key: a key that is there with the wrong matcher reports as wired and fires never.
|
2. `{HOOKS_ROOT}/tools/wire-cli.sh {cli}` — owns both the wiring and its verification: it refreshes the link, writes the config, alias or hook file inside a `<!-- jsc-hooks -->` (or `# jsc-hooks`) marker block, re-reads every file it wrote, confirms the block is present and correctly placed, and confirms the stored runtime paths resolve to existing scripts before it prints a success status. Exit 0 is `wired` and is the expected result on claude, codex, copilot and antigravity; exit 1 is `degraded` and is expected on kiro alone; exit 3 is `skipped`, exit 4 is `failed` and goes to step 3. Exit 2 is a bad CLI name — fix the name and rerun. The matcher is verified on its own, not just the presence of a key: a key that is there with the wrong matcher reports as wired and fires never.
|
||||||
3. `tools/wire-cli.sh status {cli}` — the read-only inventory of what the previous stage wrote. It writes nothing and runs no hook, so it is safe to run right after wiring. Exit 0 is `wired` (claude, codex, copilot, antigravity), exit 1 is `degraded` (kiro), exit 3 is `skipped`, exit 5 is `unwired`, which names every missing item and means the wiring stage has to run again before you continue. Exit 2 is a bad CLI name. For codex this stage is the only one that reads the installed `jsc-hooks` manifest in the Codex plugin cache and reports a stale `UserPromptSubmit` command there, the one that expands `${CLAUDE_PLUGIN_ROOT}` into `/hooks/...`; carry that item into the report.
|
3. `{HOOKS_ROOT}/tools/wire-cli.sh status {cli}` — the read-only inventory of what the previous stage wrote. It writes nothing and runs no hook, so it is safe to run right after wiring. Exit 0 is `wired` (claude, codex, copilot, antigravity), exit 1 is `degraded` (kiro), exit 3 is `skipped`, exit 5 is `unwired`, which names every missing item and means the wiring stage has to run again before you continue. Exit 2 is a bad CLI name. For codex this stage is the only one that reads the installed `jsc-hooks` manifest in the Codex plugin cache and reports a stale `UserPromptSubmit` command there, the one that expands `${CLAUDE_PLUGIN_ROOT}` into `/hooks/...`; carry that item into the report.
|
||||||
4. `tools/wire-cli.sh smoke {cli}` — runs every wired mode of all nine hooks once, plus each decision path of the work-package check, of the restart gate and of the write and commit guard. It catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. It also runs each CLI's real payload through `skill-name.sh`, each blocking shape through `deny.sh`, and those same payloads straight through `restart-gate.sh` end to end, so a break anywhere along parse, decide and emit is caught — the two ends look healthy on their own while the middle silently passes everything through, which is exactly how three CLIs went unguarded. It prints its own result-line count as `lines<TAB>{count}` and asserts that count against what it expected to run, so read the number from that line and never restate a number of your own. Exit 0 is `ok`, exit 4 is `failed` — either a hook errored or the line count did not match, and both go to step 3. Exit 2 is a bad CLI name.
|
4. `{HOOKS_ROOT}/tools/wire-cli.sh smoke {cli}` — runs every wired mode of all nine hooks once, plus each decision path of the work-package check, of the restart gate and of the write and commit guard. It catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. It also runs each CLI's real payload through `skill-name.sh`, each blocking shape through `deny.sh`, and those same payloads straight through `restart-gate.sh` end to end, so a break anywhere along parse, decide and emit is caught — the two ends look healthy on their own while the middle silently passes everything through, which is exactly how three CLIs went unguarded. It prints its own result-line count as `lines<TAB>{count}` and asserts that count against what it expected to run, so read the number from that line and never restate a number of your own. Exit 0 is `ok`, exit 4 is `failed` — either a hook errored or the line count did not match, and both go 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.
|
5. `{JSC_ROOT}/jsc-hooks/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. 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 — the directory repo would not resolve, so nothing indexes the page; the page URL could not be read back, so the page name comes out on its own; or the URL failed the reachability check, so the directory row carries the page name as plain text with no link — carry that note into step 4. Every link on that row is written as `[{text}]({url})` with the URL from `gitea.sh wiki-url`, and the script checks it with `jsc-gitea/tools/link-check.sh` before writing: exit 0 writes the link, anything else keeps the row and drops the link, and none of it changes the exit code — this is the failure-reporting path, so a failed report must never become a second failure. 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`.
|
3. For each error — a failed purge, a failed wiring, an `unwired` status, a failed smoke, or a scanned error with `jsc=true` — run `{JSC_ROOT}/jsc-hooks/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` in `report-error.sh` itself, the directory through `wiki-repo CONTENTS` inside `wiki-contents.sh`. 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 — the directory repo would not resolve (or `wiki-contents.sh` is not on disk), so nothing indexes the page; the page URL could not be read back, so the page name comes out on its own; or the URL failed the reachability check, so the directory entry's page-name bullet carries the page name as plain text with no link — carry that note into step 4. One error report is one H2 block on the directory page: the heading is that report's own page name, `ERROR_{HASH}`, and the fields sit under it as one bullet each — time, page name, repo, hook, exit code, summary, written `- {field}:{value}` — with no markdown table anywhere on the page. Every link inside that block is written as `[{text}]({url})` with the URL from `gitea.sh wiki-url`, and the script checks it with `jsc-gitea/tools/link-check.sh` before writing: exit 0 writes the link, anything else keeps the bullet and drops the link, and none of it changes the exit code — this is the failure-reporting path, so a failed report must never become a second failure. 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 directory page left untouched because the old one could not be read back. The directory page itself is not assembled here: `report-error.sh` builds only its own block and hands it to `jsc-gitea/tools/wiki-contents.sh upsert ERROR 2 {page name} {block file} {template}`, so the read, the key match and the whole-page write have exactly one owner. That page is upserted, never overwritten: every block on it is somebody else's error report, so the tool reads the page, replaces the block whose heading equals this page name or appends a new block at the end, and writes the page back. 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 blocks 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`.
|
||||||
5. Record how the whole install ended. Run `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {exit} "{detail}"` — the script is in this same repo, so it takes the plain `tools/` path that every other stage above uses. **The main agent makes this one call, after every per-CLI report is in.** The per-CLI pipelines run as parallel sub agents and one skill run is one event, so a call inside those sub agents would write one line per CLI and turn the install's outcome into five contradictory ones. The gate that records a skill's start fires when the skill is loaded and can never see how it ended; without this line a finished install and an install abandoned halfway look identical afterwards, which is the whole reason the closing step exists.
|
5. Record how the whole install ended. Run `tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {exit} "{detail}"` — the script is in this same repo, so it takes the plain `tools/` path that every other stage above uses. **The main agent makes this one call, after every per-CLI report is in.** The per-CLI pipelines run as parallel sub agents and one skill run is one event, so a call inside those sub agents would write one line per CLI and turn the install's outcome into five contradictory ones. The gate that records a skill's start fires when the skill is loaded and can never see how it ended; without this line a finished install and an install abandoned halfway look identical afterwards, which is the whole reason the closing step exists.
|
||||||
- `{status}` is one of five. `ok`: every detected CLI passed all five stages and every one of them reported `wired` — in practice that means kiro was not on the machine. `degraded`: the pipeline ran to the end and part of it did not reach `wired`. That covers kiro, which is `degraded` by design because the CLI cannot block a skill call, and it covers a CLI whose stage failed and was handed to `jsc-hooks:repair` with a PR against `develop` — the failure has an owner and a fix in flight, so the install is incomplete, not broken. A `skipped` CLI belongs here too. `failed`: a stage failed and the failure was left with nobody holding it — `jsc-hooks:repair` could not be started, or it came back with no PR — so a broken hook stays wired and nothing is going to fix it. `blocked`: `wire-cli.sh` refused with exit 6 under `JSC_READONLY=1`, so no CLI was purged or wired at all. `aborted`: step 1 detected no CLI, so there was nothing to wire and the run stopped on a precondition rather than on an error.
|
- `{status}` is one of five. `ok`: every detected CLI passed all five stages and every one of them reported `wired` — in practice that means kiro was not on the machine. `degraded`: the pipeline ran to the end and part of it did not reach `wired`. That covers kiro, which is `degraded` by design because the CLI cannot block a skill call, and it covers a CLI whose stage failed and was handed to `jsc-hooks:repair` with a PR against `develop` — the failure has an owner and a fix in flight, so the install is incomplete, not broken. A `skipped` CLI belongs here too. `failed`: a stage failed and the failure was left with nobody holding it — `jsc-hooks:repair` could not be started, or it came back with no PR — so a broken hook stays wired and nothing is going to fix it. `blocked`: `wire-cli.sh` refused with exit 6 under `JSC_READONLY=1`, so no CLI was purged or wired at all. `aborted`: step 1 detected no CLI, so there was nothing to wire and the run stopped on a precondition rather than on an error.
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
>
|
>
|
||||||
> 存放位置:本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的目錄專用存取庫,與異常頁的存取庫是兩個不同的存取庫。
|
> 存放位置:本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的目錄專用存取庫,與異常頁的存取庫是兩個不同的存取庫。
|
||||||
>
|
>
|
||||||
> 寫入語意:一列代表一次 hook 異常回報。寫入前先讀回整頁,同一筆異常已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。一律 upsert 附加,禁止整頁覆蓋,也不得改動別人的列。
|
> 寫入語意:一個區塊代表一次 hook 異常回報。H2 標題就是那一筆的異常頁頁名 `ERROR_{HASH}`,欄位是標題底下的一層條列,一個欄位一條。寫入前先讀回整頁,同一筆異常已經有區塊就整塊換掉,沒有才在文末附加一個新區塊,最後整頁寫回。一律 upsert 附加,禁止整頁覆蓋,也不得改動別人的區塊。
|
||||||
>
|
>
|
||||||
> 連結寫法:一律寫成 `[{文字}]({絕對網址})`,網址取 `jsc-gitea/tools/gitea.sh wiki-url` 印出的那一個,不自己組路徑。wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常文字或死連結。
|
> 連結寫法:一律寫成 `[{文字}]({絕對網址})`,網址取 `jsc-gitea/tools/gitea.sh wiki-url` 印出的那一個,不自己組路徑。wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常文字或死連結。
|
||||||
>
|
>
|
||||||
> 寫入前驗證:這一列要放進去的連結,先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才把連結寫進那一格。驗不過就只留純文字頁名,那一列照寫,異常紀錄不因為一條連結整份丟掉。驗證走 API,不看網頁狀態碼——私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把還在的頁判成死連結。結束碼 7 是金鑰失效,不算死連結,也不改寫任何既有列。
|
> 寫入前驗證:要放進條列的連結,先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才把連結寫進那一條。驗不過就只留純文字頁名,那一條照寫,異常紀錄不因為一條連結整份丟掉。驗證走 API,不看網頁狀態碼——私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把還在的頁判成死連結。結束碼 7 是金鑰失效,不算死連結,也不改寫任何既有區塊。
|
||||||
|
|
||||||
## 異常清單
|
## ERROR_{HASH}
|
||||||
|
|
||||||
| 時間 | 頁名 | 存取庫名稱 | 觸發 hook | 退出碼 | 摘要 |
|
- 時間:{yyyy-MM-dd HH:mm:ss}
|
||||||
| --- | --- | --- | --- | --- | --- |
|
- 頁名:[{頁名}]({wiki-url 印出的絕對網址})
|
||||||
| {yyyy-MM-dd HH:mm:ss} | [{頁名}]({wiki-url 印出的絕對網址}) | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
- 存取庫名稱:{owner}/{repo}
|
||||||
|
- 觸發 hook:{hook_name}
|
||||||
|
- 退出碼:{exit_code}
|
||||||
|
- 摘要:{error_summary}
|
||||||
|
|||||||
+62
-60
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
|
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
|
||||||
# 並在 ERROR_CONTENTS 附上一列索引。頁面內容套用 templates/ 的兩份範本,
|
# 並在異常目錄頁附上一個索引區塊。頁面內容套用 templates/ 的兩份範本,
|
||||||
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
|
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
|
||||||
# Gitea 寫入確認。
|
# Gitea 寫入確認。
|
||||||
|
# 目錄頁的讀回、比對與整頁寫回一律交給 jsc-gitea 的 tools/wiki-contents.sh,本腳本只組出
|
||||||
|
# 自己那一個區塊。目錄頁版面只留一份正本,十幾個目錄頁才不會各長一種樣子。
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
|
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
|
||||||
@@ -20,23 +22,24 @@
|
|||||||
# 3. 算不出 HASH(hash-id 失敗或回空字串)
|
# 3. 算不出 HASH(hash-id 失敗或回空字串)
|
||||||
# 4. 建不出暫存檔(mktemp 失敗)
|
# 4. 建不出暫存檔(mktemp 失敗)
|
||||||
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
|
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
|
||||||
# 異常頁、只解不出目錄頁的存取庫,就只寫異常頁、跳過目錄頁更新,印出頁名,仍然 exit 0。
|
# 異常頁、只解不出目錄頁的存取庫(wiki-contents.sh 回 3),就只寫異常頁、跳過目錄頁
|
||||||
|
# 更新,印出頁名,仍然 exit 0。找不到 wiki-contents.sh 也走同一條降級路。
|
||||||
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
||||||
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
||||||
#
|
#
|
||||||
# 連結:
|
# 連結:
|
||||||
# 目錄頁那一列指向異常頁,一律寫成 [{文字}]({連結}),網址取 jsc-gitea 的
|
# 目錄頁那個區塊裡的「頁名」那一條指向異常頁,一律寫成 [{文字}]({連結}),網址取
|
||||||
# gitea.sh wiki-url,不自己組路徑。
|
# jsc-gitea 的 gitea.sh wiki-url,不自己組路徑。
|
||||||
# 寫入前先把那個網址交給 jsc-gitea 的 link-check.sh,結束碼 0 才把連結寫進那一列。
|
# 寫入前先把那個網址交給 jsc-gitea 的 link-check.sh,結束碼 0 才把連結寫進那一條。
|
||||||
# 驗不過就只留純文字頁名:那一列照寫、異常頁照寫、結束碼照舊。這一段一律不改結束碼,
|
# 驗不過就只留純文字頁名:那一條照寫、異常頁照寫、結束碼照舊。這一段一律不改結束碼,
|
||||||
# 本腳本是失敗回報路徑,回報失敗不該再變成一次失敗。
|
# 本腳本是失敗回報路徑,回報失敗不該再變成一次失敗。
|
||||||
#
|
#
|
||||||
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
|
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
|
||||||
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
|
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
|
||||||
# 2=用法錯誤(缺 --hook 或 --summary)
|
# 2=用法錯誤(缺 --hook 或 --summary)
|
||||||
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
|
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
|
||||||
# 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。
|
# 讀不回來(wiki-contents.sh 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;
|
||||||
# 讀不回來就不寫,是為了不拿範本蓋掉一份還在的目錄頁
|
# 訊息走 stderr。讀不回來就不寫,是為了不拿範本蓋掉一份還在的目錄頁
|
||||||
# 註: 本檔以 `. "$ROOT/hooks/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會
|
# 註: 本檔以 `. "$ROOT/hooks/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會
|
||||||
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
|
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
|
||||||
#
|
#
|
||||||
@@ -83,9 +86,8 @@ 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。
|
# 目錄頁的存取庫不在這裡解:讀回、比對、整頁寫回都由 wiki-contents.sh 做,它自己解目錄專用
|
||||||
# 一份寫得成的異常紀錄,不該因為索引沒地方放就整份丟掉。
|
# 存取庫,這邊再解一次就會有兩份規則。解不出來時它回 3,本腳本照原本的語意降級。
|
||||||
crepo=$(sh "$gsh" wiki-repo CONTENTS 2>/dev/null || true)
|
|
||||||
|
|
||||||
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
||||||
if [ -z "$repo" ]; then
|
if [ -z "$repo" ]; then
|
||||||
@@ -108,9 +110,10 @@ ts=$(date +'%Y-%m-%d %H:%M:%S')
|
|||||||
ticket_ts=$(date +'%Y%m%d_%H%M%S')
|
ticket_ts=$(date +'%Y%m%d_%H%M%S')
|
||||||
if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi
|
if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi
|
||||||
[ -n "$detail" ] || detail="(無)"
|
[ -n "$detail" ] || detail="(無)"
|
||||||
# 摘要與相關輸出都落在 markdown 表格欄位裡,半形 | 會把欄位切斷,改成全形
|
# 相關輸出落在異常頁的表格欄位裡,半形 | 會把欄位切斷,改成全形
|
||||||
detail=$(printf '%s' "$detail" | sed 's/|/|/g')
|
detail=$(printf '%s' "$detail" | sed 's/|/|/g')
|
||||||
summary=$(printf '%s' "$summary" | sed 's/|/|/g')
|
# 摘要在目錄頁是一整條條列,換行會把一條拆成兩行,先併成一行
|
||||||
|
summary=$(printf '%s' "$summary" | tr '\n' ' ')
|
||||||
|
|
||||||
hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0
|
hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0
|
||||||
[ -n "$hash" ] || exit 0
|
[ -n "$hash" ] || exit 0
|
||||||
@@ -121,8 +124,8 @@ esc() { printf '%s' "$1" | tr '\n' ' ' | sed 's/[\\&|]/\\&/g'; }
|
|||||||
fill() { sed "s|$1|$(esc "$2")|g"; }
|
fill() { sed "s|$1|$(esc "$2")|g"; }
|
||||||
|
|
||||||
tmp_page=$(mktemp) || exit 0
|
tmp_page=$(mktemp) || exit 0
|
||||||
tmp_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
|
tmp_entry=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
|
||||||
trap 'rm -f "$tmp_page" "$tmp_list"' EXIT
|
trap 'rm -f "$tmp_page" "$tmp_entry"' EXIT
|
||||||
|
|
||||||
fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
|
fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
|
||||||
| fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \
|
| fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \
|
||||||
@@ -140,16 +143,17 @@ fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
|
|||||||
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
|
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
|
||||||
|
|
||||||
# 異常頁先寫,網址後取。頁名的 hash 帶時間戳,每次回報都是一個全新的頁,寫進去之前查網址
|
# 異常頁先寫,網址後取。頁名的 hash 帶時間戳,每次回報都是一個全新的頁,寫進去之前查網址
|
||||||
# 一定是 404,拿到的必然是空字串;目錄頁那一列會變成沒有連結的死字,stdout 也少一半。
|
# 一定是 404,拿到的必然是空字串;目錄頁那一條會變成沒有連結的死字,stdout 也少一半。
|
||||||
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
|
||||||
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 目錄頁那一列指向異常頁,連結一律寫成 [{文字}]({連結}),網址取 gitea.sh wiki-url。
|
# 目錄頁那個區塊的「頁名」那一條指向異常頁,連結一律寫成 [{文字}]({連結}),網址取
|
||||||
|
# gitea.sh wiki-url。
|
||||||
# wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常
|
# wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常
|
||||||
# 文字或死連結,巡不到也修不了。
|
# 文字或死連結,巡不到也修不了。
|
||||||
# 網址取不到不算失敗:異常頁已經寫成功了,只是這一列少一條連結。這裡把原因記下來走 stderr,
|
# 網址取不到不算失敗:異常頁已經寫成功了,只是這一條少一個連結。這裡把原因記下來走 stderr,
|
||||||
# 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。
|
# 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。
|
||||||
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null)
|
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null)
|
||||||
url_code=$?
|
url_code=$?
|
||||||
@@ -162,76 +166,74 @@ if [ "$url_code" -ne 0 ]; then
|
|||||||
7) url_note='金鑰失效或權限不足(wiki-url 回 7)' ;;
|
7) url_note='金鑰失效或權限不足(wiki-url 回 7)' ;;
|
||||||
*) url_note="wiki-url 結束碼 $url_code" ;;
|
*) url_note="wiki-url 結束碼 $url_code" ;;
|
||||||
esac
|
esac
|
||||||
echo "[jsc] 取不到 $page 的網址:$url_note。目錄頁那一列與輸出只留頁名。" >&2
|
echo "[jsc] 取不到 $page 的網址:$url_note。目錄頁那一條與輸出只留頁名。" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只印頁名,不印一個空欄位
|
emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只印頁名,不印一個空欄位
|
||||||
if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi
|
if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 連結先驗證連得到,才寫進目錄頁那一列。沒驗過的連結寫進去,異常頁一樣會在目錄頁長出
|
# 連結先驗證連得到,才寫進目錄頁那一條。沒驗過的連結寫進去,異常頁一樣會在目錄頁長出
|
||||||
# 死連結,而目錄頁是別人查問題的入口。驗不過就只留純文字頁名,那一列照寫。
|
# 死連結,而目錄頁是別人查問題的入口。驗不過就只留純文字頁名,那一條照寫。
|
||||||
link_cell="$page"
|
link_text="$page"
|
||||||
if [ -n "$url" ]; then
|
if [ -n "$url" ]; then
|
||||||
# link-check.sh 與 gitea.sh 同一個 tools 目錄,路徑直接由已經解出來的那一支推得,
|
# link-check.sh 與 gitea.sh 同一個 tools 目錄,路徑直接由已經解出來的那一支推得,
|
||||||
# 不另寫一套搜尋,兩邊才不會一支解到開發版面、一支解到安裝版面。
|
# 不另寫一套搜尋,兩邊才不會一支解到開發版面、一支解到安裝版面。
|
||||||
lcs="$(dirname "$gsh")/link-check.sh"
|
lcs="$(dirname "$gsh")/link-check.sh"
|
||||||
if [ ! -f "$lcs" ]; then
|
if [ ! -f "$lcs" ]; then
|
||||||
echo "[jsc] 找不到 $lcs,這一列的連結沒驗過,只留頁名;$page 已建立。" >&2
|
echo "[jsc] 找不到 $lcs,這一條的連結沒驗過,只留頁名;$page 已建立。" >&2
|
||||||
else
|
else
|
||||||
sh "$lcs" "$url" >/dev/null 2>&1
|
sh "$lcs" "$url" >/dev/null 2>&1
|
||||||
lc_code=$?
|
lc_code=$?
|
||||||
case "$lc_code" in
|
case "$lc_code" in
|
||||||
0) link_cell=$(printf '[%s](%s)' "$page" "$url") ;;
|
0) link_text=$(printf '[%s](%s)' "$page" "$url") ;;
|
||||||
# 7 是金鑰失效,不是死連結。金鑰過期時私有存取庫的回應與「頁不存在」分不出來,
|
# 7 是金鑰失效,不是死連結。金鑰過期時私有存取庫的回應與「頁不存在」分不出來,
|
||||||
# 把它當成死連結就會連還在的頁一起判死。
|
# 把它當成死連結就會連還在的頁一起判死。
|
||||||
7) echo "[jsc] 連結驗證遇上金鑰失效(link-check.sh 回 7),不判成死連結,這一列只留頁名;$page 已建立。" >&2 ;;
|
7) echo "[jsc] 連結驗證遇上金鑰失效(link-check.sh 回 7),不判成死連結,這一條只留頁名;$page 已建立。" >&2 ;;
|
||||||
3) echo "[jsc] GITEA_HOST 未設定(link-check.sh 回 3),連結沒驗過,這一列只留頁名;$page 已建立。" >&2 ;;
|
3) echo "[jsc] GITEA_HOST 未設定(link-check.sh 回 3),連結沒驗過,這一條只留頁名;$page 已建立。" >&2 ;;
|
||||||
*) echo "[jsc] 連結驗不過(link-check.sh 結束碼 $lc_code),這一列只留頁名;$page 已建立。" >&2 ;;
|
*) echo "[jsc] 連結驗不過(link-check.sh 結束碼 $lc_code),這一條只留頁名;$page 已建立。" >&2 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
row=$(printf '| %s | %s | %s | %s | %s | %s |' \
|
# 目錄頁上這一筆是一個 H2 區塊:標題就是異常頁頁名,欄位一行一條,順序與範本的示範區塊
|
||||||
"$ts" "$link_cell" "$repo" "$hook" "$code" "$summary")
|
# 一致。標題不放連結也不放網址——頁名只由存取庫名稱、hook 與時間決定,換主機或改存取庫
|
||||||
|
# 都動不到它,比對鍵才找得到既有那一筆。
|
||||||
|
{
|
||||||
|
printf '## %s\n\n' "$page"
|
||||||
|
printf -- '- 時間:%s\n' "$ts"
|
||||||
|
printf -- '- 頁名:%s\n' "$link_text"
|
||||||
|
printf -- '- 存取庫名稱:%s\n' "$repo"
|
||||||
|
printf -- '- 觸發 hook:%s\n' "$hook"
|
||||||
|
printf -- '- 退出碼:%s\n' "$code"
|
||||||
|
printf -- '- 摘要:%s\n' "$summary"
|
||||||
|
} > "$tmp_entry"
|
||||||
|
|
||||||
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
# 目錄頁交給 wiki-contents.sh:它負責解目錄專用存取庫、讀回舊頁、比對 H2 標題找既有區塊、
|
||||||
# 範本的示範列整列換成本次這一列,不逐格填。連結那一格已經驗過也組好了,拆成頁名與
|
# 附加或整塊換掉,再整頁寫回,還會把舊的表格頁轉成條列。頁上每一個區塊都是別人回報的異常,
|
||||||
# 網址兩個佔位再填,會在驗不過的時候留下一個空網址的死連結。
|
# 那套「讀得回舊內容才寫」的判斷只留一份正本,才不會每個目錄頁各寫一套、錯一次少一筆紀錄。
|
||||||
ROW="$row" awk '
|
# 傳進去的那個 2 是 key-col:舊表格版目錄頁裡持有身分的欄位序號。舊版第 2 欄是頁名,
|
||||||
index($0, "| {yyyy-MM-dd HH:mm:ss} |") == 1 { print ENVIRON["ROW"]; next }
|
# 舊頁自動轉條列時要靠它取 H2 標題;頁面已經是條列格式時這個參數用不到。
|
||||||
{ print }
|
# 路徑由已經解出來的 gitea.sh 推得,與 link-check.sh 同一套做法,三支同一個 tools 目錄。
|
||||||
' "$ROOT/templates/error-contents.md" > "$tmp_list"
|
wcs="$(dirname "$gsh")/wiki-contents.sh"
|
||||||
}
|
if [ ! -f "$wcs" ]; then
|
||||||
|
echo "[jsc] 找不到 $wcs,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2
|
||||||
if [ -z "$crepo" ]; then
|
|
||||||
echo "[jsc] 目錄頁的 wiki 存取庫解不出來,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2
|
|
||||||
emit
|
emit
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過
|
sh "$wcs" upsert ERROR 2 "$page" "$tmp_entry" "$ROOT/templates/error-contents.md" >/dev/null
|
||||||
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
|
wc_code=$?
|
||||||
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
|
case "$wc_code" in
|
||||||
# 0 → 讀到既有內容,附加新列(讀得到但整頁是空的,沒有既有列會被蓋掉,套範本才安全)
|
0) ;;
|
||||||
# 4 → 頁面真的還不存在,只有這個碼可以用範本建立新頁
|
# 3 是目錄頁的存取庫沒設定。異常頁已經寫成功,一份寫得成的異常紀錄不該因為索引沒地方放
|
||||||
# 7 → 金鑰失效或權限不足,舊內容未知,放棄目錄頁寫入
|
# 就整份丟掉,所以只跳過目錄頁,結束碼照舊回 0。
|
||||||
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
|
3) echo "[jsc] 目錄頁的 wiki 存取庫解不出來(wiki-contents.sh 回 3),只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2 ;;
|
||||||
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
|
# 其餘結束碼都代表這一筆沒進到目錄頁:7 金鑰失效、8 其他 API 失敗都是舊內容未知而中止,
|
||||||
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
|
# 1 是組不出頁面內容或寫入失敗,2 是用法錯誤,4 是頁面不存在又沒收到範本。
|
||||||
sh "$gsh" wiki-get "$crepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null
|
|
||||||
get_code=$?
|
|
||||||
case "$get_code" in
|
|
||||||
0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;;
|
|
||||||
4) build_contents ;;
|
|
||||||
*)
|
*)
|
||||||
echo "[jsc] 讀取目錄頁失敗($crepo,wiki-get 結束碼 $get_code),舊內容未知,不寫目錄頁;$page 已建立。" >&2
|
echo "[jsc] 目錄頁寫入失敗(wiki-contents.sh 結束碼 $wc_code),$page 已建立。" >&2
|
||||||
exit 4 ;;
|
exit 4 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if ! sh "$gsh" wiki-put "$crepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
|
|
||||||
echo "[jsc] 寫入目錄頁失敗($crepo),$page 已建立。" >&2
|
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
|
|
||||||
emit
|
emit
|
||||||
|
|||||||
+42
-4
@@ -4,7 +4,9 @@
|
|||||||
# wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線
|
# wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線
|
||||||
# wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook
|
# wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook
|
||||||
# wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪九支 hook,驗執行期
|
# wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪九支 hook,驗執行期
|
||||||
# wire-cli.sh status {claude|codex|copilot|antigravity|kiro} 唯讀盤點接線現況,不寫檔也不執行 hook
|
# wire-cli.sh status {claude|codex|copilot|antigravity|kiro} [--verdict]
|
||||||
|
# 唯讀盤點接線現況,不寫檔也不執行 hook。
|
||||||
|
# --verdict 只換結束碼語意,輸出一字不變
|
||||||
#
|
#
|
||||||
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
|
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
|
||||||
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
|
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
|
||||||
@@ -128,6 +130,9 @@
|
|||||||
# 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed
|
# 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed
|
||||||
# 結束碼(smoke): 0=ok 2=用法錯誤 4=failed(含結果行數與預期不符)
|
# 結束碼(smoke): 0=ok 2=用法錯誤 4=failed(含結果行數與預期不符)
|
||||||
# 結束碼(status): 0=wired 1=degraded 2=用法錯誤 3=skipped 5=unwired(該接的段落缺了至少一項)
|
# 結束碼(status): 0=wired 1=degraded 2=用法錯誤 3=skipped 5=unwired(該接的段落缺了至少一項)
|
||||||
|
# 結束碼(status --verdict): 0=該接的都接了(含 degraded——先天限制不算缺漏)
|
||||||
|
# 2=用法錯誤 3=skipped 5=unwired
|
||||||
|
# 給拿結束碼判成敗的呼叫端用,例如助理的內建檢查項。理由見下方 --verdict 那一段。
|
||||||
# 結束碼(唯讀模式): 6=readonly(JSC_READONLY=1 之下拒絕 purge 與接線),status 與 smoke 不受影響
|
# 結束碼(唯讀模式): 6=readonly(JSC_READONLY=1 之下拒絕 purge 與接線),status 與 smoke 不受影響
|
||||||
# status 之外的動作都會寫檔,體檢類技能(/jsc-cli:doctor)只能呼叫 status。判讀邏輯跟接線
|
# status 之外的動作都會寫檔,體檢類技能(/jsc-cli:doctor)只能呼叫 status。判讀邏輯跟接線
|
||||||
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
|
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
|
||||||
@@ -136,8 +141,14 @@
|
|||||||
# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪
|
# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪
|
||||||
# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。
|
# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。
|
||||||
set -u
|
set -u
|
||||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
# 這兩行一定要實體解析(`cd -P` 加 `pwd -P`),不能拿邏輯路徑。
|
||||||
ROOT=$(cd "$HERE/.." && pwd)
|
# ROOT 會被 ensure_stable_root() 當成 `ln -sfn "$ROOT" "$_link"` 的目標,而這支腳本本身
|
||||||
|
# 常常就是經由那條連結被叫起來的。邏輯解析會把連結原樣留在路徑裡,於是 ROOT 等於連結
|
||||||
|
# 自己,連結被改成指向自己,之後每一支 hook 的接線路徑都解不開,全機器 hook 一起失效。
|
||||||
|
# 這件事實際發生過。實體解析永遠退到連結指向的那個實際目錄,這個失敗模式就不可能成立。
|
||||||
|
# 事後才用 `[ -f ]` 檢查連結通不通不夠:那要等連結已經被寫壞才攔得到。
|
||||||
|
HERE=$(CDPATH= cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||||
|
ROOT=$(CDPATH= cd -P -- "$HERE/.." && pwd -P)
|
||||||
HOOKS="$ROOT/hooks"
|
HOOKS="$ROOT/hooks"
|
||||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||||
WIRE_ROOT="$ROOT"
|
WIRE_ROOT="$ROOT"
|
||||||
@@ -163,6 +174,30 @@ case "$cli" in
|
|||||||
claude|codex|copilot|antigravity|kiro) ;;
|
claude|codex|copilot|antigravity|kiro) ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
|
shift 2>/dev/null || true
|
||||||
|
|
||||||
|
# --verdict:輸出一字不變,只有結束碼換一套語意——該接的都接了就回 0,先天限制不算。
|
||||||
|
#
|
||||||
|
# 為什麼要有這個旗標。status 的結束碼帶的是狀態:0 是接好、1 是接好但這支 CLI 做不到、
|
||||||
|
# 5 是有東西沒接。那是給人看的三分法,也是對的。問題出在被當成檢查用:助理的內建檢查項
|
||||||
|
# 照結束碼判成敗,非零就是那一筆失敗、失敗次數加一。
|
||||||
|
# 於是有先天限制的那一支 CLI 每一輪都讓那一筆失敗一次,一天 96 次,而沒有人修得動——
|
||||||
|
# 那支 CLI 擋不下技能叫用是它的架構,不是接線缺漏,16 個接線項目全部就位。
|
||||||
|
# 那個計數存在的理由是指出「有一筆壞掉的項目每輪重試而沒人知道」,被這樣填滿就等於用
|
||||||
|
# 一個修不動的數字把真的壞掉蓋掉。
|
||||||
|
# 修在這裡而不是修在讀的那一邊:狀態與成敗是兩種語意,混在同一個通道上才是根因。這個
|
||||||
|
# 旗標把成敗那一種單獨拉出來,`status` 保持原樣給人看。
|
||||||
|
VERDICT_ONLY=0
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--verdict) VERDICT_ONLY=1; shift ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "$action:$VERDICT_ONLY" in
|
||||||
|
status:1|*:0) ;;
|
||||||
|
*) printf '[jsc] --verdict 只有 status 用得到,%s 不收這個旗標。\n' "$action" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# 唯讀契約在程式層把關,不靠呼叫端記得只打 status。子命令解析完就判:預設動作是接線,
|
# 唯讀契約在程式層把關,不靠呼叫端記得只打 status。子命令解析完就判:預設動作是接線,
|
||||||
# 所以少打一個子命令就會直接改環境,這個判定要擋的正是那一次手滑。
|
# 所以少打一個子命令就會直接改環境,這個判定要擋的正是那一次手滑。
|
||||||
@@ -2513,7 +2548,10 @@ if [ "$action" = status ]; then
|
|||||||
fi
|
fi
|
||||||
if [ -n "$st_degrade" ]; then
|
if [ -n "$st_degrade" ]; then
|
||||||
printf 'status=degraded reason=%s\n' "$st_degrade"
|
printf 'status=degraded reason=%s\n' "$st_degrade"
|
||||||
cat "$st_items"; rm -f "$st_items"; exit 1
|
cat "$st_items"; rm -f "$st_items"
|
||||||
|
# --verdict 之下先天限制不算失敗:輸出照印,讓人看得到,但結束碼說「該接的都接了」。
|
||||||
|
[ "$VERDICT_ONLY" -eq 1 ] && exit 0
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf 'status=wired reason=%s\n' "$st_wired"
|
printf 'status=wired reason=%s\n' "$st_wired"
|
||||||
cat "$st_items"; rm -f "$st_items"; exit 0
|
cat "$st_items"; rm -f "$st_items"; exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user