Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2349db5a1c | ||
|
|
9568b9c787 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.5.2",
|
"version": "0.4.2",
|
||||||
"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.5.2",
|
"version": "0.4.2",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) |
|
| `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) |
|
||||||
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間。`start` 與 `restart` 判定為新工作階段時,另外呼叫 `restart-gate.sh clear` 放下部署後的重啟閘門——新工作階段代表 CLI 行程是新起的,新版一定已經載入。清除的範圍只有跑到這支腳本的那一支 CLI 自己那一份狀態檔,別支沒重啟就繼續被擋 |
|
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間。`start` 與 `restart` 判定為新工作階段時,另外呼叫 `restart-gate.sh clear` 放下部署後的重啟閘門——新工作階段代表 CLI 行程是新起的,新版一定已經載入。清除的範圍只有跑到這支腳本的那一支 CLI 自己那一份狀態檔,別支沒重啟就繼續被擋 |
|
||||||
| `hooks/session-reminder.sh` | SessionStart | 把助理算好的未讀提醒帶到前景。只讀 `$JSC_HOME/assistant/reminders.tsv`(`jsc-assist` 的巡檢每一輪重寫),逾期的排前面、使用者自己登錄的到期提醒在後,最多列 8 筆;委派清單種入的內建項只印一行總數(那幾筆等的是接線不是人,每一輪都到期、每一輪都一樣,逐筆吐出來就是噪音),另加一行「有幾筆待辦連續失敗」。**這一支一個判定都不做**:自己拿 `due` 欄與 `next_run` 去跟現在比就是第二套到期判定,跟助理那一套遲早對不上。一個工作階段只提一次,記號是 `$JSC_HOME/sessions/{代號}.reminded`,接不到 session id 的 CLI 由 `session-timer.sh restart` 清掉那個記號。佇列檔頭帶那一輪的時間戳與 epoch,超過心跳門檻或心跳不新鮮就明說「這批提醒是多久以前算的、助理現在的心跳是什麼狀態」——一份沒有人更新的佇列讀起來跟新的一模一樣,而「沒有提醒」與「沒有人算提醒」不可以長得一樣。助理狀態目錄不存在時一個字都不印:那台機器從沒啟動過助理,每個工作階段催一次不是提醒是噪音。子指令 `peek` 只印不記號,給人重看與檢核用。永遠 exit 0 |
|
|
||||||
| `hooks/skill-name.sh` | 不直接接線,由 `version-guard.sh` 與 `restart-gate.sh` 呼叫 | 從各 CLI 的 hook 負載解析出這一次要用哪一支 jsc 技能,一支 CLI 一個子命令,印一行「{domain}<TAB>{技能名}」,解析不出來就印空字串。取值來源:claude 讀 stdin JSON 的 `skill` 欄位、codex 讀 `tool_input.command` 裡那條 `SKILL.md` 路徑(Codex 沒有 Skill 工具,技能是模型自己用 Bash 讀 `SKILL.md` 載入的)、copilot 讀 `toolArgs`(字串化的 JSON,要先剝一層跳脫)、antigravity 讀 `toolCall.args.AbsolutePath` 另收提示字串(斜線指令不產生工具呼叫)、kiro 讀 `prompt` 開頭那個斜線指令;五支都先看環境變數 `JSC_SKILL`、`SKILL`。永遠 exit 0:閘門那一端一律 fail-open,而且 copilot 的 command hook 是 fail-closed 的,回非零等於拒絕。規則只有這一份,兩支閘門都不重寫第二套 |
|
| `hooks/skill-name.sh` | 不直接接線,由 `version-guard.sh` 與 `restart-gate.sh` 呼叫 | 從各 CLI 的 hook 負載解析出這一次要用哪一支 jsc 技能,一支 CLI 一個子命令,印一行「{domain}<TAB>{技能名}」,解析不出來就印空字串。取值來源:claude 讀 stdin JSON 的 `skill` 欄位、codex 讀 `tool_input.command` 裡那條 `SKILL.md` 路徑(Codex 沒有 Skill 工具,技能是模型自己用 Bash 讀 `SKILL.md` 載入的)、copilot 讀 `toolArgs`(字串化的 JSON,要先剝一層跳脫)、antigravity 讀 `toolCall.args.AbsolutePath` 另收提示字串(斜線指令不產生工具呼叫)、kiro 讀 `prompt` 開頭那個斜線指令;五支都先看環境變數 `JSC_SKILL`、`SKILL`。永遠 exit 0:閘門那一端一律 fail-open,而且 copilot 的 command hook 是 fail-closed 的,回非零等於拒絕。規則只有這一份,兩支閘門都不重寫第二套 |
|
||||||
| `hooks/deny.sh` | 不直接接線,由 `version-guard.sh` 與 `restart-gate.sh` 呼叫 | 產出各 CLI 認得的阻擋輸出,訊息從參數或標準輸入進。claude、codex、copilot 訊息寫 stderr 並回 exit 2;antigravity 印 stdout 的 `{"decision":"deny","reason":"..."}` 並固定回 0——那支 CLI 的結束碼語意兩邊文件都沒寫,靠結束碼會變成「判定擋下、CLI 照樣放行」的無聲失效,所以 stdout 只准有那一行;kiro 擋不下技能叫用,改印警告後回 0;認不得的代號走 stderr 加 2 這個保守預設 |
|
| `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` 的檔頭,那裡一項一個理由 |
|
||||||
@@ -37,7 +36,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從可驗證來源讀出模型 id,比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖。**來源依 CLI 分流**,一支 CLI 只讀自己的紀錄:claude 讀 transcript 與 hook stdin JSON,codex 讀 hook stdin JSON 與自己的 session 記錄,copilot、antigravity、kiro 本機沒有可讀的模型紀錄,判不出 CLI 時不採用任何自動來源;所有 CLI 最後都接受 `JSC_MODEL` 人工覆寫,且回報會標明人工覆寫。政策是 **fail-closed**:不知道能力就擋下,三種情形一律擋——判不出 CLI、判不出模型、模型不在能力標籤表上;每一則擋下的訊息都會印出兩條逃生門(設 `JSC_MODEL`,或執行 `sdlc-gate.sh unlock {狀態檔}`)。`check` 在上述任一情形以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`report` 印出階段、必要標籤、模型 id、模型來源與判定結果;`unlock` 為逃生門,不帶參數清這個工作階段的新舊兩份,帶參數只清指定的那一支。階段鎖狀態檔是 `$JSC_HOME/sessions/{CLI 代號}-{sid}.stage`,舊路徑 `$JSC_HOME/sessions/{sid}.stage` 仍讀得到。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index} [{工作包代號}]` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo} {index}` 結清那一筆(檔案不存在也算成功)、`wp-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁頁名}]` 記下這個存取庫目前領取哪一包、`wp-unclaim {owner}/{repo}` 交回、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔一個工作包一支,在 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋;一個工作包一支鎖檔是為了讓好幾個互不相依的工作包能同時記在案,不會互相覆蓋掉對方的鎖。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `analyze` 與 `maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程),也放行 `plan`,只注入提醒(plan 是純邏輯階段、不碰程式碼,而這道閘門只知道「有 PR 未合併」、判不出跟新計畫有沒有關聯;放棄的是在製品上限,`analyze` 與 `maintain` 兩道仍在,上限晚一個階段才生效)——這一層是整個存取庫共用的粗粒度提醒,「某個候選工作包能不能挑」的細粒度判斷在 `jsc-sdlc/tools/wp-gate.sh check-deps`,不是這裡。另外會比對歸屬:未結清的 PR 不屬於目前領取的工作包時,`prompt` 多注入一行「那幾支交給領取它的工作階段」,`skill` 在擋下 `analyze`、`maintain` 時一併點名,`plan` 與 `implement` 仍放行但收到同一則提醒。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 |
|
| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從可驗證來源讀出模型 id,比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖。**來源依 CLI 分流**,一支 CLI 只讀自己的紀錄:claude 讀 transcript 與 hook stdin JSON,codex 讀 hook stdin JSON 與自己的 session 記錄,copilot、antigravity、kiro 本機沒有可讀的模型紀錄,判不出 CLI 時不採用任何自動來源;所有 CLI 最後都接受 `JSC_MODEL` 人工覆寫,且回報會標明人工覆寫。政策是 **fail-closed**:不知道能力就擋下,三種情形一律擋——判不出 CLI、判不出模型、模型不在能力標籤表上;每一則擋下的訊息都會印出兩條逃生門(設 `JSC_MODEL`,或執行 `sdlc-gate.sh unlock {狀態檔}`)。`check` 在上述任一情形以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`report` 印出階段、必要標籤、模型 id、模型來源與判定結果;`unlock` 為逃生門,不帶參數清這個工作階段的新舊兩份,帶參數只清指定的那一支。階段鎖狀態檔是 `$JSC_HOME/sessions/{CLI 代號}-{sid}.stage`,舊路徑 `$JSC_HOME/sessions/{sid}.stage` 仍讀得到。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index} [{工作包代號}]` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo} {index}` 結清那一筆(檔案不存在也算成功)、`wp-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁頁名}]` 記下這個存取庫目前領取哪一包、`wp-unclaim {owner}/{repo}` 交回、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔一個工作包一支,在 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋;一個工作包一支鎖檔是為了讓好幾個互不相依的工作包能同時記在案,不會互相覆蓋掉對方的鎖。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `analyze` 與 `maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程),也放行 `plan`,只注入提醒(plan 是純邏輯階段、不碰程式碼,而這道閘門只知道「有 PR 未合併」、判不出跟新計畫有沒有關聯;放棄的是在製品上限,`analyze` 與 `maintain` 兩道仍在,上限晚一個階段才生效)——這一層是整個存取庫共用的粗粒度提醒,「某個候選工作包能不能挑」的細粒度判斷在 `jsc-sdlc/tools/wp-gate.sh check-deps`,不是這裡。另外會比對歸屬:未結清的 PR 不屬於目前領取的工作包時,`prompt` 多注入一行「那幾支交給領取它的工作階段」,`skill` 在擋下 `analyze`、`maintain` 時一併點名,`plan` 與 `implement` 仍放行但收到同一則提醒。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 |
|
||||||
| `hooks/write-guard.sh` | PreToolUse(Write、Edit、MultiEdit)、PreToolUse(Bash) | 寫入與提交閘門,共三種擋人模式,目前只接在 claude 上;codex、copilot、antigravity 三支已經有可用的 pre-tool hook(見上面「各 CLI 的 pre-tool 接線位置」),只是這三種模式還沒接過去,kiro 則是本來就擋不下來。另有一個不接 hook 的 `release` 解除模式。`stage`:`sdlc-gate.sh` 的階段鎖鎖在 `plan` 或 `analyze` 時,以 exit 2 擋下 `Write`、`Edit`、`MultiEdit`——那兩個階段的產出是計畫頁與分析頁,不是檔案。階段鎖狀態檔沿用 `sdlc-gate.sh` 那一份,這裡只讀不寫。`review`:目前技能是 `jsc-review:code-review` 或 `jsc-review:api-doc` 時擋下寫入,那兩支只回報發現、不改程式碼。技能名先讀環境變數,取不到才讀 `skill-usage.sh` 記下的那一份;沒有「技能結束」事件可讀,所以紀錄超過 `JSC_WRITE_GUARD_TTL` 秒就當那支技能早已跑完。`jsc-review:comment-cleanup` **刻意不擋**:它本來就要改檔,只是限定僅註解行,而精確判定要解析工具參數裡整份新內容再逐語言判斷哪幾行是註解,判錯會擋掉合法的清理,代價比漏擋大,所以那條界線留給技能內文與後續審查。`commit`:擋下「同一道指令把全部變更一次加進索引再提交」,也擋下含簡體字、亂碼或非 UTF-8 編碼的提交訊息(判定整段轉呼叫 `lang-guard.sh`,字表仍是 `hooks/simplified.txt`,這裡不留第二份樣式)。跨兩次工具呼叫的 `git add -A` 不擋:那要記跨呼叫狀態,而被擋下的人沒有辦法讓那個狀態自己消失,閘門會把解除自己的路徑一起鎖掉。`release`:刪掉 `review` 模式認人用的那份紀錄,一律 exit 0,由 `jsc-review:code-review` 與 `jsc-review:api-doc` 在收尾時各呼叫一次。有這個模式是因為那份紀錄記的是「最近一次載入的技能」不是「還在跑的技能」——稽核收尾後呼叫端本來就要動手改,那時紀錄仍寫著稽核技能,TTL 內每一次寫入都被擋,解除路徑只剩逃生門或空等;閘門不得把解除自己的路徑一起鎖掉。逃生門 `JSC_WRITE_GUARD=off`(`release` 不受它影響,清紀錄擋不到任何人) |
|
| `hooks/write-guard.sh` | PreToolUse(Write、Edit、MultiEdit)、PreToolUse(Bash) | 寫入與提交閘門,共三種擋人模式,目前只接在 claude 上;codex、copilot、antigravity 三支已經有可用的 pre-tool hook(見上面「各 CLI 的 pre-tool 接線位置」),只是這三種模式還沒接過去,kiro 則是本來就擋不下來。另有一個不接 hook 的 `release` 解除模式。`stage`:`sdlc-gate.sh` 的階段鎖鎖在 `plan` 或 `analyze` 時,以 exit 2 擋下 `Write`、`Edit`、`MultiEdit`——那兩個階段的產出是計畫頁與分析頁,不是檔案。階段鎖狀態檔沿用 `sdlc-gate.sh` 那一份,這裡只讀不寫。`review`:目前技能是 `jsc-review:code-review` 或 `jsc-review:api-doc` 時擋下寫入,那兩支只回報發現、不改程式碼。技能名先讀環境變數,取不到才讀 `skill-usage.sh` 記下的那一份;沒有「技能結束」事件可讀,所以紀錄超過 `JSC_WRITE_GUARD_TTL` 秒就當那支技能早已跑完。`jsc-review:comment-cleanup` **刻意不擋**:它本來就要改檔,只是限定僅註解行,而精確判定要解析工具參數裡整份新內容再逐語言判斷哪幾行是註解,判錯會擋掉合法的清理,代價比漏擋大,所以那條界線留給技能內文與後續審查。`commit`:擋下「同一道指令把全部變更一次加進索引再提交」,也擋下含簡體字、亂碼或非 UTF-8 編碼的提交訊息(判定整段轉呼叫 `lang-guard.sh`,字表仍是 `hooks/simplified.txt`,這裡不留第二份樣式)。跨兩次工具呼叫的 `git add -A` 不擋:那要記跨呼叫狀態,而被擋下的人沒有辦法讓那個狀態自己消失,閘門會把解除自己的路徑一起鎖掉。`release`:刪掉 `review` 模式認人用的那份紀錄,一律 exit 0,由 `jsc-review:code-review` 與 `jsc-review:api-doc` 在收尾時各呼叫一次。有這個模式是因為那份紀錄記的是「最近一次載入的技能」不是「還在跑的技能」——稽核收尾後呼叫端本來就要動手改,那時紀錄仍寫著稽核技能,TTL 內每一次寫入都被擋,解除路徑只剩逃生門或空等;閘門不得把解除自己的路徑一起鎖掉。逃生門 `JSC_WRITE_GUARD=off`(`release` 不受它影響,清紀錄擋不到任何人) |
|
||||||
|
|
||||||
Claude 由 `hooks/hooks.json` 自動接線十支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。寫進使用者設定的長期命令一律指向 `$JSC_HOME/current/jsc-hooks`,不指向帶版號的 plugin 快取目錄,也不指向開發存取庫。
|
Claude 由 `hooks/hooks.json` 自動接線九支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。寫進使用者設定的長期命令一律指向 `$JSC_HOME/current/jsc-hooks`,不指向帶版號的 plugin 快取目錄,也不指向開發存取庫。
|
||||||
|
|
||||||
### 各 CLI 的 pre-tool 接線位置
|
### 各 CLI 的 pre-tool 接線位置
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ Claude 由 `hooks/hooks.json` 自動接線十支 hook;其他 CLI 用 `hooks-in
|
|||||||
|
|
||||||
- **kiro** 擋不下技能叫用,這是 CLI 的限制,不是我們接錯。技能走 `ResolveSkill` 這個 agent 內部請求,不經工具管線,`preToolUse` 攔不到;`userPromptSubmit` 的非零結束碼也不會擋下那一輪。唯一可用的介入是 `userPromptSubmit` 的 stdout 注入,所以兩道閘門只印警告。hook 宣告只認 **agent 設定檔的 `hooks` 鍵**,`.kiro/hooks/` 目錄不在它的設定目錄常數裡,一份都不會被讀。同一份 agent 檔還要寫 `resources` 的**兩層** `skill://` glob(預設只掃一層,jsc 的技能在 `jsc-{domain}/{name}/SKILL.md` 第二層,少了那一條一支都載不到)與**明列的 `tools`**(自訂 agent 沒宣告時可用工具會受限),並把 `chat.defaultAgent` 設成 `jsc`,那個 agent 才會被選用。
|
- **kiro** 擋不下技能叫用,這是 CLI 的限制,不是我們接錯。技能走 `ResolveSkill` 這個 agent 內部請求,不經工具管線,`preToolUse` 攔不到;`userPromptSubmit` 的非零結束碼也不會擋下那一輪。唯一可用的介入是 `userPromptSubmit` 的 stdout 注入,所以兩道閘門只印警告。hook 宣告只認 **agent 設定檔的 `hooks` 鍵**,`.kiro/hooks/` 目錄不在它的設定目錄常數裡,一份都不會被讀。同一份 agent 檔還要寫 `resources` 的**兩層** `skill://` glob(預設只掃一層,jsc 的技能在 `jsc-{domain}/{name}/SKILL.md` 第二層,少了那一條一支都載不到)與**明列的 `tools`**(自訂 agent 沒宣告時可用工具會受限),並把 `chat.defaultAgent` 設成 `jsc`,那個 agent 才會被選用。
|
||||||
|
|
||||||
> 覆蓋範圍其餘部分仍要據實看待:只有 claude 同時有 PreToolUse、PostToolUse 與 UserPromptSubmit,十支 hook 全接得上。codex、copilot、antigravity 三支目前接上的是版本前置檢查與部署後重啟閘門兩道;`write-guard.sh` 的三種模式還沒接線,SDLC 模型鎖仍只剩技能步驟檢查,註解範圍與繁中編碼仍是 `sweep`。codex 另外沒有工作階段開始事件,計時改由 `tools/jsc-wrap.sh` 的 `codex` 別名在啟動當下開始;沒走別名啟動時,時間從第一輪回應算起。
|
> 覆蓋範圍其餘部分仍要據實看待:只有 claude 同時有 PreToolUse、PostToolUse 與 UserPromptSubmit,九支 hook 全接得上。codex、copilot、antigravity 三支目前接上的是版本前置檢查與部署後重啟閘門兩道;`write-guard.sh` 的三種模式還沒接線,SDLC 模型鎖仍只剩技能步驟檢查,註解範圍與繁中編碼仍是 `sweep`。codex 另外沒有工作階段開始事件,計時改由 `tools/jsc-wrap.sh` 的 `codex` 別名在啟動當下開始;沒走別名啟動時,時間從第一輪回應算起。
|
||||||
|
|
||||||
### 驗證等級
|
### 驗證等級
|
||||||
|
|
||||||
@@ -170,8 +169,8 @@ 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}`,並在異常目錄頁附上一個索引區塊。目錄頁一筆一個 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/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/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` 回報 |
|
||||||
|
|
||||||
@@ -183,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`,彙整所有異常頁,方便先看最新問題再往下追。版面是 H1 頁名加 `>` 引言,之後一筆一個 H2 區塊,標題就是異常頁頁名,欄位一行一條;範本只留一個示範區塊,用 `{佔位符}` 寫。落在目錄專用存取庫,一律 upsert 附加,連結一律寫成 `[{文字}]({連結})` 且先過 `link-check.sh` 驗過才寫。 |
|
| `templates/error-contents.md` | 異常目錄 `ERROR_CONTENTS`,彙整所有異常頁,方便先看最新問題再往下追。落在目錄專用存取庫,一律 upsert 附加,連結一律寫成 `[{文字}]({連結})` 且先過 `link-check.sh` 驗過才寫。 |
|
||||||
|
|
||||||
## Skills 目錄
|
## Skills 目錄
|
||||||
|
|
||||||
@@ -193,7 +192,7 @@ Claude 由 `hooks/hooks.json` 自動接線十支 hook;其他 CLI 用 `hooks-in
|
|||||||
|
|
||||||
### `hooks-install`
|
### `hooks-install`
|
||||||
|
|
||||||
把十支 hook 接線到所有已安裝的 CLI,每個 CLI 走五道關卡:先 `tools/wire-cli.sh purge {cli}` 備份後移除所有 hook(含非 jsc 的第三方項目,乾淨起跑才分得清後續失敗是誰的),再 `tools/wire-cli.sh {cli}` 接線(claude 由 `hooks.json` 自動接線,無需寫入;其他 CLI 的持久命令會寫成 `$JSC_HOME/current/jsc-hooks` 穩定路徑),接著 `tools/wire-cli.sh status {cli}` 唯讀盤點接線結果,再 `tools/wire-cli.sh smoke {cli}` 驗執行期,最後 `tools/scan-hook-errors.sh --cli {cli}` 掃原生紀錄。第一支 CLI 的管線單獨跑完(`$JSC_HOME/current/jsc-hooks` 連結由它統一更新),其餘各 CLI 的管線才並行。codex、copilot、antigravity 由接線腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍重寫到各自的規則檔(以 `<!-- jsc-hooks -->` 標記整段取代,等同先移除再重裝,不重複追加)。codex、copilot、antigravity、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入。版本前置檢查與部署後重啟閘門在 codex、copilot、antigravity 三支都擋得下來,各自接在自己的 pre-tool 位置(見上面「各 CLI 的 pre-tool 接線位置」),三支回報 `wired`;kiro 擋不下技能叫用,只注入警告,回報 `degraded`,那是 CLI 的限制。`write-guard.sh` 的三種模式目前仍只接在 claude。這四個 CLI 都沒有 post-tool hook,`comment-scope.sh` 接不到逐檔即時掃描,改用 `sweep` 掃整個 git 工作區——codex 每輪結束、kiro 每輪提示送出時、copilot 與 antigravity 只有工作階段結束時掃一次,腳本會在 `reason` 裡講明各自的時機,也只有 claude 掃得到執行期錯誤紀錄。antigravity 與 kiro 的 hook 觸發都沒有實跑驗證(前者 quota 用盡、後者未登入),回報時要把「接線已驗」與「觸發未驗」分開講。任一關卡出錯(purge、接線、冒煙失敗,或掃到 `jsc=true` 的執行期錯誤)就先寫 `ERROR_{HASH}`,再交給 `repair` 技能接手並以 `develop` PR 收尾;此時允許中止剩下的安裝,但修正一定要開始。掃到 `jsc=false` 的第三方 hook 錯誤只回報,不轉修正。
|
把九支 hook 接線到所有已安裝的 CLI,每個 CLI 走五道關卡:先 `tools/wire-cli.sh purge {cli}` 備份後移除所有 hook(含非 jsc 的第三方項目,乾淨起跑才分得清後續失敗是誰的),再 `tools/wire-cli.sh {cli}` 接線(claude 由 `hooks.json` 自動接線,無需寫入;其他 CLI 的持久命令會寫成 `$JSC_HOME/current/jsc-hooks` 穩定路徑),接著 `tools/wire-cli.sh status {cli}` 唯讀盤點接線結果,再 `tools/wire-cli.sh smoke {cli}` 驗執行期,最後 `tools/scan-hook-errors.sh --cli {cli}` 掃原生紀錄。第一支 CLI 的管線單獨跑完(`$JSC_HOME/current/jsc-hooks` 連結由它統一更新),其餘各 CLI 的管線才並行。codex、copilot、antigravity 由接線腳本裝上 `tools/jsc-wrap.sh` 包裝別名補上計時與用量回填(結束時自動跑 `tools/scan-logs.sh`),語言規則仍重寫到各自的規則檔(以 `<!-- jsc-hooks -->` 標記整段取代,等同先移除再重裝,不重複追加)。codex、copilot、antigravity、kiro 的 SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 SDLC 技能直接呼叫 `sdlc-gate.sh lock` 寫入。版本前置檢查與部署後重啟閘門在 codex、copilot、antigravity 三支都擋得下來,各自接在自己的 pre-tool 位置(見上面「各 CLI 的 pre-tool 接線位置」),三支回報 `wired`;kiro 擋不下技能叫用,只注入警告,回報 `degraded`,那是 CLI 的限制。`write-guard.sh` 的三種模式目前仍只接在 claude。這四個 CLI 都沒有 post-tool hook,`comment-scope.sh` 接不到逐檔即時掃描,改用 `sweep` 掃整個 git 工作區——codex 每輪結束、kiro 每輪提示送出時、copilot 與 antigravity 只有工作階段結束時掃一次,腳本會在 `reason` 裡講明各自的時機,也只有 claude 掃得到執行期錯誤紀錄。antigravity 與 kiro 的 hook 觸發都沒有實跑驗證(前者 quota 用盡、後者未登入),回報時要把「接線已驗」與「觸發未驗」分開講。任一關卡出錯(purge、接線、冒煙失敗,或掃到 `jsc=true` 的執行期錯誤)就先寫 `ERROR_{HASH}`,再交給 `repair` 技能接手並以 `develop` PR 收尾;此時允許中止剩下的安裝,但修正一定要開始。掃到 `jsc=false` 的第三方 hook 錯誤只回報,不轉修正。
|
||||||
|
|
||||||
### `repair`
|
### `repair`
|
||||||
|
|
||||||
@@ -207,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}`,由 `wiki-contents.sh` 解析。目錄頁與內容頁分屬兩個不同的存取庫,各解各的 | 退回 `JSC_WIKI_REPO`;還是解不出來時 `wiki-contents.sh` 回 3,只寫內容頁、跳過目錄頁更新,`tools/report-error.sh` 仍回 exit 0 |
|
| `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` |
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "sh -c 'JSC_CLI=codex; export JSC_CLI; root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-timer.sh\" start'"
|
"command": "sh -c 'JSC_CLI=codex; export JSC_CLI; root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-timer.sh\" start'"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "sh -c 'JSC_CLI=codex; export JSC_CLI; root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-reminder.sh\"'"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,14 +65,10 @@ scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 只留註解行:行首註解符號,或行中出現 // 與 # 的行尾註解。
|
# 只留註解行:行首註解符號,或行中出現 // 與 # 的行尾註解。
|
||||||
# `--` 後面一定要接空白:CSS 自訂屬性也是 `--` 開頭(`--ink: #121a1d;`),
|
comments=$(printf '%s\n' "$lines" | grep -E '^[[:space:]]*(//|#|--|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]' || true)
|
||||||
# 不要求空白就會把整份樣式表的變數宣告當成 SQL 註解送去比對,色碼再被咬成議題編號。
|
|
||||||
comments=$(printf '%s\n' "$lines" | grep -E '^[[:space:]]*(//|#|--[[:space:]]|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]' || true)
|
|
||||||
[ -n "$comments" ] || return 0
|
[ -n "$comments" ] || return 0
|
||||||
|
|
||||||
# 白名單先剪掉,再比對禁止樣式。剪掉而不是整行放行——同一行可能一半合規、一半違規。
|
# 白名單先剪掉,再比對禁止樣式。剪掉而不是整行放行——同一行可能一半合規、一半違規。
|
||||||
# 最後一條剪的是十六進位色碼:註解裡提到 `#e9eef0` 這種寫法,議題編號那一條會咬到開頭的
|
|
||||||
# 數字。只剪含十六進位字母的那些——議題編號是純十進位,所以剪不到真的編號。
|
|
||||||
cleaned=$(printf '%s\n' "$comments" | sed -E \
|
cleaned=$(printf '%s\n' "$comments" | sed -E \
|
||||||
-e 's#SPDX-License-Identifier:[^[:space:]]*##g' \
|
-e 's#SPDX-License-Identifier:[^[:space:]]*##g' \
|
||||||
-e 's#CVE-[0-9]{4}-[0-9]+##g' \
|
-e 's#CVE-[0-9]{4}-[0-9]+##g' \
|
||||||
@@ -80,8 +76,7 @@ scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,
|
|||||||
-e 's#@(deprecated|since|param|returns?|throws|type|typedef|example|see|link|inheritdoc|override|nullable|internal)##g' \
|
-e 's#@(deprecated|since|param|returns?|throws|type|typedef|example|see|link|inheritdoc|override|nullable|internal)##g' \
|
||||||
-e 's#https?://(github|gitlab|bitbucket)\.com/[^[:space:]]*##g' \
|
-e 's#https?://(github|gitlab|bitbucket)\.com/[^[:space:]]*##g' \
|
||||||
-e 's#https?://[^[:space:]]*[{<][^[:space:]]*##g' \
|
-e 's#https?://[^[:space:]]*[{<][^[:space:]]*##g' \
|
||||||
-e 's#[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2}##g' \
|
-e 's#[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2}##g')
|
||||||
-e 's/(^|[^0-9A-Za-z_])#[0-9a-fA-F]*[a-fA-F][0-9a-fA-F]*([^0-9A-Za-z_]|$)/\1\2/g')
|
|
||||||
|
|
||||||
out=$(
|
out=$(
|
||||||
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
|
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-timer.sh\" start'"
|
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-timer.sh\" start'"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/session-reminder.sh\"'"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-58
@@ -20,17 +20,6 @@ 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
|
||||||
@@ -240,61 +229,27 @@ 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() {
|
||||||
_c=""
|
|
||||||
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
|
|
||||||
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
||||||
_c="$JSC_GITEA_TOOLS/gitea.sh"
|
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
|
||||||
fi
|
fi
|
||||||
|
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
|
||||||
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
|
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
|
||||||
if [ -z "$_c" ]; then
|
for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
|
||||||
for _p in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
|
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||||
[ -f "$_p" ] && { _c="$_p"; break; }
|
done
|
||||||
done
|
|
||||||
fi
|
|
||||||
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
|
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
|
||||||
if [ -z "$_c" ]; then
|
_c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
|
||||||
_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,175 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# session-reminder.sh — 工作階段開始時,把助理算好的未讀提醒帶到前景。
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# session-reminder.sh # SessionStart:印出未讀提醒,並記下這個工作階段提過了
|
|
||||||
# session-reminder.sh peek # 只印,不記。人要重看一次時用,也給檢核用
|
|
||||||
#
|
|
||||||
# 結束碼:0=一律成功,只有這一種。這一支接在工作階段開始那個事件上,它的產出走 stdout,
|
|
||||||
# 不走結束碼——那個事件的 stdout 會成為額外 context。佇列不在、讀不到、格式對不上,
|
|
||||||
# 一律印一行說明然後 exit 0;一支在每個工作階段開頭都會跑的 hook 絕對不可以擋人。
|
|
||||||
# 唯一的非零來源是 `. lib.sh` 載入失敗,那時 sh 自己回 2。
|
|
||||||
#
|
|
||||||
# --- 這一支不做判定 ---
|
|
||||||
#
|
|
||||||
# 提醒該不該送、哪幾筆該送,全由助理那一輪算完寫進佇列(jsc-assist 的 tools/patrol.sh
|
|
||||||
# 每一輪重寫 $JSC_HOME/assistant/reminders.tsv)。這裡只把那份檔案印出來。
|
|
||||||
# 不自己判的理由有兩個。一是快:這一支跑在每一個工作階段的開頭,讀一個檔案就回來。
|
|
||||||
# 二是不漂移:自己拿 due 欄與 next_run 去跟現在比,就是第二套到期判定,跟助理那一套遲早
|
|
||||||
# 對不上,而對不上的那一天兩邊都說自己是對的。
|
|
||||||
#
|
|
||||||
# --- 「沒有提醒」與「沒有人算提醒」不可以長得一樣 ---
|
|
||||||
#
|
|
||||||
# 只印佇列換來一個新的失效模式:助理停了,佇列就不再更新,而一份舊佇列讀起來跟新的一模一樣。
|
|
||||||
# 所以佇列檔頭帶著那一輪的時間戳,這裡算出它多舊,超過心跳門檻就明說「這份提醒是多久以前
|
|
||||||
# 算的、助理現在的心跳是什麼狀態」。門檻與狀態都取 heartbeat.sh 印的那一行,不自己定一套。
|
|
||||||
#
|
|
||||||
# 助理的狀態目錄根本不存在時,這一支一個字都不印:那代表這台機器從沒啟動過助理,
|
|
||||||
# 每個工作階段開頭都催一次「你要不要啟動助理」不是提醒,是噪音。
|
|
||||||
#
|
|
||||||
# --- 逐筆點名與只算總數,分開兩種 ---
|
|
||||||
#
|
|
||||||
# 逾期與使用者自己登錄的提醒逐筆點名:人看到就做得了。委派清單種入的內建項只算一個總數,
|
|
||||||
# 因為那幾筆等的是接線不是人,每一輪都到期、每一輪都一樣。分種類的判定在寫佇列那一邊,
|
|
||||||
# 這裡只照它標好的種類決定怎麼印。
|
|
||||||
#
|
|
||||||
# --- 一個工作階段只提一次 ---
|
|
||||||
#
|
|
||||||
# 記號檔是 $JSC_HOME/sessions/{工作階段}.reminded。工作階段開始那個事件在續接同一階段時
|
|
||||||
# 會再觸發,沒有記號就會每次都再提一次同一批。
|
|
||||||
# 接不到工作階段代號的 CLI(記號都落在 default 上)另有一條路:session-timer.sh 的 restart
|
|
||||||
# 分支會把這個記號刪掉。判定「這是不是新的工作階段」只有那一支知道,所以刪除掛在那裡,
|
|
||||||
# 這裡不自己再判一次。
|
|
||||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
|
||||||
hook_trace "session-reminder ${1:-}"
|
|
||||||
read_stdin
|
|
||||||
sid=$(session_id)
|
|
||||||
|
|
||||||
MODE="${1:-show}"
|
|
||||||
STATE_DIR="$JSC_HOME/assistant"
|
|
||||||
QUEUE="$STATE_DIR/reminders.tsv"
|
|
||||||
MARK="$JSC_HOME/sessions/$sid.reminded"
|
|
||||||
MAX_ROWS=8
|
|
||||||
|
|
||||||
# 助理沒啟動過就整支安靜退出。
|
|
||||||
[ -d "$STATE_DIR" ] || exit 0
|
|
||||||
|
|
||||||
# 這個工作階段提過了就不再提。peek 一律印,那是人自己要重看。
|
|
||||||
if [ "$MODE" != peek ] && [ -f "$MARK" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
mark_done() {
|
|
||||||
[ "$MODE" = peek ] && return 0
|
|
||||||
mkdir -p "$JSC_HOME/sessions" 2>/dev/null || return 0
|
|
||||||
now_epoch >"$MARK" 2>/dev/null || true
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -f "$QUEUE" ]; then
|
|
||||||
echo "[jsc] 助理的狀態目錄在,但還沒有提醒佇列($QUEUE)。跑過一輪巡檢才會產生,所以這裡沒有提醒**不代表沒有事要做**——要現在看就跑 /jsc-assist:assistant status。"
|
|
||||||
mark_done
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
TAB=$(printf '\t')
|
|
||||||
|
|
||||||
# 檔頭。第一行不是 round 就是格式不對,或者檔案被別的東西蓋掉了,兩種都照實說。
|
|
||||||
_head=$(head -n1 "$QUEUE" 2>/dev/null)
|
|
||||||
_kind=$(printf '%s' "$_head" | cut -f1)
|
|
||||||
if [ "$_kind" != round ]; then
|
|
||||||
echo "[jsc] 提醒佇列($QUEUE)的第一行不是輪次資訊,這一份讀不了。助理下一輪會重寫;在那之前要看待辦就跑 /jsc-assist:assistant status。"
|
|
||||||
mark_done
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
_iso=$(printf '%s' "$_head" | cut -f3)
|
|
||||||
_failing=$(printf '%s' "$_head" | cut -f4)
|
|
||||||
_epoch=$(printf '%s' "$_head" | cut -f5)
|
|
||||||
_tasks=$(printf '%s' "$_head" | cut -f6)
|
|
||||||
case "${_failing:-}" in ''|*[!0-9]*) _failing=0 ;; esac
|
|
||||||
case "${_tasks:-}" in ''|*[!0-9]*) _tasks=0 ;; esac
|
|
||||||
|
|
||||||
# 佇列有多舊。門檻與心跳狀態一律取 heartbeat.sh 印的那一行:那是這台機器判定「助理還在跑」
|
|
||||||
# 的唯一一套規則,這裡再定一套就會出現兩個說法。
|
|
||||||
_age=''
|
|
||||||
case "${_epoch:-}" in
|
|
||||||
''|*[!0-9]*) ;;
|
|
||||||
*) _age=$(( $(now_epoch) - _epoch )) ;;
|
|
||||||
esac
|
|
||||||
_hb=$(sh "$HERE/heartbeat.sh" report </dev/null 2>/dev/null || true)
|
|
||||||
_hbstate=$(printf '%s' "$_hb" | sed -n 's/^state=\([a-z]*\).*/\1/p')
|
|
||||||
_ttl=$(printf '%s' "$_hb" | sed -n 's/.*[[:space:]]ttl=\([0-9]*\).*/\1/p')
|
|
||||||
case "${_ttl:-}" in ''|*[!0-9]*) _ttl=300 ;; esac
|
|
||||||
|
|
||||||
# 兩種列分開數。逐筆點名的是逾期與使用者自己登錄的提醒;內建項只算一個總數。
|
|
||||||
#
|
|
||||||
# 為什麼內建項不逐筆點名:那幾筆等的是接線,不是人。動作是「只提醒」的內建項到現在還沒有
|
|
||||||
# 執行入口,所以每一輪都到期、每一輪都一樣。實測踩到:這台機器八筆全是那一種,於是每一個
|
|
||||||
# 工作階段開頭固定吐八行一模一樣的東西——那不是提醒,是噪音,而這一支自己的註解裡就寫著
|
|
||||||
# 「對著一個刻意的決定每個工作階段催一次,那是噪音不是提醒」。
|
|
||||||
# 分種類的判定在寫佇列那一邊(那裡讀得到 spec_key),這裡只照它標好的種類決定怎麼印。
|
|
||||||
_rows=$(awk -F"$TAB" '$1 == "overdue" || $1 == "remind" { n++ } END { print n + 0 }' "$QUEUE" 2>/dev/null)
|
|
||||||
case "${_rows:-}" in ''|*[!0-9]*) _rows=0 ;; esac
|
|
||||||
_builtin=$(awk -F"$TAB" '$1 == "builtin" { n++ } END { print n + 0 }' "$QUEUE" 2>/dev/null)
|
|
||||||
case "${_builtin:-}" in ''|*[!0-9]*) _builtin=0 ;; esac
|
|
||||||
|
|
||||||
_stale=0
|
|
||||||
if [ -n "$_age" ] && [ "$_age" -gt "$_ttl" ]; then _stale=1; fi
|
|
||||||
[ "$_hbstate" = fresh ] || _stale=1
|
|
||||||
|
|
||||||
if [ "$_rows" -eq 0 ] && [ "$_failing" -eq 0 ] && [ "$_builtin" -eq 0 ]; then
|
|
||||||
# 佇列是新的而且空的:這才是真的「沒有提醒」,安靜退出。
|
|
||||||
# 過期又空的那一種要分兩路。待辦簿有東西,就代表「有事而現在沒有人在算它到期沒到期」,
|
|
||||||
# 那要說一句;零筆就只是助理閒著,人自己按停也算這一種——對著一個刻意的決定每個工作階段
|
|
||||||
# 催一次,那是噪音不是提醒。
|
|
||||||
if [ "$_stale" -eq 1 ] && [ "$_tasks" -gt 0 ]; then
|
|
||||||
_agetxt='年紀算不出來'
|
|
||||||
[ -n "$_age" ] && _agetxt="$(( _age / 60 )) 分鐘前"
|
|
||||||
echo "[jsc] 助理的提醒清單是 ${_iso:-未知時間}($_agetxt)那一輪算的,那一輪沒有提醒;心跳現在是「${_hbstate:-讀不到}」。待辦簿還有 $_tasks 筆,**助理停著的時候沒有人再判它們到期了沒有**,所以這裡安靜不等於沒事。要現況就跑 /jsc-assist:assistant status。"
|
|
||||||
fi
|
|
||||||
# 記號一律記:不記的話同一個工作階段每次觸發都會再讀一次、再說一次同一句話。
|
|
||||||
mark_done
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_stale" -eq 1 ]; then
|
|
||||||
_agetxt='年紀算不出來'
|
|
||||||
[ -n "$_age" ] && _agetxt="$(( _age / 60 )) 分鐘前算的"
|
|
||||||
echo "[jsc] 下面這批提醒是 ${_iso:-未知時間}($_agetxt)那一輪算出來的,助理現在的心跳是「${_hbstate:-讀不到}」,門檻 $_ttl 秒。**助理停著的時候這份清單不會更新**,所以它現在說什麼都只描述那一輪,不描述現在。要現況就跑 /jsc-assist:assistant status。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 逾期排前面:那幾筆的截止時間已經過了,比「該做了」更急。
|
|
||||||
#
|
|
||||||
# 先把兩種併成一份有序清單,再用 head 截筆數,不在迴圈裡自己數。
|
|
||||||
# 理由是那個迴圈接在管線後面,殼會把它放進子殼跑——在子殼裡加的計數,回到外面就沒了,
|
|
||||||
# 於是那道「只列前幾筆」的上限看起來寫了,實際上一次都沒生效。
|
|
||||||
_ordered=$(
|
|
||||||
awk -F"$TAB" '$1 == "overdue" { print }' "$QUEUE" 2>/dev/null
|
|
||||||
awk -F"$TAB" '$1 == "remind" { print }' "$QUEUE" 2>/dev/null
|
|
||||||
)
|
|
||||||
printf '%s\n' "$_ordered" | head -n "$MAX_ROWS" | while IFS="$TAB" read -r _k _id _why _title; do
|
|
||||||
[ -n "$_id" ] || continue
|
|
||||||
if [ "$_k" = overdue ]; then
|
|
||||||
echo "[jsc] 逾期 $_id:${_title:--} —— 已經逾期 ${_why:--}"
|
|
||||||
else
|
|
||||||
echo "[jsc] 到期 $_id:${_title:--} —— ${_why:--}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$_rows" -gt "$MAX_ROWS" ]; then
|
|
||||||
echo "[jsc] 這一批共 $_rows 筆,上面只列了 $MAX_ROWS 筆。其餘的跑 /jsc-assist:assistant status 看得到全部。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 內建項一行講完,逐筆的內容留在監控頁與狀態查詢那邊。
|
|
||||||
# 這一行會一直出現,直到那幾筆有執行入口或被改掉——講明它是常態,讀的人才不會每次都
|
|
||||||
# 當成新消息。
|
|
||||||
if [ "$_builtin" -gt 0 ]; then
|
|
||||||
echo "[jsc] 另有 $_builtin 筆委派清單的內建檢查項到期,動作是只提醒、還沒有執行入口,所以每一輪都會再到期一次。**這一行會一直出現,直到那幾筆接上入口或被改掉**——要逐筆看就跑 /jsc-assist:assistant status。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_failing" -gt 0 ]; then
|
|
||||||
echo "[jsc] 另有 $_failing 筆待辦連續失敗,每一輪都在重試而且不會自動暫停。跑 /jsc-assist:assistant status 看是哪幾筆。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mark_done
|
|
||||||
exit 0
|
|
||||||
@@ -18,9 +18,6 @@
|
|||||||
# restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default,
|
# restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default,
|
||||||
# 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。
|
# 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。
|
||||||
#
|
#
|
||||||
# restart 另外清掉提醒記號($JSC_HOME/sessions/{代號}.reminded):那個記號讓提醒一個工作
|
|
||||||
# 階段只提一次,而共用 default 代號的 CLI 不清就等於只提第一次、往後永遠不提。
|
|
||||||
#
|
|
||||||
# 這兩個子命令另外兼一件事:判定為「新的工作階段」時清除部署後的重啟閘門
|
# 這兩個子命令另外兼一件事:判定為「新的工作階段」時清除部署後的重啟閘門
|
||||||
# (restart-gate.sh clear)。新工作階段代表 CLI 行程是新起的,新版技能組一定已經載入。
|
# (restart-gate.sh clear)。新工作階段代表 CLI 行程是新起的,新版技能組一定已經載入。
|
||||||
# 判準只有這裡知道——start 分支的「起始檔不存在」就是這個 session id 第一次開始,
|
# 判準只有這裡知道——start 分支的「起始檔不存在」就是這個 session id 第一次開始,
|
||||||
@@ -49,11 +46,6 @@ case "${1:-mark}" in
|
|||||||
restart)
|
restart)
|
||||||
now_epoch > "$JSC_HOME/sessions/$sid.start"
|
now_epoch > "$JSC_HOME/sessions/$sid.start"
|
||||||
rm -f "$JSC_HOME/sessions/$sid.end"
|
rm -f "$JSC_HOME/sessions/$sid.end"
|
||||||
# 提醒記號一起清。那個記號讓提醒一個工作階段只提一次,而接不到 session id 的 CLI
|
|
||||||
# 全部共用 default 這一個代號——不清的話第一個工作階段提過之後,往後每一個工作階段
|
|
||||||
# 都會被當成「已經提過」,那支 CLI 從此再也收不到任何提醒。
|
|
||||||
# 清除掛在這裡不掛在提醒那一支:「這是不是新的工作階段」的判準只有這一支知道。
|
|
||||||
rm -f "$JSC_HOME/sessions/$sid.reminded"
|
|
||||||
clear_restart_gate ;; # 接不到 session id 的 CLI 每次工作階段開始都算新的,一律清
|
clear_restart_gate ;; # 接不到 session id 的 CLI 每次工作階段開始都算新的,一律清
|
||||||
mark)
|
mark)
|
||||||
now_epoch > "$JSC_HOME/sessions/$sid.end" ;;
|
now_epoch > "$JSC_HOME/sessions/$sid.end" ;;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-hooks",
|
"name": "jsc-hooks",
|
||||||
"version": "0.5.2",
|
"version": "0.4.2",
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門",
|
||||||
"skills": "./skills/",
|
"skills": "./skills/",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 觸發時機 | 裝好或更新完 jsc 技能組之後,要把十支 hook 接線到每一支已安裝的 CLI 時用;`jsc-cli:deploy` 收尾會把偵測到的 CLI 清單交給它。不用於撰寫新的 hook,也不用於單獨修一支壞掉的 hook,那是 `jsc-hooks:repair` 的事 |
|
| 觸發時機 | 裝好或更新完 jsc 技能組之後,要把九支 hook 接線到每一支已安裝的 CLI 時用;`jsc-cli:deploy` 收尾會把偵測到的 CLI 清單交給它。不用於撰寫新的 hook,也不用於單獨修一支壞掉的 hook,那是 `jsc-hooks:repair` 的事 |
|
||||||
| 關鍵步驟 | 先跑前置步驟解出兩個字面絕對路徑:`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 裡會變成五筆互相矛盾的結局。腳本不在就安靜跳過,回報失敗不得變成接線失敗 |
|
| 關鍵步驟 | 取得 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`(前置步驟解兩個根目錄,各一次)、`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` 做冒煙斷言 |
|
| 外部呼叫 | `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` 做冒煙斷言 |
|
||||||
| 完成條件 | 前置步驟解出的兩個根目錄都是一條存在的絕對路徑(各自用 `[ -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 都有五道關卡各一行判定,沒有任何一道回結束碼 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`。腳本不在磁碟上就跳過,這一步照樣算走完 |
|
||||||
| 可驗證跡象 | 各 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 的結束碼,也不影響本技能的結局 |
|
| 可驗證跡象 | 各 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 的結束碼,也不影響本技能的結局 |
|
||||||
|
|
||||||
## 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` 留下這一輪的 `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` 不在那台機器上就沒有這一筆,修正結果一字不變 |
|
| 可驗證跡象 | 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` 不在那台機器上就沒有這一筆,修正結果一字不變 |
|
||||||
|
|||||||
@@ -7,30 +7,9 @@ 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_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.
|
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/...`.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -87,16 +66,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_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.
|
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.
|
||||||
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.
|
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.
|
||||||
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.
|
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.
|
||||||
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.
|
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.
|
||||||
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.
|
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.
|
||||||
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.
|
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.
|
||||||
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.
|
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 `{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`.
|
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`.
|
||||||
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,17 +4,14 @@
|
|||||||
>
|
>
|
||||||
> 存放位置:本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的目錄專用存取庫,與異常頁的存取庫是兩個不同的存取庫。
|
> 存放位置:本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的目錄專用存取庫,與異常頁的存取庫是兩個不同的存取庫。
|
||||||
>
|
>
|
||||||
> 寫入語意:一個區塊代表一次 hook 異常回報。H2 標題就是那一筆的異常頁頁名 `ERROR_{HASH}`,欄位是標題底下的一層條列,一個欄位一條。寫入前先讀回整頁,同一筆異常已經有區塊就整塊換掉,沒有才在文末附加一個新區塊,最後整頁寫回。一律 upsert 附加,禁止整頁覆蓋,也不得改動別人的區塊。
|
> 寫入語意:一列代表一次 hook 異常回報。寫入前先讀回整頁,同一筆異常已經有列就更新那一列,沒有才在文末附加一列,最後整頁寫回。一律 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}
|
## 異常清單
|
||||||
|
|
||||||
- 時間:{yyyy-MM-dd HH:mm:ss}
|
| 時間 | 頁名 | 存取庫名稱 | 觸發 hook | 退出碼 | 摘要 |
|
||||||
- 頁名:[{頁名}]({wiki-url 印出的絕對網址})
|
| --- | --- | --- | --- | --- | --- |
|
||||||
- 存取庫名稱:{owner}/{repo}
|
| {yyyy-MM-dd HH:mm:ss} | [{頁名}]({wiki-url 印出的絕對網址}) | {owner}/{repo} | {hook_name} | {exit_code} | {error_summary} |
|
||||||
- 觸發 hook:{hook_name}
|
|
||||||
- 退出碼:{exit_code}
|
|
||||||
- 摘要:{error_summary}
|
|
||||||
|
|||||||
+60
-62
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
|
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
|
||||||
# 並在異常目錄頁附上一個索引區塊。頁面內容套用 templates/ 的兩份範本,
|
# 並在 ERROR_CONTENTS 附上一列索引。頁面內容套用 templates/ 的兩份範本,
|
||||||
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
|
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
|
||||||
# Gitea 寫入確認。
|
# Gitea 寫入確認。
|
||||||
# 目錄頁的讀回、比對與整頁寫回一律交給 jsc-gitea 的 tools/wiki-contents.sh,本腳本只組出
|
|
||||||
# 自己那一個區塊。目錄頁版面只留一份正本,十幾個目錄頁才不會各長一種樣子。
|
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
|
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
|
||||||
@@ -22,24 +20,23 @@
|
|||||||
# 3. 算不出 HASH(hash-id 失敗或回空字串)
|
# 3. 算不出 HASH(hash-id 失敗或回空字串)
|
||||||
# 4. 建不出暫存檔(mktemp 失敗)
|
# 4. 建不出暫存檔(mktemp 失敗)
|
||||||
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
|
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
|
||||||
# 異常頁、只解不出目錄頁的存取庫(wiki-contents.sh 回 3),就只寫異常頁、跳過目錄頁
|
# 異常頁、只解不出目錄頁的存取庫,就只寫異常頁、跳過目錄頁更新,印出頁名,仍然 exit 0。
|
||||||
# 更新,印出頁名,仍然 exit 0。找不到 wiki-contents.sh 也走同一條降級路。
|
|
||||||
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
|
||||||
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
|
||||||
#
|
#
|
||||||
# 連結:
|
# 連結:
|
||||||
# 目錄頁那個區塊裡的「頁名」那一條指向異常頁,一律寫成 [{文字}]({連結}),網址取
|
# 目錄頁那一列指向異常頁,一律寫成 [{文字}]({連結}),網址取 jsc-gitea 的
|
||||||
# jsc-gitea 的 gitea.sh wiki-url,不自己組路徑。
|
# 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-contents.sh 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;
|
# 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。
|
||||||
# 訊息走 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 在不在。
|
||||||
#
|
#
|
||||||
@@ -86,8 +83,9 @@ 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
|
||||||
# 目錄頁的存取庫不在這裡解:讀回、比對、整頁寫回都由 wiki-contents.sh 做,它自己解目錄專用
|
# 目錄頁的存取庫解不出來不算失敗:異常頁照寫,只跳過目錄頁更新,仍然 exit 0。
|
||||||
# 存取庫,這邊再解一次就會有兩份規則。解不出來時它回 3,本腳本照原本的語意降級。
|
# 一份寫得成的異常紀錄,不該因為索引沒地方放就整份丟掉。
|
||||||
|
crepo=$(sh "$gsh" wiki-repo CONTENTS 2>/dev/null || true)
|
||||||
|
|
||||||
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
|
||||||
if [ -z "$repo" ]; then
|
if [ -z "$repo" ]; then
|
||||||
@@ -110,10 +108,9 @@ 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
|
||||||
@@ -124,8 +121,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_entry=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
|
tmp_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
|
||||||
trap 'rm -f "$tmp_page" "$tmp_entry"' EXIT
|
trap 'rm -f "$tmp_page" "$tmp_list"' 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" \
|
||||||
@@ -143,17 +140,16 @@ 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=$?
|
||||||
@@ -166,74 +162,76 @@ 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_text="$page"
|
link_cell="$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_text=$(printf '[%s](%s)' "$page" "$url") ;;
|
0) link_cell=$(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
|
||||||
|
|
||||||
# 目錄頁上這一筆是一個 H2 區塊:標題就是異常頁頁名,欄位一行一條,順序與範本的示範區塊
|
row=$(printf '| %s | %s | %s | %s | %s | %s |' \
|
||||||
# 一致。標題不放連結也不放網址——頁名只由存取庫名稱、hook 與時間決定,換主機或改存取庫
|
"$ts" "$link_cell" "$repo" "$hook" "$code" "$summary")
|
||||||
# 都動不到它,比對鍵才找得到既有那一筆。
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
|
|
||||||
# 目錄頁交給 wiki-contents.sh:它負責解目錄專用存取庫、讀回舊頁、比對 H2 標題找既有區塊、
|
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
|
||||||
# 附加或整塊換掉,再整頁寫回,還會把舊的表格頁轉成條列。頁上每一個區塊都是別人回報的異常,
|
# 範本的示範列整列換成本次這一列,不逐格填。連結那一格已經驗過也組好了,拆成頁名與
|
||||||
# 那套「讀得回舊內容才寫」的判斷只留一份正本,才不會每個目錄頁各寫一套、錯一次少一筆紀錄。
|
# 網址兩個佔位再填,會在驗不過的時候留下一個空網址的死連結。
|
||||||
# 傳進去的那個 2 是 key-col:舊表格版目錄頁裡持有身分的欄位序號。舊版第 2 欄是頁名,
|
ROW="$row" awk '
|
||||||
# 舊頁自動轉條列時要靠它取 H2 標題;頁面已經是條列格式時這個參數用不到。
|
index($0, "| {yyyy-MM-dd HH:mm:ss} |") == 1 { print ENVIRON["ROW"]; next }
|
||||||
# 路徑由已經解出來的 gitea.sh 推得,與 link-check.sh 同一套做法,三支同一個 tools 目錄。
|
{ print }
|
||||||
wcs="$(dirname "$gsh")/wiki-contents.sh"
|
' "$ROOT/templates/error-contents.md" > "$tmp_list"
|
||||||
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=$?
|
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
|
||||||
case "$wc_code" in
|
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
|
||||||
0) ;;
|
# 0 → 讀到既有內容,附加新列(讀得到但整頁是空的,沒有既有列會被蓋掉,套範本才安全)
|
||||||
# 3 是目錄頁的存取庫沒設定。異常頁已經寫成功,一份寫得成的異常紀錄不該因為索引沒地方放
|
# 4 → 頁面真的還不存在,只有這個碼可以用範本建立新頁
|
||||||
# 就整份丟掉,所以只跳過目錄頁,結束碼照舊回 0。
|
# 7 → 金鑰失效或權限不足,舊內容未知,放棄目錄頁寫入
|
||||||
3) echo "[jsc] 目錄頁的 wiki 存取庫解不出來(wiki-contents.sh 回 3),只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2 ;;
|
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
|
||||||
# 其餘結束碼都代表這一筆沒進到目錄頁:7 金鑰失效、8 其他 API 失敗都是舊內容未知而中止,
|
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
|
||||||
# 1 是組不出頁面內容或寫入失敗,2 是用法錯誤,4 是頁面不存在又沒收到範本。
|
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
|
||||||
|
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] 目錄頁寫入失敗(wiki-contents.sh 結束碼 $wc_code),$page 已建立。" >&2
|
echo "[jsc] 讀取目錄頁失敗($crepo,wiki-get 結束碼 $get_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
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
#
|
#
|
||||||
# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh):
|
# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh):
|
||||||
# {ts,cli,hook,event,exit,detail,jsc}
|
# {ts,cli,hook,event,exit,detail,jsc}
|
||||||
# jsc 欄位:command 或 stderr 命中十支 hook 腳本任一支,或命中 jsc-hooks 路徑,就是 true,
|
# jsc 欄位:command 或 stderr 命中九支 hook 腳本任一支,或命中 jsc-hooks 路徑,就是 true,
|
||||||
# 否則 false。分得出來才用得上——非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、
|
# 否則 false。分得出來才用得上——非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、
|
||||||
# 不轉 jsc-hooks:repair。
|
# 不轉 jsc-hooks:repair。
|
||||||
#
|
#
|
||||||
@@ -52,7 +52,7 @@ case "$cli" in
|
|||||||
codex|copilot|antigravity|kiro)
|
codex|copilot|antigravity|kiro)
|
||||||
printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到"
|
printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到"
|
||||||
echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。"
|
echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。"
|
||||||
echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行十支 hook 驗執行期。"
|
echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行九支 hook 驗執行期。"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -98,7 +98,6 @@ extract_errors() {
|
|||||||
# 第三方的,只回報不修正。
|
# 第三方的,只回報不修正。
|
||||||
function is_jsc(t) {
|
function is_jsc(t) {
|
||||||
if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \
|
if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \
|
||||||
|| index(t, "session-reminder.sh") \
|
|
||||||
|| index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \
|
|| index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \
|
||||||
|| index(t, "version-guard.sh") || index(t, "restart-gate.sh") \
|
|| index(t, "version-guard.sh") || index(t, "restart-gate.sh") \
|
||||||
|| index(t, "comment-scope.sh") || index(t, "lang-guard.sh") \
|
|| index(t, "comment-scope.sh") || index(t, "lang-guard.sh") \
|
||||||
|
|||||||
+23
-91
@@ -3,10 +3,8 @@
|
|||||||
# 用法:
|
# 用法:
|
||||||
# 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} [--verdict]
|
# wire-cli.sh status {claude|codex|copilot|antigravity|kiro} 唯讀盤點接線現況,不寫檔也不執行 hook
|
||||||
# 唯讀盤點接線現況,不寫檔也不執行 hook。
|
|
||||||
# --verdict 只換結束碼語意,輸出一字不變
|
|
||||||
#
|
#
|
||||||
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
|
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
|
||||||
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
|
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
# 就好,不必在 SKILL.md 或 README 各抄一份——抄了就會在加減判定路徑時漂移。
|
# 就好,不必在 SKILL.md 或 README 各抄一份——抄了就會在加減判定路徑時漂移。
|
||||||
#
|
#
|
||||||
# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc 段落,不會重複疊加):
|
# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc 段落,不會重複疊加):
|
||||||
# claude — 什麼都不用寫,hooks/hooks.json 已自動接線十支 hook(PreToolUse matcher Skill)
|
# claude — 什麼都不用寫,hooks/hooks.json 已自動接線九支 hook(PreToolUse matcher Skill)
|
||||||
# codex — pre-tool hook 寫在 .codex-plugin/plugin.json 的 hooks 鍵,做 Codex 專屬覆寫
|
# codex — pre-tool hook 寫在 .codex-plugin/plugin.json 的 hooks 鍵,做 Codex 專屬覆寫
|
||||||
# (PreToolUse matcher Bash,接 restart-gate.sh 與 version-guard.sh)。
|
# (PreToolUse matcher Bash,接 restart-gate.sh 與 version-guard.sh)。
|
||||||
# Claude 用的 hooks/hooks.json 完全不動:那一份是 Skill matcher,而 Codex
|
# Claude 用的 hooks/hooks.json 完全不動:那一份是 Skill matcher,而 Codex
|
||||||
@@ -74,7 +72,7 @@
|
|||||||
#
|
#
|
||||||
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護,也不得再說「沒有 pre-tool hook」——
|
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護,也不得再說「沒有 pre-tool hook」——
|
||||||
# 五支裡有四支都有,先前失效的原因是接錯位置,不是沒有位置可接:
|
# 五支裡有四支都有,先前失效的原因是接錯位置,不是沒有位置可接:
|
||||||
# claude 十支 hook 全接,回報 wired
|
# claude 九支 hook 全接,回報 wired
|
||||||
# codex、copilot、antigravity 版本前置檢查與部署後重啟閘門都擋得下來,回報 wired。
|
# codex、copilot、antigravity 版本前置檢查與部署後重啟閘門都擋得下來,回報 wired。
|
||||||
# SDLC 模型鎖仍是技能步驟檢查,write-guard.sh 三種模式尚未接線,
|
# SDLC 模型鎖仍是技能步驟檢查,write-guard.sh 三種模式尚未接線,
|
||||||
# 註解範圍與繁中編碼仍是 sweep,reason 要逐項講明。
|
# 註解範圍與繁中編碼仍是 sweep,reason 要逐項講明。
|
||||||
@@ -130,9 +128,6 @@
|
|||||||
# 結束碼(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。判讀邏輯跟接線
|
||||||
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
|
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
|
||||||
@@ -141,14 +136,8 @@
|
|||||||
# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪
|
# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪
|
||||||
# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。
|
# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。
|
||||||
set -u
|
set -u
|
||||||
# 這兩行一定要實體解析(`cd -P` 加 `pwd -P`),不能拿邏輯路徑。
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
# ROOT 會被 ensure_stable_root() 當成 `ln -sfn "$ROOT" "$_link"` 的目標,而這支腳本本身
|
ROOT=$(cd "$HERE/.." && pwd)
|
||||||
# 常常就是經由那條連結被叫起來的。邏輯解析會把連結原樣留在路徑裡,於是 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"
|
||||||
@@ -174,30 +163,6 @@ 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。子命令解析完就判:預設動作是接線,
|
||||||
# 所以少打一個子命令就會直接改環境,這個判定要擋的正是那一次手滑。
|
# 所以少打一個子命令就會直接改環境,這個判定要擋的正是那一次手滑。
|
||||||
@@ -282,19 +247,6 @@ codex_derive_hooks() { # $1=來源 hooks.json
|
|||||||
# 條目形態照這台機器上既有的那一筆第三方設定(type、bash、timeoutSec),不套 Claude 的形狀。
|
# 條目形態照這台機器上既有的那一筆第三方設定(type、bash、timeoutSec),不套 Claude 的形狀。
|
||||||
# 指令前綴帶 JSC_CLI=copilot:閘門靠 cli_name() 認代號才取得到技能名與阻擋形態,沒設就一律
|
# 指令前綴帶 JSC_CLI=copilot:閘門靠 cli_name() 認代號才取得到技能名與阻擋形態,沒設就一律
|
||||||
# 安靜放行,matcher 對、位置對、卻一次都擋不下來。別名那條路只在走別名啟動時才成立,不能靠它。
|
# 安靜放行,matcher 對、位置對、卻一次都擋不下來。別名那條路只在走別名啟動時才成立,不能靠它。
|
||||||
# 列出一份檔案或一段設定裡引用到的 hook 腳本名,一行一個、去重。
|
|
||||||
#
|
|
||||||
# 這一支存在的理由是「接線寫了哪幾支」與「盤點驗了哪幾支」本來是兩份手寫清單。
|
|
||||||
# 2026-09-07 實測踩到:新加第十支 hook 之後,claude 與 kiro 的盤點點名得出它,
|
|
||||||
# codex、copilot、antigravity 三段的盤點卻各自只驗自己寫死的那兩支——那三支的接線
|
|
||||||
# 盤點看不出第十支在不在,而 smoke 那一行還自稱「十支 hook」。兩句話都是真的,
|
|
||||||
# 範圍不同,讀的人分不出來。
|
|
||||||
# 改成從接線那一邊實際會寫出去的內容抽名字,兩邊就只有一份清單:往後加一支 hook,
|
|
||||||
# 盤點自動跟著驗,不必記得回來改第二個地方。
|
|
||||||
hook_scripts_of() { # 讀標準輸入,印出 {名稱}.sh
|
|
||||||
grep -oE '[a-z][a-z0-9-]*\.sh' 2>/dev/null | sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
copilot_hook_entries() {
|
copilot_hook_entries() {
|
||||||
cat <<JSCEOF
|
cat <<JSCEOF
|
||||||
{
|
{
|
||||||
@@ -557,8 +509,7 @@ kiro_agent_json() {
|
|||||||
],
|
],
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"agentSpawn": [
|
"agentSpawn": [
|
||||||
{ "command": "JSC_CLI=kiro sh \\"$WIRE_HOOKS/session-timer.sh\\" restart </dev/null", "timeout_ms": 10000 },
|
{ "command": "JSC_CLI=kiro sh \\"$WIRE_HOOKS/session-timer.sh\\" restart </dev/null", "timeout_ms": 10000 }
|
||||||
{ "command": "JSC_CLI=kiro sh \\"$WIRE_HOOKS/session-reminder.sh\\" </dev/null", "timeout_ms": 10000 }
|
|
||||||
],
|
],
|
||||||
"userPromptSubmit": [
|
"userPromptSubmit": [
|
||||||
{ "command": "JSC_CLI=kiro sh \\"$WIRE_HOOKS/restart-gate.sh\\"", "timeout_ms": 10000 },
|
{ "command": "JSC_CLI=kiro sh \\"$WIRE_HOOKS/restart-gate.sh\\"", "timeout_ms": 10000 },
|
||||||
@@ -1401,7 +1352,7 @@ if [ "$action" = smoke ]; then
|
|||||||
smoke_n_sn=0; smoke_n_dn=0; smoke_n_cx=0; smoke_n_sh=0
|
smoke_n_sn=0; smoke_n_dn=0; smoke_n_cx=0; smoke_n_sh=0
|
||||||
# 預期條數(改動判定路徑時一起改):每一類都要有自己的計數器,印得出結果行卻沒人計數的
|
# 預期條數(改動判定路徑時一起改):每一類都要有自己的計數器,印得出結果行卻沒人計數的
|
||||||
# 那一類會讓總數永遠對不上,斷言也就形同虛設。
|
# 那一類會讓總數永遠對不上,斷言也就形同虛設。
|
||||||
# hook 模式 十支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、
|
# hook 模式 九支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、
|
||||||
# lang-guard.sh 與 write-guard.sh 各有多個模式,所以比 hook 支數多
|
# lang-guard.sh 與 write-guard.sh 各有多個模式,所以比 hook 支數多
|
||||||
# 模型與階段鎖 sdlc-gate.sh 取模型代號的四條來源判定路徑,加上「不知道能力就擋下」的
|
# 模型與階段鎖 sdlc-gate.sh 取模型代號的四條來源判定路徑,加上「不知道能力就擋下」的
|
||||||
# 三種情形、check 的 fail-closed,以及階段鎖狀態檔的 CLI 區隔與舊格式相容
|
# 三種情形、check 的 fail-closed,以及階段鎖狀態檔的 CLI 區隔與舊格式相容
|
||||||
@@ -1413,7 +1364,7 @@ if [ "$action" = smoke ]; then
|
|||||||
# 阻擋形態 deny.sh 四種形態(stderr 加 2、stdout deny JSON、kiro 注入、未知代號的保守預設)
|
# 阻擋形態 deny.sh 四種形態(stderr 加 2、stdout deny JSON、kiro 注入、未知代號的保守預設)
|
||||||
# 跨 CLI 貫通 restart-gate.sh 吃五支 CLI 的真實負載,驗判定與輸出形態串得起來
|
# 跨 CLI 貫通 restart-gate.sh 吃五支 CLI 的真實負載,驗判定與輸出形態串得起來
|
||||||
# 接線形狀 每支 CLI 要寫出去的內容真的產出來一次,驗結構本身(正反案例各一組)
|
# 接線形狀 每支 CLI 要寫出去的內容真的產出來一次,驗結構本身(正反案例各一組)
|
||||||
SMOKE_EXPECT_HOOK=18
|
SMOKE_EXPECT_HOOK=17
|
||||||
SMOKE_EXPECT_MODEL=12
|
SMOKE_EXPECT_MODEL=12
|
||||||
SMOKE_EXPECT_WP=6
|
SMOKE_EXPECT_WP=6
|
||||||
SMOKE_EXPECT_RS=16
|
SMOKE_EXPECT_RS=16
|
||||||
@@ -1464,7 +1415,6 @@ if [ "$action" = smoke ]; then
|
|||||||
}
|
}
|
||||||
|
|
||||||
smoke_one session-timer.sh mark
|
smoke_one session-timer.sh mark
|
||||||
smoke_one session-reminder.sh
|
|
||||||
smoke_one sdlc-gate.sh check
|
smoke_one sdlc-gate.sh check
|
||||||
smoke_one version-guard.sh
|
smoke_one version-guard.sh
|
||||||
smoke_one restart-gate.sh
|
smoke_one restart-gate.sh
|
||||||
@@ -2255,7 +2205,7 @@ if [ "$action" = smoke ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$smoke_fails" -eq 0 ]; then
|
if [ "$smoke_fails" -eq 0 ]; then
|
||||||
printf 'status=ok reason=%s\n' "十支 hook 的每個接線模式都跑得完,模型與階段鎖、工作包歸屬、部署後重啟閘門、寫入提交閘門、相依版本檢查、五支 CLI 的技能名解析、四種阻擋形態、跨 CLI 貫通與各 CLI 的接線形狀的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤"
|
printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型與階段鎖、工作包歸屬、部署後重啟閘門、寫入提交閘門、相依版本檢查、五支 CLI 的技能名解析、四種阻擋形態、跨 CLI 貫通與各 CLI 的接線形狀的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤"
|
||||||
printf 'lines\t%s\n' "$smoke_lines"
|
printf 'lines\t%s\n' "$smoke_lines"
|
||||||
cat "$smoke_out"; rm -f "$smoke_out"; exit 0
|
cat "$smoke_out"; rm -f "$smoke_out"; exit 0
|
||||||
fi
|
fi
|
||||||
@@ -2275,8 +2225,8 @@ if [ "$action" = status ]; then
|
|||||||
st_missing=0
|
st_missing=0
|
||||||
st_degrade=""
|
st_degrade=""
|
||||||
# 接上時的 reason 也要一支 CLI 一句:五支的覆蓋範圍不一樣,共用一句話就會把「codex 接了兩道閘門」
|
# 接上時的 reason 也要一支 CLI 一句:五支的覆蓋範圍不一樣,共用一句話就會把「codex 接了兩道閘門」
|
||||||
# 講成「codex 十支全接」。預設值給 claude,其餘各自在下面覆寫。
|
# 講成「codex 九支全接」。預設值給 claude,其餘各自在下面覆寫。
|
||||||
st_wired="hooks.json 自動接線全部十支 hook"
|
st_wired="hooks.json 自動接線全部九支 hook"
|
||||||
|
|
||||||
# 記一個檢查點。$1=項目名 $2=路徑 $3=present|missing|unverified
|
# 記一個檢查點。$1=項目名 $2=路徑 $3=present|missing|unverified
|
||||||
#
|
#
|
||||||
@@ -2351,13 +2301,10 @@ if [ "$action" = status ]; then
|
|||||||
claude_hooks="$claude_root/hooks/hooks.json"
|
claude_hooks="$claude_root/hooks/hooks.json"
|
||||||
if [ -n "$claude_root" ] && [ -f "$claude_hooks" ]; then st_item hooks.json "$claude_hooks" present
|
if [ -n "$claude_root" ] && [ -f "$claude_hooks" ]; then st_item hooks.json "$claude_hooks" present
|
||||||
else st_item hooks.json "${claude_hooks:-$HOME/.claude/plugins/installed_plugins.json}" missing; fi
|
else st_item hooks.json "${claude_hooks:-$HOME/.claude/plugins/installed_plugins.json}" missing; fi
|
||||||
# 十支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。
|
# 九支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。
|
||||||
# 每一支都列一項,一支都不省。reason 那行講的是「全部十支」,列舉卻只挑幾支的話,
|
# 每一支都列一項,一支都不省。reason 那行講的是「全部九支」,列舉卻只挑幾支的話,
|
||||||
# 拿這份輸出驗收接線的人會把沒列到的當成沒接——模型能力鎖就是這樣被誤判成沒接線的。
|
# 拿這份輸出驗收接線的人會把沒列到的當成沒接——模型能力鎖就是這樣被誤判成沒接線的。
|
||||||
# 別支 CLI 的那幾段同樣逐支點名,但名單各自取自「那一支的接線實際會寫出去的內容」,
|
for _h in comment-scope lang-guard restart-gate skill-usage version-guard; do
|
||||||
# 因為每一支能接到的範圍不同:codex 拿的是從這一份推導出來的複本(十支全帶),
|
|
||||||
# copilot 與 antigravity 只接得上兩道閘門,其餘幾支在那兩支 CLI 上是沒有事件可掛。
|
|
||||||
for _h in comment-scope lang-guard restart-gate skill-usage session-reminder version-guard; do
|
|
||||||
if [ -f "$claude_hooks" ] && grep -qF "$_h.sh" "$claude_hooks" 2>/dev/null
|
if [ -f "$claude_hooks" ] && grep -qF "$_h.sh" "$claude_hooks" 2>/dev/null
|
||||||
then st_item "$_h" "$claude_hooks" present
|
then st_item "$_h" "$claude_hooks" present
|
||||||
else st_item "$_h" "$claude_hooks" missing; fi
|
else st_item "$_h" "$claude_hooks" missing; fi
|
||||||
@@ -2434,9 +2381,7 @@ if [ "$action" = status ]; then
|
|||||||
if [ -f "$cx_hooks" ] && grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null
|
if [ -f "$cx_hooks" ] && grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null
|
||||||
then st_item codex-no-skill-matcher "$cx_hooks" missing
|
then st_item codex-no-skill-matcher "$cx_hooks" missing
|
||||||
else st_item codex-no-skill-matcher "$cx_hooks" present; fi
|
else st_item codex-no-skill-matcher "$cx_hooks" present; fi
|
||||||
# 逐支對照來源那一份。codex 讀的是從 hooks/hooks.json 推導出來的複本,所以「該有哪幾支」
|
for _g in restart-gate.sh version-guard.sh; do
|
||||||
# 的答案在來源檔裡,不在這裡寫死——推導漏掉一支的時候,這一項才說得出是漏了哪一支。
|
|
||||||
for _g in $(hook_scripts_of <"$HOOKS/hooks.json"); do
|
|
||||||
if [ -f "$cx_hooks" ] && grep -qF "$_g" "$cx_hooks" 2>/dev/null
|
if [ -f "$cx_hooks" ] && grep -qF "$_g" "$cx_hooks" 2>/dev/null
|
||||||
then st_item "codex-${_g%.sh}" "$cx_hooks" present
|
then st_item "codex-${_g%.sh}" "$cx_hooks" present
|
||||||
else st_item "codex-${_g%.sh}" "$cx_hooks" missing; fi
|
else st_item "codex-${_g%.sh}" "$cx_hooks" missing; fi
|
||||||
@@ -2464,13 +2409,6 @@ if [ "$action" = status ]; then
|
|||||||
if copilot_single_case_ok "$cp_settings"
|
if copilot_single_case_ok "$cp_settings"
|
||||||
then st_item single-event-case "$cp_settings" present
|
then st_item single-event-case "$cp_settings" present
|
||||||
else st_item single-event-case "$cp_settings" missing; fi
|
else st_item single-event-case "$cp_settings" missing; fi
|
||||||
# 逐支對照接線那一段自己會寫出去的條目。原本這一段一支 hook 都沒點名,只驗「hooks 鍵在」
|
|
||||||
# ——鍵在而某一支條目掉了,照樣回 present,那正是這一整支腳本一直在防的形態。
|
|
||||||
for _g in $(copilot_hook_entries | hook_scripts_of); do
|
|
||||||
if [ -f "$cp_settings" ] && grep -qF "$_g" "$cp_settings" 2>/dev/null
|
|
||||||
then st_item "${_g%.sh}" "$cp_settings" present
|
|
||||||
else st_item "${_g%.sh}" "$cp_settings" missing; fi
|
|
||||||
done
|
|
||||||
# CLI 代號也是接線的一部分,理由同 codex 那一項:取不到代號的閘門一律安靜放行。
|
# CLI 代號也是接線的一部分,理由同 codex 那一項:取不到代號的閘門一律安靜放行。
|
||||||
if [ -f "$cp_settings" ] && grep -qF 'JSC_CLI=copilot' "$cp_settings" 2>/dev/null
|
if [ -f "$cp_settings" ] && grep -qF 'JSC_CLI=copilot' "$cp_settings" 2>/dev/null
|
||||||
then st_item cli-code "$cp_settings" present
|
then st_item cli-code "$cp_settings" present
|
||||||
@@ -2508,8 +2446,7 @@ if [ "$action" = status ]; then
|
|||||||
if antigravity_flat_ok "$ag_hooks"
|
if antigravity_flat_ok "$ag_hooks"
|
||||||
then st_item preinvocation-flat "$ag_hooks" present
|
then st_item preinvocation-flat "$ag_hooks" present
|
||||||
else st_item preinvocation-flat "$ag_hooks" missing; fi
|
else st_item preinvocation-flat "$ag_hooks" missing; fi
|
||||||
# 逐支對照接線那一段自己會寫出去的內容,不在這裡另寫一份清單。
|
for _g in restart-gate.sh version-guard.sh; do
|
||||||
for _g in $(antigravity_hooks_block | hook_scripts_of); do
|
|
||||||
if [ -f "$ag_hooks" ] && grep -qF "$_g" "$ag_hooks" 2>/dev/null
|
if [ -f "$ag_hooks" ] && grep -qF "$_g" "$ag_hooks" 2>/dev/null
|
||||||
then st_item "${_g%.sh}" "$ag_hooks" present
|
then st_item "${_g%.sh}" "$ag_hooks" present
|
||||||
else st_item "${_g%.sh}" "$ag_hooks" missing; fi
|
else st_item "${_g%.sh}" "$ag_hooks" missing; fi
|
||||||
@@ -2557,7 +2494,7 @@ if [ "$action" = status ]; then
|
|||||||
then st_item "event-$_e" "$kr_agent" present
|
then st_item "event-$_e" "$kr_agent" present
|
||||||
else st_item "event-$_e" "$kr_agent" missing; fi
|
else st_item "event-$_e" "$kr_agent" missing; fi
|
||||||
done
|
done
|
||||||
for _g in session-timer.sh session-reminder.sh restart-gate.sh version-guard.sh ste100-guard.sh comment-scope.sh lang-guard.sh; do
|
for _g in session-timer.sh restart-gate.sh version-guard.sh ste100-guard.sh comment-scope.sh lang-guard.sh; do
|
||||||
if [ -f "$kr_agent" ] && grep -qF "$_g" "$kr_agent" 2>/dev/null
|
if [ -f "$kr_agent" ] && grep -qF "$_g" "$kr_agent" 2>/dev/null
|
||||||
then st_item "${_g%.sh}" "$kr_agent" present
|
then st_item "${_g%.sh}" "$kr_agent" present
|
||||||
else st_item "${_g%.sh}" "$kr_agent" missing; fi
|
else st_item "${_g%.sh}" "$kr_agent" missing; fi
|
||||||
@@ -2576,10 +2513,7 @@ 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"
|
cat "$st_items"; rm -f "$st_items"; exit 1
|
||||||
# --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
|
||||||
@@ -2605,7 +2539,7 @@ case "$cli" in
|
|||||||
done
|
done
|
||||||
printf 'status=wired reason=%s\n' "hooks.json 自動接線"
|
printf 'status=wired reason=%s\n' "hooks.json 自動接線"
|
||||||
echo "$WIRE_PATH_NOTE"
|
echo "$WIRE_PATH_NOTE"
|
||||||
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部十支 hook,無需寫入設定。"
|
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部九支 hook,無需寫入設定。"
|
||||||
echo "[jsc] claude:只有 claude 有 pre-tool hook,版本前置檢查、部署後重啟閘門與 write-guard.sh 的三種模式只在這裡擋得下來;其他四個 CLI 這幾道閘門都接不上。"
|
echo "[jsc] claude:只有 claude 有 pre-tool hook,版本前置檢查、部署後重啟閘門與 write-guard.sh 的三種模式只在這裡擋得下來;其他四個 CLI 這幾道閘門都接不上。"
|
||||||
echo "[jsc] claude:只有 claude 有 post-tool hook,comment-scope.sh 與 lang-guard.sh 的逐檔即時掃描只在這裡接得上;其他四個 CLI 改用 sweep 掃整個工作區,時機晚一輪或晚到工作階段結束。"
|
echo "[jsc] claude:只有 claude 有 post-tool hook,comment-scope.sh 與 lang-guard.sh 的逐檔即時掃描只在這裡接得上;其他四個 CLI 改用 sweep 掃整個工作區,時機晚一輪或晚到工作階段結束。"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -2660,11 +2594,9 @@ case "$cli" in
|
|||||||
|| fail "$cx_hooks 沒有 Bash matcher,codex 沒有 Skill 工具,擋不到技能載入那一次"
|
|| fail "$cx_hooks 沒有 Bash matcher,codex 沒有 Skill 工具,擋不到技能載入那一次"
|
||||||
grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null \
|
grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null \
|
||||||
&& fail "$cx_hooks 還留著 Skill matcher,codex 沒有那個工具,那一組永遠不會被叫用"
|
&& fail "$cx_hooks 還留著 Skill matcher,codex 沒有那個工具,那一組永遠不會被叫用"
|
||||||
# 逐支對照來源那一份,不只驗那兩道閘門:推導是整份複製再改 matcher,漏掉任何一支都算
|
for _g in restart-gate.sh version-guard.sh; do
|
||||||
# 推導壞了,而漏掉的那一支在 codex 上就是永遠不會被叫用。
|
|
||||||
for _g in $(hook_scripts_of <"$HOOKS/hooks.json"); do
|
|
||||||
grep -qF "$_g" "$cx_hooks" 2>/dev/null \
|
grep -qF "$_g" "$cx_hooks" 2>/dev/null \
|
||||||
|| fail "$cx_hooks 沒有接上 $_g,那一支在 codex 上不會生效(來源是 $HOOKS/hooks.json,推導漏了它)"
|
|| fail "$cx_hooks 沒有接上 $_g,那道閘門在 codex 上不會生效"
|
||||||
done
|
done
|
||||||
# 代號單獨驗一項:少了它,兩道閘門認不出現在跑的是哪一支 CLI,技能名解不出來就整批安靜放行。
|
# 代號單獨驗一項:少了它,兩道閘門認不出現在跑的是哪一支 CLI,技能名解不出來就整批安靜放行。
|
||||||
grep -qF 'JSC_CLI=codex' "$cx_hooks" 2>/dev/null \
|
grep -qF 'JSC_CLI=codex' "$cx_hooks" 2>/dev/null \
|
||||||
@@ -2830,7 +2762,7 @@ case "$cli" in
|
|||||||
# 兩層 glob 是技能看不看得到的關鍵:預設只掃一層,jsc 的技能在第二層。
|
# 兩層 glob 是技能看不看得到的關鍵:預設只掃一層,jsc 的技能在第二層。
|
||||||
grep -qF '/skills/*/*/SKILL.md' "$kr_agent" 2>/dev/null \
|
grep -qF '/skills/*/*/SKILL.md' "$kr_agent" 2>/dev/null \
|
||||||
|| fail "$kr_agent 的 resources 少了兩層 glob,jsc 技能一支都載不到"
|
|| fail "$kr_agent 的 resources 少了兩層 glob,jsc 技能一支都載不到"
|
||||||
for _s in session-timer.sh session-reminder.sh restart-gate.sh version-guard.sh ste100-guard.sh comment-scope.sh lang-guard.sh; do
|
for _s in session-timer.sh restart-gate.sh version-guard.sh ste100-guard.sh comment-scope.sh lang-guard.sh; do
|
||||||
grep -qF "$_s" "$kr_agent" 2>/dev/null || fail "$kr_agent 的 hooks 沒有接到 $_s"
|
grep -qF "$_s" "$kr_agent" 2>/dev/null || fail "$kr_agent 的 hooks 沒有接到 $_s"
|
||||||
done
|
done
|
||||||
# 這個 agent 要被選用才算數,所以還要設 chat.defaultAgent。內建 agent 改不了,
|
# 這個 agent 要被選用才算數,所以還要設 chat.defaultAgent。內建 agent 改不了,
|
||||||
|
|||||||
Reference in New Issue
Block a user