From eebe2a28734df4400956447a1f2ad66e3cef6c08 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 31 Aug 2026 19:04:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(wire-cli):=20=E6=8E=A5=E4=B8=8A=E5=9B=9B?= =?UTF-8?q?=E6=94=AF=20CLI=20=E7=9A=84=20pre-tool=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What: - tools/wire-cli.sh 重寫 codex、copilot、antigravity、kiro 四支的接線、purge、status 與 smoke。 - 新增 hooks/codex-hooks.json,由 hooks/hooks.json 推導產生。 - .codex-plugin/plugin.json 加上 hooks 路徑鍵,指向那份檔案。 Why:這四支其實都有能介入的 pre-tool 事件,原本卻接錯位置,抓到五個同一類的無聲失效——設定看起來正確、CLI 靜默不理、不報錯: - codex 的 matcher 用 Skill,但 Codex 沒有 Skill 這個工具,技能是模型自己用 Bash 讀 SKILL.md 載入的。 - codex 的 hooks 鍵寫成內嵌物件,實際規格是路徑字串,內嵌物件解析不了。 - antigravity 的 PreToolUse 寫成 Flat,實際要 matcher 加 hooks 包一層的 Grouped。Flat 的那一段整個被丟掉,hook 名稱照樣登記,檔案讀起來還是對的。 - copilot 的設定寫到 $COPILOT_HOME/hooks/,那是 hook 要跑的腳本目錄、不是設定目錄,設定從來不會被讀。 - 三支新接的命令沒帶 JSC_CLI={代號},閘門認不出自己跑在哪支 CLI 上,一次都擋不下來。 How: - codex:PreToolUse matcher 換成 Bash。codex-hooks.json 由 hooks/hooks.json 整份複製,再把 matcher 的 Skill 改寫成 Bash,其餘事件原樣保留,Claude 那份維持唯一真實來源,第二份絕不手寫。 - copilot:pre-tool hook 併進 ~/.copilot/settings.json 的頂層 hooks 鍵,matcher 用小寫 skill,事件名只寫一種大小寫。那份檔案同時裝著 enabledPlugins 與 extraKnownMarketplaces,所以合併不覆寫:寫前備份、只動 jsc 自己那幾筆、寫後回讀核對最上層鍵與別人的條目,對不上就還原。指引檔改寫到 $COPILOT_HOME 底下。 - antigravity:寫 ~/.gemini/config/hooks.json 的 jsc 段落。PreToolUse 改成 Grouped、matcher 是錨定的 view_file,錨點不能省,省了會連 view_file_outline 一起命中;另接 Flat 的 PreInvocation,攔斜線指令那條不產生工具呼叫的路。 - kiro:hook 宣告搬到 ~/.kiro/agents/jsc.json 的 hooks 鍵,事件只用 agentSpawn、userPromptSubmit、stop,欄位是 command 與 timeout_ms,並把 settings/cli.json 的 chat.defaultAgent 設成 jsc。kiro-cli agent validate 四種情況一律回 0,所以判準看輸出、不看結束碼。 - 四支非 claude 的接線命令一律以 JSC_CLI={代號} 前綴自帶代號,接線時、status 與 smoke 各斷言一次。 - status 從只驗「鍵在不在」改成驗形狀與位置,並新增第三格 unverified,把「驗不了」跟「驗過了」分開,只有 missing 算缺項。 - smoke 新增 26 條接線形狀斷言,每條正向配一條反向,另把五支 CLI 的真實負載直接餵進 restart-gate.sh,驗解析、判定、輸出整條串得起來。前兩段分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前失效的樣子。 Who:codex、copilot、antigravity、kiro 四支 CLI 的 pre-tool hook 接線修正。 --- .codex-plugin/plugin.json | 3 +- hooks/codex-hooks.json | 125 ++++ tools/wire-cli.sh | 1368 +++++++++++++++++++++++++++++++++---- 3 files changed, 1347 insertions(+), 149 deletions(-) create mode 100644 hooks/codex-hooks.json diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index a8aab1a..20b68ee 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,7 @@ { + "hooks": "./hooks/codex-hooks.json", "name": "jsc-hooks", - "version": "0.3.3", + "version": "0.3.4", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查、註解範圍守門、繁中編碼守門、部署後強制重啟、寫入與提交閘門", "skills": "./skills", "jsc": { diff --git a/hooks/codex-hooks.json b/hooks/codex-hooks.json new file mode 100644 index 0000000..5b64e35 --- /dev/null +++ b/hooks/codex-hooks.json @@ -0,0 +1,125 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "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'" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "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/ste100-guard.sh\"'" + }, + { + "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/sdlc-gate.sh\" check'" + }, + { + "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/sdlc-gate.sh\" wp-check prompt'" + }, + { + "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/comment-scope.sh\" prompt'" + }, + { + "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/lang-guard.sh\" prompt'" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "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\" mark'" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "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\" mark'" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "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/restart-gate.sh\"'" + }, + { + "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/version-guard.sh\"'" + }, + { + "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/sdlc-gate.sh\" wp-check skill'" + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "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/write-guard.sh\" stage'" + }, + { + "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/write-guard.sh\" review'" + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "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/write-guard.sh\" commit'" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "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/skill-usage.sh\"'" + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "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/comment-scope.sh\"'" + }, + { + "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/lang-guard.sh\"'" + } + ] + } + ] + } +} diff --git a/tools/wire-cli.sh b/tools/wire-cli.sh index d660c2a..0fd2dde 100755 --- a/tools/wire-cli.sh +++ b/tools/wire-cli.sh @@ -25,37 +25,79 @@ # version-guard.sh 的相依版本檢查同法,只是沙箱換成暫時的 HOME(註冊檔路徑由 $HOME 決定): # 相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、 # 逃生門,再加一條回歸——多行縮排的 manifest,jsc.requires 的最後一個鍵也要解得到。 +# skill-name.sh 與 deny.sh 各有一組:前者把五支 CLI 的真實負載形態各餵一次,驗解得出技能名, +# 再加三條要解不出來的;後者驗四種阻擋形態各自出得來。最後一組是跨 CLI 貫通——把五支 CLI 的 +# 負載直接餵進 restart-gate.sh,驗「解析 → 判定 → 輸出形態」整條串得起來。這一組不能省: +# 前兩組分開看都會顯示正常,中間接不上照樣是全程放行,那正是先前三支 CLI 失效的樣子。 # # smoke 自己數結果行並自我斷言:`lines{數量}` 那一行印的是其後 `[jsc]` 結果行的實際條數, # 與腳本內宣告的預期條數逐類比對,不符就回非零。數字寫在腳本裡、由腳本自己印,散文引用那一行 # 就好,不必在 SKILL.md 或 README 各抄一份——抄了就會在加減判定路徑時漂移。 # -# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加): -# claude — 什麼都不用寫,hooks.json 已自動接線九支 hook -# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時, +# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc 段落,不會重複疊加): +# claude — 什麼都不用寫,hooks/hooks.json 已自動接線九支 hook(PreToolUse matcher Skill) +# codex — pre-tool hook 寫在 .codex-plugin/plugin.json 的 hooks 鍵,做 Codex 專屬覆寫 +# (PreToolUse matcher Bash,接 restart-gate.sh 與 version-guard.sh)。 +# Claude 用的 hooks/hooks.json 完全不動:那一份是 Skill matcher,而 Codex +# 根本沒有 Skill 這個工具,技能是模型自己用 Bash 讀 SKILL.md 載入的。 +# 另外在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時, # 結束時收尾掃一次註解範圍與繁中編碼);在 config.toml 設 notify(每輪補 # session-timer.sh start 再 mark,最後 comment-scope.sh sweep 與 # lang-guard.sh sweep 掃整個工作區,JSC_CLI=codex);在 AGENTS.md 附加 # STE100、註解範圍與繁中編碼規則段落(prompt 降級) -# copilot — 在 shell rc 檔加上 copilot 別名,轉呼叫 tools/jsc-wrap.sh copilot(結束時 -# 收尾掃一次註解範圍與繁中編碼);在 copilot-instructions.md 附加 STE100、 -# 註解範圍與繁中編碼規則段落 -# antigravity — 在 shell rc 檔加上 agy 別名,轉呼叫 tools/jsc-wrap.sh antigravity(同樣收尾 -# 掃一次);在全域規則檔附加 STE100、註解範圍與繁中編碼規則段落 -# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(每輪 mark 加 STE100、 -# 註解範圍與繁中編碼規則,再 comment-scope.sh sweep 與 lang-guard.sh sweep -# 掃整個工作區)與 jsc-hooks-session-start.json(sessionStart 開始計時), -# 皆帶 JSC_CLI=kiro +# copilot — pre-tool hook 併進 ~/.copilot/settings.json 的頂層 hooks 鍵(PreToolUse +# matcher skill,小寫)。設定只認那個鍵:$COPILOT_HOME/hooks/ 底下放的是 hook +# 要跑的**腳本**,不是設定,寫進去的設定檔一次都不會被讀。事件名只寫一種 +# 大小寫:copilot 兩種都吃,兩種同時存在會把同一支 hook 跑兩次。 +# 那份檔案同時裝著 enabledPlugins 與 extraKnownMarketplaces,弄壞會讓外掛整批 +# 失效,所以**合併不覆寫**:寫前備份,只動 jsc 自己那幾筆,寫後回讀核對最上層鍵 +# 與別人的條目,對不上就還原。另外在 shell rc 檔加上 copilot 別名;指引檔寫在 +# $COPILOT_HOME 底下(舊接線寫在 ~/.config/copilot/,那個位置從來不會被載入) +# antigravity — pre-tool hook 寫在 ~/.gemini/config/hooks.json 的 jsc 段落。那個檔案的最上層 +# 一個安裝來源一個命名空間鍵,寫 jsc 那一個不會動到別人的段落;plugin.json +# 不能宣告 hook,所以只有這一個位置。matcher 是 ^view_file$,錨點不能省 +# (省了會連 view_file_outline 一起命中);另接 PreInvocation,攔斜線指令那條 +# 不產生工具呼叫的路。另外在 shell rc 檔加上 agy 別名、在全域規則檔附加規則段落 +# kiro — hook 宣告寫在 ~/.kiro/agents/jsc.json 的 hooks 鍵(agentSpawn、 +# userPromptSubmit、stop,欄位是 command 與 timeout_ms),並把 settings/cli.json +# 的 chat.defaultAgent 設成 jsc。同一份 agent 檔還要寫 resources 的兩層 +# skill:// glob 與明列的 tools。合法事件只有 agentSpawn、userPromptSubmit、 +# preToolUse、postToolUse、stop 五個;sessionStart 與 sessionEnd 都不合法, +# 欄位也沒有 on、run、env。.kiro/hooks/ 目錄不在 kiro 的設定目錄常數裡,一份都 +# 不會被讀,舊接線寫在那裡等於沒接;purge 仍會清掉那個目錄,免得誤導下一個人。 +# 接線後用 kiro-cli agent validate 驗一次,**判準是輸出不是結束碼**:那支指令 +# 四種情況一律回 0,看結束碼會做出一支永遠通過的檢查。而且最上層的未知鍵會被 +# 靜默忽略——舊版的 on/run/env 就是這樣一個錯都不報、卻什麼都沒做,所以 +# 「validate 通過」不等於「接線生效」,形狀要另外驗 # -# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護: -# claude 九支 hook 全接,回報 wired -# codex、copilot、antigravity、kiro 只有別名、notify 或規則檔,接不上 PreToolUse、PostToolUse -# 與 UserPromptSubmit,版本前置檢查、部署後重啟閘門、寫入與 -# 提交閘門與 SDLC 模型鎖都沒接上,一律回報 degraded 並在 -# reason 講明。 -# 重啟閘門在這四個 CLI 上一次技能呼叫都擋不下來:狀態檔照樣 -# 寫、下次工作階段開始照樣清,只是中間沒有任何判定點,重啟 -# 只能靠 /jsc-cli:deploy 收尾的提示自己動手 +# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護,也不得再說「沒有 pre-tool hook」—— +# 五支裡有四支都有,先前失效的原因是接錯位置,不是沒有位置可接: +# claude 九支 hook 全接,回報 wired +# codex、copilot、antigravity 版本前置檢查與部署後重啟閘門都擋得下來,回報 wired。 +# SDLC 模型鎖仍是技能步驟檢查,write-guard.sh 三種模式尚未接線, +# 註解範圍與繁中編碼仍是 sweep,reason 要逐項講明。 +# antigravity 的擋人一律走 stdout 的 deny JSON:那支 CLI 的結束碼 +# 語意兩邊文件都沒寫,靠結束碼會變成無聲失效 +# kiro 回報 degraded。技能叫用是 ResolveSkill 這個 agent 內部請求, +# preToolUse 攔不到,userPromptSubmit 的非零結束碼也不會擋下那 +# 一輪,所以兩道閘門只能以 stdout 注入警告。這是 CLI 的限制, +# 不是接線缺漏 +# +# 驗證等級要逐支標明,「形狀」與「觸發」是兩件事,不得混為一談: +# claude 形狀實證、觸發實證 +# codex 形狀實證(config.toml 的 [hooks.state] 以 {事件}:{群組}:{條目} 兩層索引登記, +# 只有 Grouped 結構才會這樣;manifest 的 hooks 是路徑字串,出自執行檔內嵌的 +# plugin-json-spec.md)。觸發未驗證 +# antigravity 形狀實證(接線後 agy -p "/hooks" 四條全載入,matcher 帶錨點,第三方段落完好)。 +# 觸發未驗證,本機對話 quota 用盡 +# copilot 形狀未證。settings.json 沒有唯讀的列出管道,位置與條目形態出自 +# copilot help config 的說明與機器上既有的第三方實例。觸發未驗證 +# kiro 形狀實證(kiro-cli agent validate 通過,並以 sessionStart、hooks 裡放 on/run、 +# 缺 command 三種反證確認它真的在判別)。觸發部分實證:agentSpawn 與 +# userPromptSubmit 實跑觸發過,preToolUse 與 stop 未驗證,模型額度用盡 +# 另有兩項未驗證:kiro 的 resources 兩層 glob 能不能真的修好技能可見性沒驗過(要模型跑得動才 +# 列得出技能);四支非 claude 的 CLI 上 write-guard.sh 三種模式與 SDLC 模型鎖仍未接線,那是 +# 還沒做,不是驗不過。smoke 驗的是接線內容與腳本邏輯,觸發不在它的範圍內。 # # 註解範圍與繁中編碼掃描每個 CLI 的時機都不同(兩支腳本接在同一批位置),回報時不得寫成五支一樣: # claude 掛在 PostToolUse,寫完哪個檔就掃哪個,逐檔即時 @@ -67,14 +109,20 @@ # 一律跟著 comment-scope.sh 接在同一批位置,兩支的掃描時機表完全一致。 # 所有 jsc 標記段落都採整段重寫,重跑等同先移除舊內容再重裝 # -# 寫入後自我驗證,通過才回報成功:每個寫過的檔案重新讀一次,確認標記段落存在且落在 -# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對 -# 且 on、run 在最上層),內容也要涵蓋這次該接上的每一支腳本(含 comment-scope.sh sweep -# 與 lang-guard.sh sweep)。 +# 寫入後自我驗證,通過才回報成功:每個寫過的檔案重新讀一次,確認段落存在且落在正確位置 +# (codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對且 hooks、 +# resources、tools 都在最上層),內容也要涵蓋這次該接上的每一支腳本(含 comment-scope.sh sweep +# 與 lang-guard.sh sweep)。matcher 本身也單獨驗一次:鍵在、matcher 卻錯的形態最難查—— +# codex 要 Bash、copilot 要小寫 skill、antigravity 要帶錨點的 ^view_file$、kiro 的 resources +# 要有兩層 glob。這四項少驗任何一項,都會出現「回報接好了、實際一次都不會被叫用」。 +# 第五項是 CLI 代號:四支非 claude 的接線命令都要以 JSC_CLI={代號} 前綴自帶代號。閘門靠代號才 +# 取得到技能名與阻擋形態,代號取不到就一律安靜放行——形態跟 matcher 寫錯一模一樣,都是 +# 「設定完全正確、卻一次都擋不下來」。這一項在接線、status 與 smoke 三處各驗一次。 # 腳本說寫好了卻寫錯位置或少接一支,是最難查的失敗,所以驗證放在腳本裡。 # # 輸出: 第一行固定為 `status=... reason=...`(可供程式判讀),其後為人類可讀的繁中說明。 -# 結束碼(接線): 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查) +# 結束碼(接線): 0=wired(claude、codex、copilot、antigravity) +# 1=degraded(kiro,CLI 本身擋不下技能叫用) # 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過) # 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報) # 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed @@ -144,6 +192,395 @@ lang_guard_text() { sh "$HOOKS/lang-guard.sh" prompt 2>/dev/null | sed '/^exit / # 三段合在一個標記段落裡,purge 與重跑接線都是整段處理,不必各自再記一組標記。 rules_text() { ste100_text; comment_scope_text; lang_guard_text; } +# --- 各 CLI 的接線位置:路徑只留這一份 --- +# 接線、移除、盤點三段都呼叫同一支函式取路徑。三邊各寫一次字面路徑,改了一處就會出現 +# 「接線寫這裡、盤點看那裡」的假接線報告,那種錯最難查。 +codex_manifest() { printf '%s' "$ROOT/.codex-plugin/plugin.json"; } +codex_hooks_file() { printf '%s' "$ROOT/hooks/codex-hooks.json"; } +# manifest 裡那個鍵的值。Codex 的 plugin manifest 規格寫得很清楚:hooks 跟 skills 一樣是 +# **路徑字串**(`"hooks": "./hooks.json"`),不是內嵌物件。寫成內嵌物件解不出來。 +CODEX_HOOKS_REL="./hooks/codex-hooks.json" +copilot_home() { printf '%s' "${COPILOT_HOME:-$HOME/.copilot}"; } +# copilot 的 hook 設定在 settings.json 的頂層 hooks 鍵,是**內嵌定義**、以事件名當鍵。 +# 不是 $COPILOT_HOME/hooks/*.json——那底下放的是 hook 要執行的**腳本**,不是設定; +# 也不是 config.json,那份檔案第一行自己就寫著「User settings belong in settings.json」。 +copilot_settings() { printf '%s' "${JSC_COPILOT_SETTINGS:-$(copilot_home)/settings.json}"; } +# 指引檔一定要在 $COPILOT_HOME 底下。舊接線寫在 ~/.config/copilot/,那個位置 copilot 從來不讀。 +copilot_instructions() { + printf '%s' "${JSC_COPILOT_INSTRUCTIONS:-$(copilot_home)/copilot-instructions.md}" +} +antigravity_hooks_file() { printf '%s' "${JSC_ANTIGRAVITY_HOOKS:-$HOME/.gemini/config/hooks.json}"; } +kiro_home() { printf '%s' "${KIRO_HOME:-$HOME/.kiro}"; } +kiro_agent_file() { printf '%s/agents/jsc.json' "$(kiro_home)"; } +kiro_cli_settings() { printf '%s/settings/cli.json' "$(kiro_home)"; } + +# --- 各 CLI 的 hook 宣告內容 --- + +# codex:manifest 的 hooks 鍵指到一份 Codex 專屬的 hook 檔,那個鍵是路徑字串。 +codex_hooks_key_block() { + printf ' "hooks": "%s"' "$CODEX_HOOKS_REL" +} + +# Codex 專屬的 hook 檔內容,**從 hooks/hooks.json 推導出來**,不是另外手寫一份。 +# 只改一件事:matcher Skill 換成 Bash。Codex 沒有 Skill 這個工具,技能是模型自己用 Bash 讀 +# SKILL.md 載入的,所以那一組 hook 要擋在 Bash 那一次;先前用 Skill matcher,一次都沒被叫用過。 +# +# 為什麼用推導、不手寫第二份:manifest 的 hooks 鍵是**覆寫**,codex 只會讀它指到的那一份, +# 手寫就等於把 SessionStart、UserPromptSubmit、Stop 那幾組全部漏掉,而且從此兩份各自漂移。 +# 推導的話 hooks/hooks.json 仍是唯一真實來源,那邊加一支 hook,這邊重跑接線就跟著有。 +# Claude 讀的還是原本那份 hooks/hooks.json,一個位元組都沒動。 +# +# 推導改兩件事,第二件跟 matcher 一樣不能省:每一條命令都補上 JSC_CLI=codex。 +# 閘門是靠 lib.sh 的 cli_name() 認出「現在是哪一支 CLI」,再拿那個代號去 skill-name.sh 取技能名、 +# 去 deny.sh 取阻擋形態。JSC_CLI 沒設時 cli_name() 只回 unknown,skill-name.sh 認不得代號就印空字串, +# 兩道閘門一律安靜放行——設定寫得完全正確、matcher 也對,卻一次都擋不下來,跟先前那個接錯位置的 +# 缺陷長得一模一樣。別名那條路(jsc-wrap.sh 會 export JSC_CLI)只在使用者從互動 shell 走別名啟動時 +# 才成立,接線不能靠它;kiro 的 hook 早就是這樣用指令前綴自帶代號的,這裡跟它一致。 +codex_derive_hooks() { # $1=來源 hooks.json + [ -f "$1" ] || return 1 + sed -e 's/"matcher": "Skill"/"matcher": "Bash"/g' \ + -e "s|sh -c 'root=|sh -c 'JSC_CLI=codex; export JSC_CLI; root=|g" "$1" +} + +# copilot 的 hook 條目。事件名只寫 PascalCase 一種:copilot 兩種大小寫都吃,兩種同時存在 +# 會把同一支 hook 跑兩次。matcher 是小寫的 skill,那是 copilot 專用技能工具的工具名。 +# 條目形態照這台機器上既有的那一筆第三方設定(type、bash、timeoutSec),不套 Claude 的形狀。 +# 指令前綴帶 JSC_CLI=copilot:閘門靠 cli_name() 認代號才取得到技能名與阻擋形態,沒設就一律 +# 安靜放行,matcher 對、位置對、卻一次都擋不下來。別名那條路只在走別名啟動時才成立,不能靠它。 +copilot_hook_entries() { + cat < 0 || + index(t, "restart-gate.sh") > 0 || + index(t, "version-guard.sh") > 0) + } + { s = s $0 "\n" } + END { + n = length(s) + if (mode == "wire") { + while ((getline line < newfile) > 0) newtext = newtext (newtext == "" ? "" : "\n") line + close(newfile) + if (newtext == "") exit 1 + } + # 一、找出最上層的 hooks 鍵,取它那個值的字元區間。 + i = 1; depth = 0; hs = 0; he = 0 + while (i <= n) { + c = substr(s, i, 1) + if (c == "\"") { + buf = ""; j = i + 1 + while (j <= n) { + c = substr(s, j, 1) + if (c == "\\") { j += 2; continue } + if (c == "\"") break + buf = buf c; j++ + } + if (j > n) exit 1 + i = j + 1 + k = skip_ws(s, i, n) + if (depth == 1 && buf == "hooks" && substr(s, k, 1) == ":") { + hs = skip_ws(s, k + 1, n) + he = skip_value(s, k + 1, n) + if (he == 0) exit 1 + break + } + continue + } + if (c == "{" || c == "[") depth++ + else if (c == "}" || c == "]") { depth--; if (depth < 0) exit 1 } + i++ + } + if (hs == 0) exit 2 # 沒有 hooks 鍵,交給呼叫端另外新增 + # 二、走訪 hooks 物件裡的每個事件,逐條篩掉 jsc 自己那幾筆。 + cnt = 0; pre = 0 + i = hs + 1 + while (i < he) { + i = skip_ws(s, i, n) + if (substr(s, i, 1) == "}") break + if (substr(s, i, 1) != "\"") { i++; continue } + ev = ""; j = i + 1 + while (j <= n) { + c = substr(s, j, 1) + if (c == "\\") { j += 2; continue } + if (c == "\"") break + ev = ev c; j++ + } + i = skip_ws(s, j + 1, n) + if (substr(s, i, 1) != ":") continue + av = skip_ws(s, i + 1, n) + ae = skip_value(s, i + 1, n) + if (ae == 0) exit 1 + body = "" + if (substr(s, av, 1) == "[") { + p = av + 1 + while (p < ae) { + p = skip_ws(s, p, n) + if (substr(s, p, 1) == "]") break + es = p + ee = skip_value(s, p, n) + if (ee == 0) exit 1 + elem = substr(s, es, ee - es) + if (!isjsc(elem)) body = body (body == "" ? "" : ",\n") " " elem + p = skip_ws(s, ee, n) + if (substr(s, p, 1) == ",") p++ + } + } + cnt++ + evname[cnt] = ev + evbody[cnt] = body + if (ev == "PreToolUse") pre = cnt + i = skip_ws(s, ae, n) + if (substr(s, i, 1) == ",") i++ + } + # 三、把 jsc 的條目接回 PreToolUse;那個事件本來不存在就新增一個。 + if (mode == "wire") { + if (pre == 0) { cnt++; evname[cnt] = "PreToolUse"; evbody[cnt] = ""; pre = cnt } + evbody[pre] = evbody[pre] (evbody[pre] == "" ? "" : ",\n") newtext + } + # 四、重組 hooks 的值。條目被清空的事件整個拿掉,免得留下空陣列。 + out = "{" + first = 1 + for (x = 1; x <= cnt; x++) { + if (evbody[x] == "") continue + out = out (first ? "" : ",") "\n \"" evname[x] "\": [\n" evbody[x] "\n ]" + first = 0 + } + out = out (first ? "}" : "\n }") + printf "%s%s%s", substr(s, 1, hs - 1), out, substr(s, he) + } + ' "$1" +} + +# antigravity:~/.gemini/config/hooks.json 的最上層是命名空間,一個安裝來源一個鍵, +# 所以 jsc 的段落就是 "jsc" 這個鍵,寫它不會動到別人的段落。 +# matcher 一定要寫錨點 ^view_file$:沒有錨點會連 view_file_outline 一起命中。 +# 另外接 PreInvocation:斜線指令與預載技能會把 SKILL.md 全文直接注入訊息,一個工具呼叫都不產生, +# PreToolUse 那一層根本看不到,只有 PreInvocation 攔得到那條路。 +# +# 兩個事件的結構**不一樣**,不能寫成同一種形狀(執行檔內嵌文件的「Supported Event Types」表): +# PreToolUse、PostToolUse Grouped——handler 要用 matcher 加 hooks 包一層 +# PreInvocation、PostInvocation、Stop +# Flat——handler 物件直接排在陣列裡 +# 這一條是實測踩出來的:PreToolUse 寫成 Flat 時 antigravity **靜默丟棄整個事件**, +# hook 名稱照樣登記,連 actions 鍵都不生成,不報任何錯,設定檔看起來也完全正常。 +# 所以 matcher 要留在 group 那一層,不是 handler 那一層。 +# 每條指令都帶 JSC_CLI=antigravity:這支的阻擋形態是 stdout 的 deny JSON,跟結束碼那三支不一樣, +# 代號取不到時 deny.sh 會退回「stderr 加 exit 2」的保守預設,那個形態 antigravity 根本不認, +# 等於判定擋下、CLI 卻收不到拒絕。技能名也一樣要靠代號才解得出來。別名那條路不能當接線的依據。 +antigravity_hooks_block() { + cat </dev/null +} + +# 反過來,PreInvocation 必須維持 Flat:那一組事件不吃 matcher 與 hooks 包裝, +# 跟著改成 Grouped 一樣會被丟掉。兩個方向都要驗,只驗一邊修好一半照樣失效。 +# $1=檔案 +antigravity_flat_ok() { + [ -f "$1" ] || return 1 + awk ' + /"PreInvocation"[[:space:]]*:/ { inflat = 1; next } + inflat && /"hooks"[[:space:]]*:[[:space:]]*\[/ { bad = 1 } + inflat && /^[[:space:]]*\][[:space:]]*$/ { inflat = 0 } + END { exit(bad ? 1 : 0) } + ' "$1" 2>/dev/null +} + +# kiro:hook 宣告只認 agent 設定檔的 hooks 鍵,.kiro/hooks/ 目錄不被讀。 +# 欄位只有 command、matcher、timeout_ms、max_output_size、cache_ttl_seconds,沒有 on、run、env, +# 所以 JSC_CLI 改用指令前綴帶進去。 +# resources 要寫兩層 glob:預設只掃 skills/*/SKILL.md 一層,jsc 的技能在 jsc-{domain}/{name}/ 第二層, +# 少了那一條,這個 agent 一支 jsc 技能都看不到。一層那一條照樣留著,別人的技能不能被我們弄不見。 +# tools 一定要明寫:自訂 agent 沒宣告 tools 時可用工具會受限,模型連讀檔都做不到。 +kiro_agent_json() { + cat </dev/null 2>&1 || return 0 + kiro_validate_output_ok "$("$_kb" agent validate --path "$1" 2>&1)" +} + +# agent 設定檔的形狀。合法事件只有 agentSpawn、userPromptSubmit、preToolUse、postToolUse、stop +# 五個;sessionStart 與 sessionEnd 都會被 validate 報錯,欄位也沒有 on、run、env。 +# 這一項單獨驗是因為**未知的頂層鍵會被靜默忽略**:舊版把 on/run/env 寫在最上層,validate +# 一個錯都不報,那份檔案卻什麼都沒做。檔案合法不等於接線生效。$1=檔案 +kiro_agent_shape_ok() { + [ -f "$1" ] || return 1 + grep -qE '"(sessionStart|sessionEnd)"' "$1" 2>/dev/null && return 1 + grep -qE '^[[:space:]]*"(on|run|env)"[[:space:]]*:' "$1" 2>/dev/null && return 1 + grep -qF '"agentSpawn"' "$1" 2>/dev/null || return 1 + grep -qF '"userPromptSubmit"' "$1" 2>/dev/null || return 1 + grep -qF '"stop"' "$1" 2>/dev/null || return 1 + grep -qF '"timeout_ms"' "$1" 2>/dev/null || return 1 + grep -qF '/skills/*/*/SKILL.md' "$1" 2>/dev/null || return 1 + return 0 +} + # 寫入 CLI 設定時用版本無關的穩定路徑。執行中的腳本仍從自己的 repo 讀規則,避免開發中 # 的檔案和剛建立的連結互相踩到;只有寫進外部設定的命令改走 current 連結。 ensure_stable_root() { @@ -329,6 +766,66 @@ json_top_key() { # 驗證:JSON 語法成對(括號收齊、字串收尾)。鍵不管。 json_pairs_ok() { json_top_key "$1" __no_such_key__ pairs; } +# 印出所有最上層鍵,一行一個,已排序。改寫別人的設定檔前後各取一次來比對: +# 只驗「我們要的鍵在不在」看不出「別的鍵被吃掉了」,而後者才是會讓外掛整批失效的那種錯。 +json_top_keys() { # $1=檔案 + [ -f "$1" ] || return 0 + awk ' + { s = s $0 "\n" } + END { + n = length(s); depth = 0; i = 1 + while (i <= n) { + c = substr(s, i, 1) + if (c == "\"") { + buf = ""; i++ + while (i <= n) { + c = substr(s, i, 1) + if (c == "\\") { i += 2; continue } + if (c == "\"") { i++; break } + buf = buf c; i++ + } + j = i + while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++ + if (substr(s, j, 1) == ":" && depth == 1) print buf + continue + } + if (c == "{" || c == "[") depth++ + else if (c == "}" || c == "]") depth-- + i++ + } + }' "$1" 2>/dev/null | sort +} + +# 印出 settings.json 的 hooks 底下**不屬於 jsc** 的條目,正規化成一行一筆。 +# 改寫前後各取一次比對,才驗得出「別人的 hook 一筆都沒被動到」。 +copilot_foreign_entries() { # $1=檔案 + [ -f "$1" ] || return 0 + sed -n '/"hooks"/,$p' "$1" 2>/dev/null \ + | grep -E '"(bash|command)"[[:space:]]*:' \ + | grep -vE 'jsc-hooks|restart-gate\.sh|version-guard\.sh' \ + | tr -d ' \t' | sort +} + +# copilot 的 hook 接上了沒:要有 PreToolUse、小寫 skill 的 matcher,兩道閘門都在。 +# 位置錯了也會回 present——這正是上一版踩到的:檔案寫進 $COPILOT_HOME/hooks/, +# 那底下是腳本目錄不是設定目錄,檔案好端端在那裡,copilot 一次都沒讀過。 +# 所以這支函式只認 settings.json 這一個位置。$1=檔案 +copilot_hooks_ok() { + [ -f "$1" ] || return 1 + json_top_key "$1" hooks || return 1 + grep -qF '"matcher": "skill"' "$1" 2>/dev/null || return 1 + grep -qF '"PreToolUse"' "$1" 2>/dev/null || return 1 + grep -qF 'restart-gate.sh' "$1" 2>/dev/null || return 1 + grep -qF 'version-guard.sh' "$1" 2>/dev/null || return 1 + return 0 +} + +# 事件名只准一種大小寫:copilot 兩種都吃,兩種同時存在會把同一支 hook 跑兩次。$1=檔案 +copilot_single_case_ok() { + [ -f "$1" ] || return 1 + ! grep -qE '"(preToolUse|postToolUse|sessionStart|userPromptSubmit)"' "$1" 2>/dev/null +} + # 找出已存在的 shell rc 檔(purge 用)。rc_files 找不到會建立 ~/.bashrc,移除流程不建檔: # 為了清 hook 而生出一個新檔案,是把環境弄得更亂,不是更乾淨。 rc_files_existing() { @@ -434,12 +931,15 @@ remove_toml_root_key() { mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; } } -# 刪掉 JSON 最上層的 hooks 鍵(含後面多餘的逗號),逐字元追蹤引號與括號深度。 +# 刪掉 JSON 最上層的某個鍵(含後面多餘的逗號),逐字元追蹤引號與括號深度。 # jq 在目標機器上不保證存在,所以要有這條純 awk 的路;只用 sed 刪不了嵌套的 {...}。 # 追蹤引號是必要的:字串裡的 { 與 } 不算深度,漏算就會把整段設定切壞。 -# $1=檔案,結果印到標準輸出;解析不出來(括號不成對、字串沒收尾)就 exit 1,不輸出半份檔案。 +# 鍵名做成參數,是因為現在要刪的最上層鍵不只一種:claude 與 codex 刪 hooks、antigravity 刪 +# jsc 那個命名空間、kiro 刪 chat.defaultAgent。同一套字元掃描抄三份只會三邊漂移。 +# $1=檔案 $2=鍵名(省略時為 hooks),結果印到標準輸出; +# 解析不出來(括號不成對、字串沒收尾)就 exit 1,不輸出半份檔案。 awk_del_hooks() { - awk ' + awk -v delkey="${2:-hooks}" ' function skip_string(s, i, n, c) { i++ while (i <= n) { @@ -485,7 +985,7 @@ awk_del_hooks() { i = j + 1 j = i while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++ - if (depth == 1 && buf == "hooks" && substr(s, j, 1) == ":") { + if (depth == 1 && buf == delkey && substr(s, j, 1) == ":") { i = skip_value(s, j + 1, n) if (i == 0) exit 1 j = i @@ -514,19 +1014,51 @@ awk_del_hooks() { }' "$1" } -# 刪掉設定檔最上層的 hooks 鍵:有 jq 就用 jq,沒有就走 awk_del_hooks。$1=檔案 +# 刪掉設定檔最上層的某個鍵:有 jq 就用 jq,沒有就走 awk_del_hooks。$1=檔案 $2=鍵名(省略為 hooks) json_del_hooks() { - _f="$1" + _f="$1"; _k="${2:-hooks}" [ -f "$_f" ] || return 0 if command -v jq >/dev/null 2>&1; then - jq 'del(.hooks)' "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + jq --arg k "$_k" 'del(.[$k])' "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } else - awk_del_hooks "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + awk_del_hooks "$_f" "$_k" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } fi [ -s "$_f.jsc-tmp" ] || { rm -f "$_f.jsc-tmp"; return 1; } mv "$_f.jsc-tmp" "$_f" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } } +# 把一個最上層鍵整段寫進 JSON 設定檔(已存在就先刪掉再寫,所以重跑等同先移除再重裝)。 +# $1=檔案 $2=鍵名 $3=完整的鍵值文字(例如 `"hooks": { ... }`,不含結尾逗號) +# 插在第一個大括號那一行之後,不插在檔尾:插檔尾就要先判斷最後一個成員有沒有逗號,判錯就做出壞掉的 +# JSON。空物件另外走一條路:那時插進去再補逗號會做出 `{ ...,}`,同樣是壞 JSON。 +# 內容不經過 awk -v:awk 會把賦值字串裡的 \" 當成跳脫序列解掉,hook 命令裡的 \" 是 JSON 語法的一部分, +# 被解掉就做出一份解不開的設定檔——而且檔案看起來還很正常,只有 CLI 讀的時候才靜靜失敗。 +# 所以這裡改用純 shell 的行操作組檔,內容一個位元組都不轉換。 +json_put_top_key() { + _f="$1"; _k="$2"; _blk="$3" + dir=$(dirname "$_f") + mkdir -p "$dir" 2>/dev/null || return 1 + [ -f "$_f" ] || printf '{}\n' > "$_f" 2>/dev/null || return 1 + json_pairs_ok "$_f" || return 1 + if json_top_key "$_f" "$_k"; then + json_del_hooks "$_f" "$_k" || return 1 + fi + if [ "$(tr -d ' \t\n\r' < "$_f")" = "{}" ]; then + printf '{\n%s\n}\n' "$_blk" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + else + # 只認「第一行就是那個大括號」這種版面。其餘版面一律拒絕,不硬插:插錯位置做出來的壞 JSON + # 比不寫更難查,呼叫端收到失敗至少會停下來講出檔名。 + [ "$(head -n1 "$_f" | tr -d ' \t\r')" = "{" ] || return 1 + { head -n1 "$_f" + printf '%s,\n' "$_blk" + tail -n +2 "$_f" + } > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } + fi + json_pairs_ok "$_f.jsc-tmp" || { rm -f "$_f.jsc-tmp"; return 1; } + json_top_key "$_f.jsc-tmp" "$_k" || { rm -f "$_f.jsc-tmp"; return 1; } + mv "$_f.jsc-tmp" "$_f" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; } +} + # --- 備份:先備份才准移除 --- BACKUP_DIR="" @@ -668,14 +1200,31 @@ if [ "$action" = purge ]; then || pfail "無法從 $agents 移除 jsc-hooks 標記段落" has_block "$agents" "" && pfail "$agents 移除後仍讀得到 jsc-hooks 標記段落" fi - purged "已移除 config.toml 的標記段落與根層 notify、rc 檔的 jsc-hooks 段落、AGENTS.md 的規則段落" + # Codex 專屬覆寫在 plugin manifest 的 hooks 鍵,那是接線寫進去的,purge 要拿得掉。 + cx_manifest=$(codex_manifest) + cx_hooks=$(codex_hooks_file) + if [ -f "$cx_hooks" ]; then + backup_file "$cx_hooks" || pfail "無法備份 $cx_hooks,沒有備份就不移除" + rm -f "$cx_hooks" 2>/dev/null || pfail "無法刪除 $cx_hooks" + [ -f "$cx_hooks" ] && pfail "$cx_hooks 刪除後檔案還在" + fi + if [ -f "$cx_manifest" ] && json_top_key "$cx_manifest" hooks; then + json_pairs_ok "$cx_manifest" || pfail "$cx_manifest 不是成對的 JSON,讀不懂就不動它" + backup_file "$cx_manifest" || pfail "無法備份 $cx_manifest,沒有備份就不移除" + json_del_hooks "$cx_manifest" hooks || pfail "無法從 $cx_manifest 刪除 hooks 鍵" + json_pairs_ok "$cx_manifest" || pfail "$cx_manifest 刪除 hooks 鍵後 JSON 括號不成對" + ! json_top_key "$cx_manifest" hooks || pfail "$cx_manifest 刪除後最上層仍有 hooks 鍵" + fi + purged "已移除 config.toml 的標記段落與根層 notify、rc 檔的 jsc-hooks 段落、AGENTS.md 的規則段落、hooks/codex-hooks.json 與 .codex-plugin/plugin.json 的 hooks 鍵" echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才真的失效。" + echo "[jsc] codex:pre-tool hook 的覆寫是 $cx_manifest 的 hooks 鍵加上它指到的 $cx_hooks,兩個都已移除;Claude 用的 hooks/hooks.json 隨 plugin 提供,purge 動不到,也不該動。" exit 0 ;; copilot) bin=$(cli_bin copilot) command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔" - instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" + instr=$(copilot_instructions) + cp_settings=$(copilot_settings) rclist=$(mktemp) || pfail "無法建立暫存檔" rc_files_existing > "$rclist" while IFS= read -r rc; do @@ -693,8 +1242,32 @@ if [ "$action" = purge ]; then || pfail "無法從 $instr 移除 jsc-hooks 標記段落" has_block "$instr" "" && pfail "$instr 移除後仍讀得到 jsc-hooks 標記段落" fi - purged "已移除 rc 檔的 jsc-hooks:copilot 段落與指引檔的規則段落" + # settings.json 只挑掉 jsc 自己那幾筆條目,別人的事件與所有其他頂層鍵原樣保留。 + # 整份刪掉那個鍵會把第三方的 SessionStart 一起清掉,那超出這道移除該負責的範圍; + # 而弄壞那份檔案會讓十個外掛整批失效,所以驗不過就還原。 + if [ -f "$cp_settings" ] && json_top_key "$cp_settings" hooks; then + json_pairs_ok "$cp_settings" || pfail "$cp_settings 不是成對的 JSON,讀不懂就不動它" + cp_top_before=$(json_top_keys "$cp_settings") + cp_others_before=$(copilot_foreign_entries "$cp_settings") + backup_file "$cp_settings" || pfail "無法備份 $cp_settings,沒有備份就不移除" + cp_tmp=$(mktemp) || pfail "無法建立暫存檔" + if copilot_merge_hooks "$cp_settings" purge > "$cp_tmp" 2>/dev/null && [ -s "$cp_tmp" ]; then + cat "$cp_tmp" > "$cp_settings" 2>/dev/null || { rm -f "$cp_tmp"; pfail "無法寫入 $cp_settings"; } + else + rm -f "$cp_tmp"; pfail "$cp_settings 的 hooks 鍵解析不出來,沒有動它" + fi + rm -f "$cp_tmp" + json_pairs_ok "$cp_settings" || pfail "$cp_settings 移除後不是成對的 JSON" + [ "$(json_top_keys "$cp_settings")" = "$cp_top_before" ] \ + || pfail "$cp_settings 的最上層鍵有增減,enabledPlugins 與 extraKnownMarketplaces 一個都不能掉" + [ "$(copilot_foreign_entries "$cp_settings")" = "$cp_others_before" ] \ + || pfail "$cp_settings 裡別人的 hook 條目被動到了" + grep -qF 'restart-gate.sh' "$cp_settings" 2>/dev/null \ + && pfail "$cp_settings 移除後仍讀得到 jsc 的 hook 條目" + fi + purged "已移除 rc 檔的 jsc-hooks:copilot 段落、指引檔的規則段落與 settings.json 的 hooks 鍵底下 jsc 那幾筆條目" echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才真的失效。" + echo "[jsc] copilot:settings.json 裡別人的 hook 條目與所有其他頂層鍵原樣保留,已回讀核對。" exit 0 ;; antigravity) @@ -718,8 +1291,19 @@ if [ "$action" = purge ]; then || pfail "無法從 $rules 移除 jsc-hooks 標記段落" has_block "$rules" "" && pfail "$rules 移除後仍讀得到 jsc-hooks 標記段落" fi - purged "已移除 rc 檔的 jsc-hooks:antigravity 段落與全域規則檔的規則段落" + # hooks.json 只刪 jsc 那個命名空間鍵,別人的段落留著:那個檔案的最上層一個安裝來源一個鍵, + # 整份刪掉會把別人的整合一起清掉,那超出這道移除該負責的範圍。 + ag_hooks=$(antigravity_hooks_file) + if [ -f "$ag_hooks" ] && json_top_key "$ag_hooks" jsc; then + json_pairs_ok "$ag_hooks" || pfail "$ag_hooks 不是成對的 JSON,讀不懂就不動它" + backup_file "$ag_hooks" || pfail "無法備份 $ag_hooks,沒有備份就不移除" + json_del_hooks "$ag_hooks" jsc || pfail "無法從 $ag_hooks 刪除 jsc 段落" + json_pairs_ok "$ag_hooks" || pfail "$ag_hooks 刪除 jsc 段落後 JSON 括號不成對" + ! json_top_key "$ag_hooks" jsc || pfail "$ag_hooks 刪除後最上層仍有 jsc 段落" + fi + purged "已移除 rc 檔的 jsc-hooks:antigravity 段落、全域規則檔的規則段落與 hooks.json 的 jsc 段落" echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才真的失效。" + echo "[jsc] antigravity:$ag_hooks 底下別的命名空間段落沒有動過。" exit 0 ;; kiro) @@ -734,8 +1318,26 @@ if [ "$action" = purge ]; then left=$(find "$hookdir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ') [ "$left" = 0 ] || pfail "$hookdir 底下還有 $left 個 hook 檔沒刪掉" fi - purged "已刪除工作區 .kiro/hooks/ 底下所有 hook 檔,含非 jsc 的第三方項目" - echo "[jsc] kiro:hook 檔綁在工作區,這次只清得到目前目錄的 ./.kiro/hooks/,其他工作區要各自跑一次。" + # 真正生效的位置是 agent 設定檔與預設 agent 設定,兩個都要拿得掉。 + kr_agent=$(kiro_agent_file) + kr_settings=$(kiro_cli_settings) + if [ -f "$kr_agent" ]; then + backup_file "$kr_agent" || pfail "無法備份 $kr_agent,沒有備份就不移除" + rm -f "$kr_agent" 2>/dev/null || pfail "無法刪除 $kr_agent" + [ -f "$kr_agent" ] && pfail "$kr_agent 刪除後檔案還在" + fi + if [ -f "$kr_settings" ] && json_top_key "$kr_settings" chat.defaultAgent; then + json_pairs_ok "$kr_settings" || pfail "$kr_settings 不是成對的 JSON,讀不懂就不動它" + backup_file "$kr_settings" || pfail "無法備份 $kr_settings,沒有備份就不移除" + json_del_hooks "$kr_settings" chat.defaultAgent \ + || pfail "無法從 $kr_settings 刪除 chat.defaultAgent" + json_pairs_ok "$kr_settings" || pfail "$kr_settings 刪除後 JSON 括號不成對" + ! json_top_key "$kr_settings" chat.defaultAgent \ + || pfail "$kr_settings 刪除後仍有 chat.defaultAgent" + fi + purged "已刪除工作區 .kiro/hooks/ 底下所有 hook 檔(含非 jsc 的第三方項目)、~/.kiro/agents/jsc.json 與 settings/cli.json 的 chat.defaultAgent" + echo "[jsc] kiro:舊的 .kiro/hooks/ 綁在工作區,這次只清得到目前目錄那一份;那個目錄本來就不會被 kiro 讀,留著只會誤導下一個人。" + echo "[jsc] kiro:真正生效的 $kr_agent 與 $kr_settings 的 chat.defaultAgent 已一併移除,之後會走回內建的 kiro_default。" exit 0 ;; esac fi @@ -747,6 +1349,7 @@ if [ "$action" = smoke ]; then # 每一類實際跑過的結果行數。收尾時與下面宣告的預期條數逐類比對,加減判定路徑卻忘了改預期 # 就會當場失敗,散文與程式之間不會再各記一份數字。 smoke_n_hook=0; smoke_n_model=0; smoke_n_wp=0; smoke_n_rs=0; smoke_n_wg=0; smoke_n_vg=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、 @@ -756,12 +1359,20 @@ if [ "$action" = smoke ]; then # 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查 # 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑 # 相依版本 version-guard.sh 讀 manifest jsc.requires 的擋人、放行、豁免與 fail-open 路徑 + # 技能名解析 skill-name.sh 五支 CLI 各自的取值來源,加上解不出來的三條 + # 阻擋形態 deny.sh 四種形態(stderr 加 2、stdout deny JSON、kiro 注入、未知代號的保守預設) + # 跨 CLI 貫通 restart-gate.sh 吃五支 CLI 的真實負載,驗判定與輸出形態串得起來 + # 接線形狀 每支 CLI 要寫出去的內容真的產出來一次,驗結構本身(正反案例各一組) SMOKE_EXPECT_HOOK=17 SMOKE_EXPECT_MODEL=4 SMOKE_EXPECT_WP=6 SMOKE_EXPECT_RS=16 SMOKE_EXPECT_WG=21 SMOKE_EXPECT_VG=13 + SMOKE_EXPECT_SN=10 + SMOKE_EXPECT_DN=9 + SMOKE_EXPECT_CX=9 + SMOKE_EXPECT_SH=26 # 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略) # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 @@ -1143,12 +1754,267 @@ if [ "$action" = smoke ]; then printf '[jsc] version-guard.sh:建不出暫存目錄,相依版本判定沒驗到。\n' >> "$smoke_out" fi + # 技能名解析(skill-name.sh):五支 CLI 的負載形態各不相同,取值規則只有這一份,所以每一支 + # 都要有自己的斷言。少驗一支,那支 CLI 的閘門就會靜靜解不出技能名而全程放行——先前 codex、 + # copilot、antigravity 三支失效,外顯就是這個樣子,從結束碼上完全看不出來。 + # JSC_SKILL 與 SKILL 一律清空:留著繼承來的值會蓋過負載解析,測到的就不是負載那條路。 + smoke_sn_case() { # $1=情境 $2=CLI 代號 $3=負載 $4=預期 domain(空=要解不出來) $5=預期技能名 $6=JSC_SKILL + _want="" + [ -n "${4:-}" ] && _want=$(printf '%s\t%s' "$4" "${5:-}") + _out=$(printf '%s' "$3" | env JSC_SKILL="${6:-}" SKILL="" \ + sh "$HOOKS/skill-name.sh" "$2" 2>/dev/null); _rc=$? + smoke_n_sn=$((smoke_n_sn + 1)) + if [ "$_rc" -eq 0 ] && [ "$_out" = "$_want" ]; then + printf '[jsc] skill-name.sh %s(%s):輸出與預期相同。\n' "$2" "$1" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] skill-name.sh %s(%s):exit %s,輸出「%s」,預期「%s」,技能名解析壞了。\n' \ + "$2" "$1" "$_rc" "$(printf '%s' "$_out" | tr '\t\n' ' ')" \ + "$(printf '%s' "$_want" | tr '\t\n' ' ')" >> "$smoke_out" + fi + } + smoke_sn_case "stdin JSON 的 skill 欄位" claude \ + '{"tool_name":"Skill","tool_input":{"skill":"jsc-sdlc:implement"}}' sdlc implement + smoke_sn_case "環境變數蓋過負載" claude '{}' sdlc implement jsc-sdlc:implement + smoke_sn_case "Bash 指令裡的 SKILL.md 路徑" codex \ + '{"tool_name":"Bash","tool_input":{"command":"cat /root/.codex/plugins/cache/jsc/jsc-sdlc/0.2.0/skills/implement/SKILL.md"}}' \ + sdlc implement + smoke_sn_case "字串化的 toolArgs 要剝兩層" copilot \ + '{"toolName":"skill","toolArgs":"{\"name\":\"jsc-sdlc:implement\"}"}' sdlc implement + smoke_sn_case "toolCall.args.AbsolutePath" antigravity \ + '{"toolCall":{"name":"view_file","args":{"AbsolutePath":"/root/.gemini/config/plugins/jsc-sdlc/skills/implement/SKILL.md","StartLine":1}}}' \ + sdlc implement + # PreInvocation 那一輪沒有工具呼叫,斜線指令只出現在提示字串裡,那條路要單獨驗。 + smoke_sn_case "PreInvocation 的斜線指令" antigravity \ + '{"prompt":"/jsc-sdlc:implement 開始這一包","stepIdx":0}' sdlc implement + smoke_sn_case "提示開頭的斜線指令" kiro '{"prompt":"/jsc-sdlc:implement 開始這一包"}' sdlc implement + smoke_sn_case "解不出技能名就印空字串" codex \ + '{"tool_name":"Bash","tool_input":{"command":"ls -al"}}' "" + smoke_sn_case "別人的技能不是我們該管的" claude '{"tool_name":"Skill","tool_input":{"skill":"design"}}' "" + smoke_sn_case "認不得的 CLI 代號" no-such-cli '{"prompt":"/jsc-sdlc:implement"}' "" + + # 阻擋形態(deny.sh):形態錯了,判定再準也擋不下來,而且從結束碼上看不出差別—— + # antigravity 那一支尤其危險,靠結束碼會變成「判定擋下、CLI 照樣放行」的無聲失效。 + dn_msg=$(printf '第一行訊息\n第二行訊息') + smoke_dn_case() { # $1=情境 $2=CLI 代號 $3=預期結束碼 $4=out 或 err $5=預期字串 + if [ "$4" = out ]; then + _out=$(printf '%s\n' "$dn_msg" | sh "$HOOKS/deny.sh" "$2" 2>/dev/null); _rc=$? + else + _out=$(printf '%s\n' "$dn_msg" | sh "$HOOKS/deny.sh" "$2" 2>&1 >/dev/null); _rc=$? + fi + smoke_n_dn=$((smoke_n_dn + 1)) + if [ "$_rc" -eq "$3" ] && printf '%s' "$_out" | grep -qF "$5"; then + printf '[jsc] deny.sh %s(%s):exit %s,%s 含預期內容,與預期相同。\n' \ + "$2" "$1" "$_rc" "$4" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] deny.sh %s(%s):exit %s,預期 %s,%s 未含「%s」,阻擋形態壞了:%s\n' \ + "$2" "$1" "$_rc" "$3" "$4" "$5" \ + "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + smoke_dn_case "stderr 加結束碼 2" claude 2 err "第一行訊息" + smoke_dn_case "stderr 加結束碼 2" codex 2 err "第二行訊息" + smoke_dn_case "stderr 加結束碼 2" copilot 2 err "第一行訊息" + smoke_dn_case "stdout 的 deny JSON" antigravity 0 out '"decision":"deny"' + smoke_dn_case "多行訊息壓進同一個 reason" antigravity 0 out '第一行訊息\n第二行訊息' + smoke_dn_case "擋不下來就改印警告" kiro 0 out "kiro 擋不下技能叫用" + smoke_dn_case "認不得的代號走保守預設" no-such-cli 2 err "第一行訊息" + # antigravity 的 stdout 只准有那一行 JSON:多印一行,那支 CLI 就解不出這份負載,等於沒擋。 + smoke_n_dn=$((smoke_n_dn + 1)) + dn_lines=$(printf '%s\n' "$dn_msg" | sh "$HOOKS/deny.sh" antigravity 2>/dev/null | wc -l | tr -d ' ') + if [ "$dn_lines" = 1 ]; then + printf '[jsc] deny.sh antigravity(stdout 只有一行 JSON):與預期相同。\n' >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] deny.sh antigravity(stdout 只有一行 JSON):實際 %s 行,多印的內容會讓那支 CLI 解不出這份負載。\n' \ + "$dn_lines" >> "$smoke_out" + fi + # 訊息也可以從參數進來,呼叫端不必為了一句話開一條管線。 + smoke_n_dn=$((smoke_n_dn + 1)) + dn_arg=$(sh "$HOOKS/deny.sh" claude "參數訊息" 2>&1 >/dev/null); dn_arg_rc=$? + if [ "$dn_arg_rc" -eq 2 ] && printf '%s' "$dn_arg" | grep -qF "參數訊息"; then + printf '[jsc] deny.sh claude(訊息走參數):exit 2,訊息傳得到,與預期相同。\n' >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] deny.sh claude(訊息走參數):exit %s,輸出「%s」,參數那條路壞了。\n' \ + "$dn_arg_rc" "$(printf '%s' "$dn_arg" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + + # 跨 CLI 貫通:把五支 CLI 的真實負載餵進 restart-gate.sh,驗「解析 → 判定 → 輸出形態」整條串得起來。 + # 上面兩段各自驗了頭尾,中間那段接不上照樣是全程放行,而且三段分開看都會顯示正常。 + # 用暫時的 $JSC_HOME 是為了不動到使用者真正的閘門狀態。 + smoke_cx_case() { # $1=情境 $2=CLI 代號 $3=負載 $4=預期結束碼 $5=out|err|none $6=預期字串 + case "$5" in + out) _out=$(printf '%s' "$3" | env JSC_HOME="$cx_home" JSC_CLI="$2" JSC_SKILL="" SKILL="" \ + sh "$HOOKS/restart-gate.sh" 2>/dev/null); _rc=$? ;; + err) _out=$(printf '%s' "$3" | env JSC_HOME="$cx_home" JSC_CLI="$2" JSC_SKILL="" SKILL="" \ + sh "$HOOKS/restart-gate.sh" 2>&1 >/dev/null); _rc=$? ;; + *) _out=$(printf '%s' "$3" | env JSC_HOME="$cx_home" JSC_CLI="$2" JSC_SKILL="" SKILL="" \ + sh "$HOOKS/restart-gate.sh" 2>&1); _rc=$? ;; + esac + smoke_n_cx=$((smoke_n_cx + 1)) + # 預期字串為空時要求輸出也是空的:放行卻印了東西,在 antigravity 上就是一份多餘的 deny JSON。 + if [ "$_rc" -eq "$4" ] && + { { [ -z "${6:-}" ] && [ -z "$_out" ]; } || { [ -n "${6:-}" ] && printf '%s' "$_out" | grep -qF "$6"; }; } + then + printf '[jsc] restart-gate.sh %s(%s):exit %s,輸出形態與預期相同。\n' "$2" "$1" "$_rc" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] restart-gate.sh %s(%s):exit %s,預期 %s,輸出「%s」,跨 CLI 判定串不起來。\n' \ + "$2" "$1" "$_rc" "$4" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + if cx_home=$(mktemp -d 2>/dev/null) && mkdir -p "$cx_home/restart-required.d" 2>/dev/null; then + for _c in claude codex copilot antigravity kiro; do + printf 'at=%s\nmode=update\ndomains=hooks\ncli=%s\n' "$(now_iso)" "$_c" \ + > "$cx_home/restart-required.d/$_c" 2>/dev/null + done + smoke_cx_case "claude 的 skill 欄位負載" claude \ + '{"tool_name":"Skill","tool_input":{"skill":"jsc-sdlc:implement"}}' 2 err "還在跑舊版" + smoke_cx_case "codex 的 Bash 負載" codex \ + '{"tool_name":"Bash","tool_input":{"command":"cat /x/jsc-sdlc/skills/implement/SKILL.md"}}' 2 err "還在跑舊版" + smoke_cx_case "copilot 的 toolArgs 負載" copilot \ + '{"toolName":"skill","toolArgs":"{\"name\":\"jsc-sdlc:implement\"}"}' 2 err "還在跑舊版" + smoke_cx_case "antigravity 的 view_file 負載" antigravity \ + '{"toolCall":{"name":"view_file","args":{"AbsolutePath":"/x/jsc-sdlc/skills/implement/SKILL.md"}}}' \ + 0 out '"decision":"deny"' + smoke_cx_case "antigravity 的 PreInvocation 負載" antigravity \ + '{"prompt":"/jsc-sdlc:implement 開始"}' 0 out '"decision":"deny"' + smoke_cx_case "kiro 只注入警告,擋不下來" kiro \ + '{"prompt":"/jsc-sdlc:implement 開始"}' 0 out "kiro 擋不下技能叫用" + smoke_cx_case "fail-open:codex 負載解不出技能名" codex \ + '{"tool_name":"Bash","tool_input":{"command":"ls -al"}}' 0 none "" + smoke_cx_case "豁免技能 jsc-cli:deploy" codex \ + '{"tool_name":"Bash","tool_input":{"command":"cat /x/jsc-cli/skills/deploy/SKILL.md"}}' 0 none "" + smoke_cx_case "豁免技能不吐 deny JSON" antigravity \ + '{"toolCall":{"name":"view_file","args":{"AbsolutePath":"/x/jsc-cli/skills/deploy/SKILL.md"}}}' 0 none "" + rm -rf "$cx_home" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] restart-gate.sh:建不出暫存目錄,跨 CLI 貫通判定沒驗到。\n' >> "$smoke_out" + fi + + # 接線形狀:把每支 CLI 要寫出去的內容真的產出來一次,驗結構本身對不對。這一組是實測踩坑後補的—— + # antigravity 的 PreToolUse 寫成 Flat 時整個事件被靜默丟棄,鍵在、JSON 合法、標記段落也在, + # 從檔案上看不出任何異常,接線與盤點兩邊都回報成功,實際上一次都不會被叫用。 + # 每一條都配一個反向案例:形狀錯的內容要被判成不合格。少了反向那半,一支永遠回 0 的檢查函式 + # 也會讓正向那條通過,等於沒驗。 + smoke_sh_case() { # $1=情境 $2=期望 ok 或 bad $3=判定指令(會被 eval) + smoke_n_sh=$((smoke_n_sh + 1)) + if eval "$3" >/dev/null 2>&1; then _got=ok; else _got=bad; fi + if [ "$_got" = "$2" ]; then + printf '[jsc] 接線形狀(%s):判定為 %s,與預期相同。\n' "$1" "$_got" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] 接線形狀(%s):判定為 %s,預期 %s,形狀檢查抓不到這種錯。\n' \ + "$1" "$_got" "$2" >> "$smoke_out" + fi + } + if sh_home=$(mktemp -d 2>/dev/null); then + # antigravity:從乾淨的第三方檔開始寫,驗形狀,也驗別人的段落沒被動到。 + sh_ag="$sh_home/hooks.json" + printf '{\n "other": {\n "PreInvocation": [\n { "type": "command", "command": "true" }\n ]\n }\n}\n' > "$sh_ag" + json_put_top_key "$sh_ag" jsc "$(antigravity_hooks_block)" >/dev/null 2>&1 + smoke_sh_case "antigravity 的 PreToolUse 是 Grouped" ok "antigravity_grouped_ok '$sh_ag'" + smoke_sh_case "antigravity 的 PreInvocation 維持 Flat" ok "antigravity_flat_ok '$sh_ag'" + smoke_sh_case "antigravity 寫入後別人的段落還在,JSON 也成對" ok \ + "json_pairs_ok '$sh_ag' && json_top_key '$sh_ag' other" + smoke_sh_case "antigravity 的命令帶得到 JSC_CLI=antigravity" ok \ + "grep -qF 'JSC_CLI=antigravity' '$sh_ag'" + # 反向:把 PreToolUse 寫成 Flat(handler 直接排在陣列裡),檢查函式要判它不合格。 + sh_ag_flat="$sh_home/hooks-flat.json" + printf '{\n "jsc": {\n "PreToolUse": [\n { "matcher": "^view_file$", "type": "command", "command": "true" }\n ],\n "PreInvocation": [\n { "type": "command", "command": "true" }\n ]\n }\n}\n' > "$sh_ag_flat" + smoke_sh_case "Flat 的 PreToolUse 要被判不合格" bad "antigravity_grouped_ok '$sh_ag_flat'" + # codex:manifest 的 hooks 鍵是路徑字串(規格裡跟 skills 同一類),不是內嵌物件; + # 它指到的那份 hook 檔要從 hooks/hooks.json 推導,matcher Skill 換成 Bash 且不留殘餘。 + sh_cx="$sh_home/plugin.json" + printf '{\n "name": "jsc-hooks"\n}\n' > "$sh_cx" + json_put_top_key "$sh_cx" hooks "$(codex_hooks_key_block)" >/dev/null 2>&1 + smoke_sh_case "codex 的 manifest hooks 鍵是路徑字串" ok \ + "json_pairs_ok '$sh_cx' && grep -qF '\"hooks\": \"$CODEX_HOOKS_REL\"' '$sh_cx'" + smoke_sh_case "內嵌物件的 hooks 鍵要被判不合格" bad \ + "printf '{ \"hooks\": { \"PreToolUse\": [] } }' > '$sh_home/inline.json'; grep -qF '\"hooks\": \"$CODEX_HOOKS_REL\"' '$sh_home/inline.json'" + sh_cxh="$sh_home/codex-hooks.json" + codex_derive_hooks "$HOOKS/hooks.json" > "$sh_cxh" 2>/dev/null + smoke_sh_case "codex 專屬 hook 檔的 matcher 換成 Bash 且沒留 Skill" ok \ + "json_pairs_ok '$sh_cxh' && grep -qF '\"matcher\": \"Bash\"' '$sh_cxh' && ! grep -qF '\"matcher\": \"Skill\"' '$sh_cxh'" + smoke_sh_case "推導不會動到 Claude 那一份" ok \ + "grep -qF '\"matcher\": \"Skill\"' '$HOOKS/hooks.json'" + smoke_sh_case "推導保住 hooks.json 的其他事件,沒有只剩 PreToolUse" ok \ + "grep -qF '\"UserPromptSubmit\"' '$sh_cxh' && grep -qF '\"SessionStart\"' '$sh_cxh'" + # 代號跟 matcher 同等重要:取不到代號的閘門解不出技能名,一律安靜放行。三支各驗一條。 + smoke_sh_case "codex 專屬 hook 檔的命令帶得到 JSC_CLI=codex" ok \ + "grep -qF 'JSC_CLI=codex' '$sh_cxh'" + # copilot:設定在 settings.json 的頂層 hooks 鍵,合併不覆寫。那份檔案同時裝著外掛啟用狀態, + # 所以「有沒有接上」與「有沒有弄壞別人的東西」要分開驗,後者的後果嚴重得多。 + sh_cp="$sh_home/settings.json" + sh_cpnew="$sh_home/entries.json" + copilot_hook_entries > "$sh_cpnew" + printf '{\n "enabledPlugins": { "jsc-hooks@jsc": true },\n "extraKnownMarketplaces": { "jsc": {} },\n "hooks": {\n "SessionStart": [\n { "bash": "bash \x27/other/agent-state.sh\x27", "timeoutSec": 10, "type": "command" }\n ]\n }\n}\n' > "$sh_cp" + # 改寫前的樣子存成檔案再比對。存成變數的話多行內容塞進 [ ] 測試會整個散掉, + # 比出來的結果看起來像「別人的鍵不見了」,其實只是比法錯了。 + json_top_keys "$sh_cp" > "$sh_home/top0" + copilot_foreign_entries "$sh_cp" > "$sh_home/oth0" + copilot_merge_hooks "$sh_cp" wire "$sh_cpnew" > "$sh_home/merged.json" 2>/dev/null \ + && cat "$sh_home/merged.json" > "$sh_cp" + smoke_sh_case "copilot 併進 settings.json 後兩道閘門在、matcher 是小寫 skill" ok \ + "copilot_hooks_ok '$sh_cp' && copilot_single_case_ok '$sh_cp'" + smoke_sh_case "併進後最上層鍵與別人的條目一筆都沒少" ok \ + "json_pairs_ok '$sh_cp' && json_top_keys '$sh_cp' | diff -q - '$sh_home/top0' && copilot_foreign_entries '$sh_cp' | diff -q - '$sh_home/oth0'" + smoke_sh_case "copilot 的 jsc 條目帶得到 JSC_CLI=copilot" ok \ + "grep -qF 'JSC_CLI=copilot' '$sh_cp'" + # 重跑要冪等:合併寫成疊加的話,每接一次線就多一份,同一支 hook 跑好幾遍。 + copilot_merge_hooks "$sh_cp" wire "$sh_cpnew" > "$sh_home/merged2.json" 2>/dev/null \ + && cat "$sh_home/merged2.json" > "$sh_cp" + smoke_sh_case "重跑接線不疊加" ok \ + "[ \"\$(grep -c 'restart-gate.sh' '$sh_cp')\" = 1 ]" + copilot_merge_hooks "$sh_cp" purge > "$sh_home/purged.json" 2>/dev/null \ + && cat "$sh_home/purged.json" > "$sh_cp" + smoke_sh_case "purge 只拿掉 jsc 那幾筆,別人的條目與最上層鍵都留著" ok \ + "json_pairs_ok '$sh_cp' && ! grep -q 'restart-gate.sh' '$sh_cp' && json_top_keys '$sh_cp' | diff -q - '$sh_home/top0' && copilot_foreign_entries '$sh_cp' | diff -q - '$sh_home/oth0'" + printf '{ "hooks": { "PreToolUse": [], "preToolUse": [] } }\n' > "$sh_home/dup.json" + smoke_sh_case "兩種大小寫並存要被判不合格" bad "copilot_single_case_ok '$sh_home/dup.json'" + # 位置錯了也要被抓到。舊版把設定寫進 $COPILOT_HOME/hooks/,那底下是腳本目錄不是設定目錄, + # 檔案內容完全正確、copilot 卻一次都沒讀過,只驗內容的檢查一律回 present。 + printf '{ "hooks": { "PreToolUse": [ { "matcher": "skill", "type": "command", "bash": "sh \x22/x/restart-gate.sh\x22" } ] } }\n' > "$sh_home/wrongplace.json" + smoke_sh_case "設定寫錯位置時 settings.json 那一項要判不合格" bad \ + "copilot_hooks_ok '$sh_home/nonexistent-settings.json'" + # kiro:resources 少了兩層 glob,這個 agent 一支 jsc 技能都看不到。 + sh_kr="$sh_home/jsc.json" + kiro_agent_json > "$sh_kr" 2>/dev/null + smoke_sh_case "kiro 的 resources 有兩層 skill:// glob" ok \ + "json_pairs_ok '$sh_kr' && grep -qF '/skills/*/*/SKILL.md' '$sh_kr'" + printf '{ "resources": ["skill://.kiro/skills/*/SKILL.md"] }\n' > "$sh_home/one.json" + smoke_sh_case "只有一層 glob 要被判不合格" bad "grep -qF '/skills/*/*/SKILL.md' '$sh_home/one.json'" + smoke_sh_case "kiro agent 的形狀合格(合法事件、timeout_ms、沒有 on/run/env)" ok \ + "kiro_agent_shape_ok '$sh_kr'" + # 反向:sessionStart 不是合法事件,最上層的 on/run/env 則是會被靜默忽略的未知鍵。 + printf '{ "name": "p", "on": ["sessionStart"], "run": "true", "env": {} }\n' > "$sh_home/legacy.json" + smoke_sh_case "舊的 on/run/env 形狀要被判不合格" bad "kiro_agent_shape_ok '$sh_home/legacy.json'" + # validate 的判準是輸出,不是結束碼。這兩條把那條規則釘住:那支指令四種情況都回 0, + # 拿結束碼當判準會做出一支永遠通過的檢查。 + smoke_sh_case "validate 輸出為空才算通過" ok "kiro_validate_output_ok \"\"" + smoke_sh_case "validate 印了錯誤就算不通過(結束碼仍是 0)" bad \ + "kiro_validate_output_ok \"Error: Json supplied is invalid: data did not match any variant\"" + # 環境問題是「驗不了」,不是「驗不過」。報成失敗的話,沒登入的機器會整批接不了線。 + smoke_sh_case "沒登入的錯誤要放行,不算驗不過" ok \ + "kiro_validate_output_ok \"error: You are not logged in, please log in with kiro-cli login\"" + smoke_sh_case "沒登入時要看得出這一項沒驗到" bad \ + "kiro_validate_ran \"error: You are not logged in, please log in with kiro-cli login\"" + rm -rf "$sh_home" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] 接線形狀:建不出暫存目錄,形狀判定沒驗到。\n' >> "$smoke_out" + fi + # 自我斷言:實際跑過的條數對上宣告的預期條數,再對上真正印出來的行數。三邊一致才算數, # 少跑一條或多印一行都會在這裡現形,散文就不必再自己記一份數字。 smoke_lines=$(wc -l < "$smoke_out" 2>/dev/null | tr -d ' ') [ -n "$smoke_lines" ] || smoke_lines=0 smoke_total=$((SMOKE_EXPECT_HOOK + SMOKE_EXPECT_MODEL + SMOKE_EXPECT_WP \ - + SMOKE_EXPECT_RS + SMOKE_EXPECT_WG + SMOKE_EXPECT_VG)) + + SMOKE_EXPECT_RS + SMOKE_EXPECT_WG + SMOKE_EXPECT_VG \ + + SMOKE_EXPECT_SN + SMOKE_EXPECT_DN + SMOKE_EXPECT_CX + SMOKE_EXPECT_SH)) smoke_mismatch="" [ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \ || smoke_mismatch="${smoke_mismatch}hook 模式 $smoke_n_hook 條(預期 $SMOKE_EXPECT_HOOK);" @@ -1162,10 +2028,18 @@ if [ "$action" = smoke ]; then || smoke_mismatch="${smoke_mismatch}寫入閘門 $smoke_n_wg 條(預期 $SMOKE_EXPECT_WG);" [ "$smoke_n_vg" = "$SMOKE_EXPECT_VG" ] \ || smoke_mismatch="${smoke_mismatch}相依版本 $smoke_n_vg 條(預期 $SMOKE_EXPECT_VG);" + [ "$smoke_n_sn" = "$SMOKE_EXPECT_SN" ] \ + || smoke_mismatch="${smoke_mismatch}技能名解析 $smoke_n_sn 條(預期 $SMOKE_EXPECT_SN);" + [ "$smoke_n_dn" = "$SMOKE_EXPECT_DN" ] \ + || smoke_mismatch="${smoke_mismatch}阻擋形態 $smoke_n_dn 條(預期 $SMOKE_EXPECT_DN);" + [ "$smoke_n_cx" = "$SMOKE_EXPECT_CX" ] \ + || smoke_mismatch="${smoke_mismatch}跨 CLI 貫通 $smoke_n_cx 條(預期 $SMOKE_EXPECT_CX);" + [ "$smoke_n_sh" = "$SMOKE_EXPECT_SH" ] \ + || smoke_mismatch="${smoke_mismatch}接線形狀 $smoke_n_sh 條(預期 $SMOKE_EXPECT_SH);" [ "$smoke_lines" = "$smoke_total" ] \ || smoke_mismatch="${smoke_mismatch}結果行數 $smoke_lines 行(預期 $smoke_total);" - smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型來源 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條、相依版本 $SMOKE_EXPECT_VG 條" + smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型來源 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條、相依版本 $SMOKE_EXPECT_VG 條、技能名解析 $SMOKE_EXPECT_SN 條、阻擋形態 $SMOKE_EXPECT_DN 條、跨 CLI 貫通 $SMOKE_EXPECT_CX 條、接線形狀 $SMOKE_EXPECT_SH 條" if [ -n "$smoke_mismatch" ]; then printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數" printf 'lines\t%s\n' "$smoke_lines" @@ -1176,7 +2050,7 @@ if [ "$action" = smoke ]; then fi if [ "$smoke_fails" -eq 0 ]; then - printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型來源、工作包歸屬、部署後重啟閘門、寫入提交閘門與相依版本檢查的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤" + printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型來源、工作包歸屬、部署後重啟閘門、寫入提交閘門、相依版本檢查、五支 CLI 的技能名解析、四種阻擋形態、跨 CLI 貫通與各 CLI 的接線形狀的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤" printf 'lines\t%s\n' "$smoke_lines" cat "$smoke_out"; rm -f "$smoke_out"; exit 0 fi @@ -1195,11 +2069,19 @@ if [ "$action" = status ]; then st_items=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; } st_missing=0 st_degrade="" + # 接上時的 reason 也要一支 CLI 一句:五支的覆蓋範圍不一樣,共用一句話就會把「codex 接了兩道閘門」 + # 講成「codex 九支全接」。預設值給 claude,其餘各自在下面覆寫。 + st_wired="hooks.json 自動接線全部九支 hook" - # 記一個檢查點。$1=項目名 $2=路徑 $3=present|missing + # 記一個檢查點。$1=項目名 $2=路徑 $3=present|missing|unverified + # + # 三種狀態,不是兩種。unverified 是「這一項驗不了」,跟「驗不過」分開記:只有 missing 算缺項。 + # 少了這一格,驗不了的項目只能在 present 與 missing 之間二選一——報 present 會讓沒驗到的東西 + # 看起來像通過,報 missing 會把環境問題(沒登入、憑證過期)算成接線失敗,兩邊都在說謊。 st_item() { printf 'item\t%s\t%s\t%s\n' "$1" "$2" "$3" >> "$st_items" - [ "$3" = present ] || st_missing=$((st_missing + 1)) + [ "$3" = missing ] && st_missing=$((st_missing + 1)) + return 0 } # 檔案存在且帶有該標記才算接上。$1=項目名 $2=檔案 $3=開頭標記行 @@ -1318,52 +2200,148 @@ if [ "$action" = status ]; then else st_item plugin-user-prompt-root "$_codex_plugin_hooks" missing fi - st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" ;; + # pre-tool hook 的 Codex 專屬覆寫。三項分開算:鍵在不在、兩支閘門各接上沒有、matcher 對不對。 + # 只驗「有 hooks 鍵」會漏掉「鍵在但 matcher 還是 Skill」,那正是先前失效的形態。 + cx_manifest=$(codex_manifest) + cx_hooks=$(codex_hooks_file) + # 鍵的**值**要單獨算一項:寫成內嵌物件時鍵照樣在,只看鍵在不在會讀成接好了。 + if [ -f "$cx_manifest" ] && grep -qF "\"hooks\": \"$CODEX_HOOKS_REL\"" "$cx_manifest" 2>/dev/null + then st_item plugin-hooks-path "$cx_manifest" present + else st_item plugin-hooks-path "$cx_manifest" missing; fi + if [ -f "$cx_hooks" ] && json_top_key "$cx_hooks" hooks + then st_item codex-hooks-file "$cx_hooks" present + else st_item codex-hooks-file "$cx_hooks" missing; fi + if [ -f "$cx_hooks" ] && grep -qF '"matcher": "Bash"' "$cx_hooks" 2>/dev/null + then st_item codex-bash-matcher "$cx_hooks" present + else st_item codex-bash-matcher "$cx_hooks" missing; fi + # 殘留的 Skill matcher 就是接線沒生效的證據:codex 沒有那個工具,那一組永遠不會被叫用。 + if [ -f "$cx_hooks" ] && grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null + then st_item codex-no-skill-matcher "$cx_hooks" missing + else st_item codex-no-skill-matcher "$cx_hooks" present; fi + for _g in restart-gate.sh version-guard.sh; do + if [ -f "$cx_hooks" ] && grep -qF "$_g" "$cx_hooks" 2>/dev/null + then st_item "codex-${_g%.sh}" "$cx_hooks" present + else st_item "codex-${_g%.sh}" "$cx_hooks" missing; fi + done + # CLI 代號也是接線的一部分:少了它,閘門認不出自己跑在哪一支 CLI 上,技能名解不出來, + # 兩道閘門一律安靜放行。只驗腳本名在不在會把這種「接了等於沒接」讀成完好。 + if [ -f "$cx_hooks" ] && grep -qF 'JSC_CLI=codex' "$cx_hooks" 2>/dev/null + then st_item codex-cli-code "$cx_hooks" present + else st_item codex-cli-code "$cx_hooks" missing; fi + st_wired="pre-tool hook 接在 hooks/codex-hooks.json 的 Bash matcher 上(manifest 的 hooks 鍵以路徑字串指過去),版本前置檢查與部署後重啟閘門都擋得下來;STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線,註解範圍與繁中編碼每輪結束掃整個工作區" ;; copilot) + cp_instr=$(copilot_instructions) + cp_settings=$(copilot_settings) st_rc_alias alias jsc-hooks:copilot - st_block ste100 "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" "" - st_comment_scope comment-scope "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" - st_lang_guard lang-guard "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" + st_block ste100 "$cp_instr" "" + st_comment_scope comment-scope "$cp_instr" + st_lang_guard lang-guard "$cp_instr" st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" - st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;; + # 位置本身就是一個檢查點:上一版把設定寫進 $COPILOT_HOME/hooks/,那底下是腳本目錄, + # 檔案好端端在那裡、內容也對,copilot 卻一次都沒讀過,盤點照樣回 present。 + if copilot_hooks_ok "$cp_settings" + then st_item settings-hooks "$cp_settings" present + else st_item settings-hooks "$cp_settings" missing; fi + if copilot_single_case_ok "$cp_settings" + then st_item single-event-case "$cp_settings" present + else st_item single-event-case "$cp_settings" missing; fi + # CLI 代號也是接線的一部分,理由同 codex 那一項:取不到代號的閘門一律安靜放行。 + if [ -f "$cp_settings" ] && grep -qF 'JSC_CLI=copilot' "$cp_settings" 2>/dev/null + then st_item cli-code "$cp_settings" present + else st_item cli-code "$cp_settings" missing; fi + # 那份檔案還裝著外掛啟用狀態,掉了會讓十個 plugin 整批失效,所以單獨算一項。 + for _k in enabledPlugins extraKnownMarketplaces; do + if [ -f "$cp_settings" ] && json_top_key "$cp_settings" "$_k" + then st_item "keep-$_k" "$cp_settings" present + else st_item "keep-$_k" "$cp_settings" missing; fi + done + st_runtime_paths hooks-file-paths "$cp_settings" + st_wired="pre-tool hook 接在 ~/.copilot/settings.json 的 hooks 鍵上(PreToolUse matcher skill),版本前置檢查與部署後重啟閘門都擋得下來;STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;; antigravity) + ag_hooks=$(antigravity_hooks_file) st_rc_alias alias jsc-hooks:antigravity st_block ste100 "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" "" st_comment_scope comment-scope "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" st_lang_guard lang-guard "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" - st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;; + if [ -f "$ag_hooks" ] && json_top_key "$ag_hooks" jsc + then st_item jsc-namespace "$ag_hooks" present + else st_item jsc-namespace "$ag_hooks" missing; fi + # 錨點單獨算一項:沒有錨點會連 view_file_outline 一起命中,那是接了卻接錯的形態。 + if [ -f "$ag_hooks" ] && grep -qF '"^view_file$"' "$ag_hooks" 2>/dev/null + then st_item anchored-matcher "$ag_hooks" present + else st_item anchored-matcher "$ag_hooks" missing; fi + if [ -f "$ag_hooks" ] && grep -qF '"PreInvocation"' "$ag_hooks" 2>/dev/null + then st_item pre-invocation "$ag_hooks" present + else st_item pre-invocation "$ag_hooks" missing; fi + # 形狀也要各算一項。只看鍵在不在會把「事件被靜默丟棄」讀成接好了——實測就是這樣漏掉的。 + if antigravity_grouped_ok "$ag_hooks" + then st_item pretooluse-grouped "$ag_hooks" present + else st_item pretooluse-grouped "$ag_hooks" missing; fi + if antigravity_flat_ok "$ag_hooks" + then st_item preinvocation-flat "$ag_hooks" present + else st_item preinvocation-flat "$ag_hooks" missing; fi + for _g in restart-gate.sh version-guard.sh; do + if [ -f "$ag_hooks" ] && grep -qF "$_g" "$ag_hooks" 2>/dev/null + then st_item "${_g%.sh}" "$ag_hooks" present + else st_item "${_g%.sh}" "$ag_hooks" missing; fi + done + # CLI 代號單獨算一項:這支的阻擋只認 stdout 的 deny JSON,代號取不到時 deny.sh 會退回 + # 結束碼形態,判定擋下了、CLI 卻收不到拒絕,設定檔看起來一樣完好。 + if [ -f "$ag_hooks" ] && grep -qF 'JSC_CLI=antigravity' "$ag_hooks" 2>/dev/null + then st_item cli-code "$ag_hooks" present + else st_item cli-code "$ag_hooks" missing; fi + st_runtime_paths hooks-file-paths "$ag_hooks" + st_wired="pre-tool hook 接在 ~/.gemini/config/hooks.json 的 jsc 段落(PreToolUse matcher ^view_file\$ 加 PreInvocation),版本前置檢查與部署後重啟閘門都以 stdout 的 deny JSON 擋得下來;hook 觸發本身未實跑驗證。STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線" ;; kiro) - # kiro 的 hook 檔綁在工作區,這裡看的一律是目前工作目錄底下那一份 - for _f in ./.kiro/hooks/jsc-hooks-session-start.json ./.kiro/hooks/jsc-hooks.json; do - if [ -f "$_f" ] && json_top_key "$_f" run; then st_item "$(basename "$_f" .json)" "$_f" present - else st_item "$(basename "$_f" .json)" "$_f" missing; fi + kr_agent=$(kiro_agent_file) + kr_settings=$(kiro_cli_settings) + if [ -f "$kr_agent" ] && json_top_key "$kr_agent" hooks + then st_item agent-hooks "$kr_agent" present + else st_item agent-hooks "$kr_agent" missing; fi + # resources 的兩層 glob 單獨算一項:少了它,這個 agent 一支 jsc 技能都看不到, + # 但 hooks 那一段照樣讀得懂,只看 JSON 成不成對完全看不出這個洞。 + if [ -f "$kr_agent" ] && grep -qF '/skills/*/*/SKILL.md' "$kr_agent" 2>/dev/null + then st_item resources-two-level "$kr_agent" present + else st_item resources-two-level "$kr_agent" missing; fi + if [ -f "$kr_agent" ] && json_top_key "$kr_agent" tools + then st_item agent-tools "$kr_agent" present + else st_item agent-tools "$kr_agent" missing; fi + # 形狀與 validate 各算一項。檔案合法不等於接線生效:未知的頂層鍵會被靜默忽略, + # 舊版的 on/run/env 就是這樣一個錯都不報、卻什麼都沒做。 + if kiro_agent_shape_ok "$kr_agent" + then st_item agent-shape "$kr_agent" present + else st_item agent-shape "$kr_agent" missing; fi + # validate 分三格,不是兩格。判準是輸出不是結束碼(那支指令一律回 0),而輸出還要再分 + # 「在講這個檔案」與「在講別的事」:沒登入時它印一行 error,那是驗不了,不是驗不過。 + # 報成 present 會讓沒驗到的東西看起來像通過,報成 missing 會把沒登入算成接線缺漏。 + kr_val="" + if [ -f "$kr_agent" ]; then + kr_val=$("$(cli_bin kiro)" agent validate --path "$kr_agent" 2>&1 || true) + fi + if [ ! -f "$kr_agent" ]; then st_item agent-validate "$kr_agent" missing + elif ! kiro_validate_output_ok "$kr_val"; then st_item agent-validate "$kr_agent" missing + elif kiro_validate_ran "$kr_val"; then st_item agent-validate "$kr_agent" present + else st_item agent-validate "$kr_agent" unverified; fi + for _e in agentSpawn userPromptSubmit stop; do + if [ -f "$kr_agent" ] && grep -qF "\"$_e\"" "$kr_agent" 2>/dev/null + then st_item "event-$_e" "$kr_agent" present + else st_item "event-$_e" "$kr_agent" missing; fi done - # userPromptSubmit 那一筆的 run 串了好幾支腳本,只驗 JSON 讀得懂會漏掉少接的那一支。 - # comment-scope.sh 的 prompt 與 sweep 是兩件事,各算一項,才看得出舊版接線少了哪一個。 - if [ -f ./.kiro/hooks/jsc-hooks.json ] && - grep -qF 'comment-scope.sh\" prompt' ./.kiro/hooks/jsc-hooks.json 2>/dev/null - then st_item comment-scope ./.kiro/hooks/jsc-hooks.json present - else st_item comment-scope ./.kiro/hooks/jsc-hooks.json missing; fi - if [ -f ./.kiro/hooks/jsc-hooks.json ] && - grep -qF 'comment-scope.sh\" sweep' ./.kiro/hooks/jsc-hooks.json 2>/dev/null - then st_item comment-scope-sweep ./.kiro/hooks/jsc-hooks.json present - else st_item comment-scope-sweep ./.kiro/hooks/jsc-hooks.json missing; fi - # lang-guard.sh 的兩個模式同理各算一項 - if [ -f ./.kiro/hooks/jsc-hooks.json ] && - grep -qF 'lang-guard.sh\" prompt' ./.kiro/hooks/jsc-hooks.json 2>/dev/null - then st_item lang-guard ./.kiro/hooks/jsc-hooks.json present - else st_item lang-guard ./.kiro/hooks/jsc-hooks.json missing; fi - if [ -f ./.kiro/hooks/jsc-hooks.json ] && - grep -qF 'lang-guard.sh\" sweep' ./.kiro/hooks/jsc-hooks.json 2>/dev/null - then st_item lang-guard-sweep ./.kiro/hooks/jsc-hooks.json present - else st_item lang-guard-sweep ./.kiro/hooks/jsc-hooks.json missing; fi - st_runtime_paths session-runtime-paths ./.kiro/hooks/jsc-hooks-session-start.json - st_runtime_paths hook-runtime-paths ./.kiro/hooks/jsc-hooks.json - st_degrade="SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" ;; + 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 + then st_item "${_g%.sh}" "$kr_agent" present + else st_item "${_g%.sh}" "$kr_agent" missing; fi + done + # agent 檔寫好了不代表被選用:不設預設 agent 就一路走內建的 kiro_default,那一份改不了。 + if [ -f "$kr_settings" ] && grep -qF '"chat.defaultAgent": "jsc"' "$kr_settings" 2>/dev/null + then st_item default-agent "$kr_settings" present + else st_item default-agent "$kr_settings" missing; fi + st_runtime_paths agent-runtime-paths "$kr_agent" + st_degrade="kiro 擋不下技能叫用——技能走 ResolveSkill 這個 agent 內部請求,preToolUse 攔不到,userPromptSubmit 的非零結束碼也不會擋下那一輪,所以版本前置檢查與部署後重啟閘門只能以 stdout 注入警告。這是 CLI 的限制,不是接線缺漏:hook、resources 與預設 agent 都已就位;hook 觸發本身未實跑驗證" ;; esac if [ "$st_missing" -gt 0 ]; then @@ -1374,7 +2352,7 @@ if [ "$action" = status ]; then printf 'status=degraded reason=%s\n' "$st_degrade" cat "$st_items"; rm -f "$st_items"; exit 1 fi - printf 'status=wired reason=%s\n' "hooks.json 自動接線全部九支 hook" + printf 'status=wired reason=%s\n' "$st_wired" cat "$st_items"; rm -f "$st_items"; exit 0 fi @@ -1437,23 +2415,57 @@ case "$cli" in has_block "$agents" "" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落" has_comment_scope "$agents" || fail "$agents 寫入後讀不到註解範圍規則" has_lang_guard "$agents" || fail "$agents 寫入後讀不到繁中與編碼規則" - printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" + # pre-tool hook:Codex 專屬覆寫寫在 .codex-plugin/plugin.json 的 hooks 鍵,matcher 是 Bash。 + # 這一段是版本前置檢查與部署後重啟閘門在 codex 上真正生效的地方,先前用 Skill matcher, + # 而 Codex 根本沒有 Skill 這個工具,所以兩道閘門從來沒有被叫用過一次。 + cx_manifest=$(codex_manifest) + cx_hooks=$(codex_hooks_file) + [ -f "$cx_manifest" ] || fail "找不到 $cx_manifest,codex 接不到 pre-tool hook" + [ -f "$HOOKS/hooks.json" ] || fail "找不到 $HOOKS/hooks.json,推導不出 codex 專屬的 hook 檔" + [ -f "$cx_hooks" ] && { backup_file "$cx_hooks" || fail "無法備份 $cx_hooks,沒有備份就不覆寫"; } + codex_derive_hooks "$HOOKS/hooks.json" > "$cx_hooks" 2>/dev/null \ + || fail "無法從 $HOOKS/hooks.json 推導出 $cx_hooks" + json_pairs_ok "$cx_hooks" || fail "$cx_hooks 推導後不是成對的 JSON" + json_top_key "$cx_hooks" hooks || fail "$cx_hooks 最上層讀不到 hooks 鍵" + grep -qF '"matcher": "Bash"' "$cx_hooks" 2>/dev/null \ + || fail "$cx_hooks 沒有 Bash matcher,codex 沒有 Skill 工具,擋不到技能載入那一次" + grep -qF '"matcher": "Skill"' "$cx_hooks" 2>/dev/null \ + && fail "$cx_hooks 還留著 Skill matcher,codex 沒有那個工具,那一組永遠不會被叫用" + for _g in restart-gate.sh version-guard.sh; do + grep -qF "$_g" "$cx_hooks" 2>/dev/null \ + || fail "$cx_hooks 沒有接上 $_g,那道閘門在 codex 上不會生效" + done + # 代號單獨驗一項:少了它,兩道閘門認不出現在跑的是哪一支 CLI,技能名解不出來就整批安靜放行。 + grep -qF 'JSC_CLI=codex' "$cx_hooks" 2>/dev/null \ + || fail "$cx_hooks 的命令沒有帶 JSC_CLI=codex,閘門取不到 CLI 代號就解不出技能名,接了也一律放行" + # Claude 那一份是唯一真實來源,推導完要確認它自己沒被動到。 + grep -qF '"matcher": "Skill"' "$HOOKS/hooks.json" 2>/dev/null \ + || fail "$HOOKS/hooks.json 的 Skill matcher 不見了,Claude 那一份不該被這段接線動到" + backup_file "$cx_manifest" || fail "無法備份 $cx_manifest,沒有備份就不改寫" + json_put_top_key "$cx_manifest" hooks "$(codex_hooks_key_block)" \ + || fail "無法把 hooks 鍵寫進 $cx_manifest" + json_top_key "$cx_manifest" hooks || fail "$cx_manifest 寫入後最上層讀不到 hooks 鍵" + grep -qF "\"hooks\": \"$CODEX_HOOKS_REL\"" "$cx_manifest" 2>/dev/null \ + || fail "$cx_manifest 的 hooks 鍵不是指向 $CODEX_HOOKS_REL 的路徑字串;Codex 的 manifest 規格裡這個鍵跟 skills 一樣是路徑,寫成內嵌物件解不出來" + printf 'status=wired reason=%s\n' "pre-tool hook 已接在 hooks/codex-hooks.json 的 Bash matcher 上(manifest 的 hooks 鍵以路徑字串指過去),版本前置檢查與部署後重啟閘門都擋得下來;STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線,註解範圍與繁中編碼每輪結束掃整個工作區,不是逐檔即時" echo "$WIRE_PATH_NOTE" echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。" echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。" echo "[jsc] codex:已設定 $config 的 notify(根層鍵,已驗證),每輪補 session-timer.sh start 再 mark,最後跑 comment-scope.sh sweep 與 lang-guard.sh sweep。" echo "[jsc] codex:已在 $agents 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。" + echo "[jsc] codex:已產出 $cx_hooks(從 hooks/hooks.json 推導,matcher Skill 換成 Bash),並把 $cx_manifest 的 hooks 鍵指到 $CODEX_HOOKS_REL。" + echo "[jsc] codex:那個鍵是路徑字串,不是內嵌物件——Codex 的 plugin manifest 規格裡 hooks 跟 skills 一樣是路徑。" + echo "[jsc] codex:Claude 用的 hooks/hooks.json 沒有動過,那一份仍是 Skill matcher,兩支 CLI 各讀各的。codex 那一份是推導出來的,hooks.json 加了新 hook,重跑接線就會跟著有。" echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" - echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" - echo "[jsc] codex:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" - echo "[jsc] codex:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。" + echo "[jsc] codex:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式還沒接線,那三條規則在這裡目前只剩 SKILL.md 的散文。" echo "[jsc] codex:註解範圍與繁中編碼除了規則提示,每輪結束會由 notify 各掃一次整個 git 工作區(codex 沒有 post-tool hook,接不到逐檔即時掃描),回饋比 claude 晚一輪。" - exit 1 ;; + exit 0 ;; copilot) bin=$(cli_bin copilot) command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔" - instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" + instr=$(copilot_instructions) + cp_settings=$(copilot_settings) alias_line="alias $bin='sh \"$WIRE_TOOLS/jsc-wrap.sh\" copilot'" write_alias_rc "jsc-hooks:copilot" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔" replace_block "$instr" "" "" "$(rules_text)" \ @@ -1461,17 +2473,61 @@ case "$cli" in has_block "$instr" "" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落" has_comment_scope "$instr" || fail "$instr 寫入後讀不到註解範圍規則" has_lang_guard "$instr" || fail "$instr 寫入後讀不到繁中與編碼規則" - printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" + # pre-tool hook:併進 settings.json 的頂層 hooks 鍵,合併不覆寫。 + # 那份檔案同時裝著 enabledPlugins 與 extraKnownMarketplaces,弄壞會讓外掛整批失效, + # 所以寫前備份、寫後逐項回讀驗證,任何一項不過就還原。 + [ -f "$cp_settings" ] || printf '{\n}\n' > "$cp_settings" 2>/dev/null \ + || fail "無法建立 $cp_settings" + json_pairs_ok "$cp_settings" || fail "$cp_settings 不是成對的 JSON,讀不懂就不動它,請先修好這個檔案" + # 動這份檔案之前先記下要保住的東西,寫完拿同一份清單回頭核對。 + cp_top_before=$(json_top_keys "$cp_settings") + cp_others_before=$(copilot_foreign_entries "$cp_settings") + backup_file "$cp_settings" || fail "無法備份 $cp_settings,沒有備份就不改寫" + cp_new=$(mktemp) || fail "無法建立暫存檔" + copilot_hook_entries > "$cp_new" + cp_tmp=$(mktemp) || { rm -f "$cp_new"; fail "無法建立暫存檔"; } + copilot_merge_hooks "$cp_settings" wire "$cp_new" > "$cp_tmp" 2>/dev/null + cp_rc=$? + if [ "$cp_rc" = 2 ]; then + # 還沒有 hooks 鍵,直接新增一個;那條路徑由 json_put_top_key 走,不必再解一次舊值。 + rm -f "$cp_tmp" + json_put_top_key "$cp_settings" hooks \ + "$(printf ' "hooks": {\n "PreToolUse": [\n%s\n ]\n }' "$(cat "$cp_new")")" \ + || { rm -f "$cp_new"; restore_backups; fail "無法把 hooks 鍵寫進 $cp_settings"; } + elif [ "$cp_rc" = 0 ] && [ -s "$cp_tmp" ]; then + cat "$cp_tmp" > "$cp_settings" 2>/dev/null || { rm -f "$cp_new" "$cp_tmp"; restore_backups; fail "無法寫入 $cp_settings"; } + rm -f "$cp_tmp" + else + rm -f "$cp_new" "$cp_tmp"; restore_backups + fail "$cp_settings 的 hooks 鍵解析不出來,已還原備份;這份檔案還裝著 enabledPlugins 與 extraKnownMarketplaces,讀不懂就不動它" + fi + rm -f "$cp_new" + # 回讀驗證:先驗沒弄壞什麼,再驗接上了什麼。順序刻意這樣排—— + # 弄壞那份檔案的後果(十個外掛整批失效)比沒接上嚴重得多。 + json_pairs_ok "$cp_settings" || { restore_backups; fail "$cp_settings 寫入後不是成對的 JSON,已還原備份"; } + [ "$(json_top_keys "$cp_settings")" = "$cp_top_before" ] \ + || { restore_backups; fail "$cp_settings 的最上層鍵有增減,已還原備份;enabledPlugins 與 extraKnownMarketplaces 一個都不能掉"; } + [ "$(copilot_foreign_entries "$cp_settings")" = "$cp_others_before" ] \ + || { restore_backups; fail "$cp_settings 裡別人的 hook 條目被動到了,已還原備份"; } + copilot_hooks_ok "$cp_settings" \ + || { restore_backups; fail "$cp_settings 的 hooks 鍵沒有接上 PreToolUse matcher skill 的兩道閘門,已還原備份"; } + copilot_single_case_ok "$cp_settings" \ + || { restore_backups; fail "$cp_settings 同時出現兩種大小寫的事件名,copilot 兩種都吃,同一支 hook 會跑兩次,已還原備份"; } + # 代號單獨驗一項:少了它,兩道閘門認不出現在跑的是哪一支 CLI,技能名解不出來就整批安靜放行。 + grep -qF 'JSC_CLI=copilot' "$cp_settings" 2>/dev/null \ + || { restore_backups; fail "$cp_settings 的 jsc 條目沒有帶 JSC_CLI=copilot,閘門取不到 CLI 代號就解不出技能名,接了也一律放行,已還原備份"; } + printf 'status=wired reason=%s\n' "pre-tool hook 已併進 ~/.copilot/settings.json 的 hooks 鍵(PreToolUse matcher skill,合併不覆寫),版本前置檢查與部署後重啟閘門都擋得下來;STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" echo "$WIRE_PATH_NOTE" echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。" - echo "[jsc] copilot:已在 $instr 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。" + echo "[jsc] copilot:已在 $instr 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。指引檔一定要在 \$COPILOT_HOME 底下,舊位置 ~/.config/copilot/ 從來沒有被載入過。" + echo "[jsc] copilot:已把 PreToolUse matcher skill 的兩道閘門併進 $cp_settings 的 hooks 鍵(合併,不覆寫)。" + echo "[jsc] copilot:那份檔案還裝著 enabledPlugins 與 extraKnownMarketplaces,寫前已備份、寫後已回讀核對最上層鍵與別人的 hook 條目,一筆都沒動到。" + echo "[jsc] copilot:\$COPILOT_HOME/hooks/ 底下是 hook 要跑的**腳本**,不是設定;設定只認 settings.json 的頂層 hooks 鍵。" echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才生效。" echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" - echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" - echo "[jsc] copilot:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" - echo "[jsc] copilot:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。" + echo "[jsc] copilot:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式還沒接線,那三條規則在這裡目前只剩 SKILL.md 的散文。" echo "[jsc] copilot:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(copilot 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。" - exit 1 ;; + exit 0 ;; antigravity) bin=$(cli_bin antigravity) @@ -1484,74 +2540,90 @@ case "$cli" in has_block "$rules" "" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落" has_comment_scope "$rules" || fail "$rules 寫入後讀不到註解範圍規則" has_lang_guard "$rules" || fail "$rules 寫入後讀不到繁中與編碼規則" - printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" + # pre-tool hook:~/.gemini/config/hooks.json 的最上層一個安裝來源一個命名空間鍵, + # 寫 jsc 那一個不會動到別人的段落。plugin.json 不能宣告 hook,所以只有這一個位置。 + ag_hooks=$(antigravity_hooks_file) + [ -f "$ag_hooks" ] && { backup_file "$ag_hooks" || fail "無法備份 $ag_hooks,沒有備份就不改寫"; } + json_put_top_key "$ag_hooks" jsc "$(antigravity_hooks_block)" \ + || fail "無法把 jsc 段落寫進 $ag_hooks" + json_top_key "$ag_hooks" jsc || fail "$ag_hooks 寫入後最上層讀不到 jsc 段落" + grep -qF 'restart-gate.sh' "$ag_hooks" 2>/dev/null \ + || fail "$ag_hooks 沒有接上 restart-gate.sh,部署後重啟閘門在 antigravity 上不會生效" + grep -qF 'version-guard.sh' "$ag_hooks" 2>/dev/null \ + || fail "$ag_hooks 沒有接上 version-guard.sh,版本前置檢查在 antigravity 上不會生效" + grep -qF '"^view_file$"' "$ag_hooks" 2>/dev/null \ + || fail "$ag_hooks 的 matcher 少了錨點,沒有錨點會連 view_file_outline 一起命中" + grep -qF '"PreInvocation"' "$ag_hooks" 2>/dev/null \ + || fail "$ag_hooks 沒有接 PreInvocation,斜線指令那條路擋不到" + antigravity_grouped_ok "$ag_hooks" \ + || fail "$ag_hooks 的 PreToolUse 不是 Grouped 形狀(matcher 要跟 hooks 包一層),antigravity 會靜默丟棄整個事件" + antigravity_flat_ok "$ag_hooks" \ + || fail "$ag_hooks 的 PreInvocation 被包成 Grouped,那一組事件只吃 Flat,包了會被丟掉" + # 代號單獨驗一項:少了它,deny.sh 會退回結束碼形態,而 antigravity 只認 stdout 的 deny JSON, + # 判定擋下了、CLI 卻收不到拒絕;技能名也一樣解不出來。 + grep -qF 'JSC_CLI=antigravity' "$ag_hooks" 2>/dev/null \ + || fail "$ag_hooks 的命令沒有帶 JSC_CLI=antigravity,閘門取不到 CLI 代號就解不出技能名,阻擋形態也會退回這支 CLI 不認的結束碼" + printf 'status=wired reason=%s\n' "pre-tool hook 已接在 ~/.gemini/config/hooks.json 的 jsc 段落(PreToolUse matcher ^view_file\$ 加 PreInvocation),版本前置檢查與部署後重啟閘門都以 stdout 的 deny JSON 擋得下來;STE100 與 SDLC 模型鎖仍是 prompt 與技能步驟檢查,write-guard.sh 三種模式尚未接線,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" echo "$WIRE_PATH_NOTE" echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。" echo "[jsc] antigravity:已在 $rules 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。" + echo "[jsc] antigravity:已在 $ag_hooks 寫入 jsc 段落,接上 restart-gate.sh 與 version-guard.sh;擋人一律走 stdout 的 deny JSON,這支 CLI 的結束碼語意沒有文件,不可靠。" + echo "[jsc] antigravity:技能沒有專用工具,模型是用 view_file 讀 SKILL.md,所以 matcher 是 ^view_file\$;錨點不能省,省了會連 view_file_outline 一起命中。" + echo "[jsc] antigravity:斜線指令與預載技能會把 SKILL.md 全文直接注入訊息,不產生工具呼叫,那條路由 PreInvocation 接住。" + echo "[jsc] antigravity:hook 觸發本身沒有實跑驗證(本機對話 quota 用盡),接線內容與腳本邏輯已驗,觸發未驗;唯讀確認可跑 agy -p \"/hooks\" 與 agy -p \"/skills\",那兩個指令不吃 quota。" echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才生效。" echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" - echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" - echo "[jsc] antigravity:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" - echo "[jsc] antigravity:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。" + echo "[jsc] antigravity:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式還沒接線,那三條規則在這裡目前只剩 SKILL.md 的散文。" echo "[jsc] antigravity:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(antigravity 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。" - exit 1 ;; + exit 0 ;; kiro) command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔" - hookdir="./.kiro/hooks" - hookfile="$hookdir/jsc-hooks.json" - startfile="$hookdir/jsc-hooks-session-start.json" - mkdir -p "$hookdir" 2>/dev/null || fail "無法建立 $hookdir" - # 計時要分兩個檔:kiro 的一個 hook 檔只有一組 run,所有事件共用。 - # sessionStart 單獨一檔跑 restart,才算得出這一次工作階段的花費時間; - # kiro 給不到 session id,紀錄共用 default,不覆寫起始時間就會把上一階段算進來。 - cat > "$startfile" 2>/dev/null < "$hookfile" 2>/dev/null </dev/null || fail "$f 缺少 JSC_CLI=kiro" - grep -qF 'session-timer.sh' "$f" 2>/dev/null || fail "$f 的 run 沒有接到 session-timer.sh" + kr_agent=$(kiro_agent_file) + kr_settings=$(kiro_cli_settings) + # hook 宣告只認 agent 設定檔的 hooks 鍵。設定目錄常數裡沒有 .kiro/hooks/, + # 那個目錄底下的檔案一份都不會被讀,先前的接線等於寫進空氣裡。 + mkdir -p "$(dirname "$kr_agent")" 2>/dev/null || fail "無法建立 $(dirname "$kr_agent")" + [ -f "$kr_agent" ] && { backup_file "$kr_agent" || fail "無法備份 $kr_agent,沒有備份就不覆寫"; } + kiro_agent_json > "$kr_agent" 2>/dev/null || fail "無法寫入 $kr_agent" + json_pairs_ok "$kr_agent" || fail "$kr_agent 寫入後不是成對的 JSON" + json_top_key "$kr_agent" hooks || fail "$kr_agent 最上層讀不到 hooks 鍵,kiro 只認這個位置" + json_top_key "$kr_agent" resources || fail "$kr_agent 最上層讀不到 resources" + json_top_key "$kr_agent" tools || fail "$kr_agent 最上層讀不到 tools,自訂 agent 沒宣告會限制可用工具" + kiro_agent_shape_ok "$kr_agent" \ + || fail "$kr_agent 的形狀不合格:合法事件只有 agentSpawn、userPromptSubmit、preToolUse、postToolUse、stop,欄位沒有 on、run、env,而且 resources 要有兩層 glob" + # 用真的 kiro-cli 驗一次。判準看輸出、不看結束碼——validate 一律回 0,看結束碼等於沒驗。 + kr_val=$("$(cli_bin kiro)" agent validate --path "$kr_agent" 2>&1) + kiro_validate_output_ok "$kr_val" \ + || fail "$kr_agent 沒通過 kiro-cli agent validate:$(printf '%s' "$kr_val" | tr '\n' ' ' | cut -c1-200)" + # 兩層 glob 是技能看不看得到的關鍵:預設只掃一層,jsc 的技能在第二層。 + grep -qF '/skills/*/*/SKILL.md' "$kr_agent" 2>/dev/null \ + || fail "$kr_agent 的 resources 少了兩層 glob,jsc 技能一支都載不到" + 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" done - grep -qF '"sessionStart"' "$startfile" 2>/dev/null || fail "$startfile 沒有接在 sessionStart" - grep -qF '"userPromptSubmit"' "$hookfile" 2>/dev/null || fail "$hookfile 沒有接在 userPromptSubmit" - grep -qF 'comment-scope.sh' "$hookfile" 2>/dev/null || fail "$hookfile 的 run 沒有接到 comment-scope.sh" - # 兩個模式接的是兩件事,只驗腳本名會漏掉少接的那一個,所以各驗一次 - grep -qF 'comment-scope.sh\" prompt' "$hookfile" 2>/dev/null \ - || fail "$hookfile 的 run 沒有接到 comment-scope.sh prompt,每輪不會注入註解範圍規則" - grep -qF 'comment-scope.sh\" sweep' "$hookfile" 2>/dev/null \ - || fail "$hookfile 的 run 沒有接到 comment-scope.sh sweep,kiro 每輪不會掃註解範圍" - grep -qF 'lang-guard.sh' "$hookfile" 2>/dev/null || fail "$hookfile 的 run 沒有接到 lang-guard.sh" - grep -qF 'lang-guard.sh\" prompt' "$hookfile" 2>/dev/null \ - || fail "$hookfile 的 run 沒有接到 lang-guard.sh prompt,每輪不會注入繁中與編碼規則" - grep -qF 'lang-guard.sh\" sweep' "$hookfile" 2>/dev/null \ - || fail "$hookfile 的 run 沒有接到 lang-guard.sh sweep,kiro 每輪不會掃簡體字與亂碼" - printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" + # 這個 agent 要被選用才算數,所以還要設 chat.defaultAgent。內建 agent 改不了, + # 不設就一路走內建的 kiro_default,寫好的 agent 檔一次都不會被讀。 + [ -f "$kr_settings" ] && { backup_file "$kr_settings" || fail "無法備份 $kr_settings,沒有備份就不改寫"; } + json_put_top_key "$kr_settings" chat.defaultAgent ' "chat.defaultAgent": "jsc"' \ + || fail "無法把 chat.defaultAgent 寫進 $kr_settings" + grep -qF '"chat.defaultAgent": "jsc"' "$kr_settings" 2>/dev/null \ + || fail "$kr_settings 寫入後讀不到 chat.defaultAgent=jsc" + printf 'status=degraded reason=%s\n' "kiro 擋不下技能叫用——技能走 ResolveSkill 這個 agent 內部請求,preToolUse 攔不到,userPromptSubmit 的非零結束碼也不會擋下那一輪,所以版本前置檢查與部署後重啟閘門只能以 stdout 注入警告。這是 CLI 的限制,不是接線缺漏:hook、resources 與預設 agent 都已寫進 ~/.kiro/agents/jsc.json 與 settings/cli.json" echo "$WIRE_PATH_NOTE" - echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。" - echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" - echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" - echo "[jsc] kiro:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" - echo "[jsc] kiro:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。" - echo "[jsc] kiro:註解範圍與繁中編碼除了規則提示,每輪提示送出時會各掃一次整個 git 工作區(kiro 沒有 post-tool hook),掃到的是上一輪寫的檔。" + echo "[jsc] kiro:已建立 $kr_agent(hooks、resources、tools 三段齊全)並把 $kr_settings 的 chat.defaultAgent 設成 jsc。" + echo "[jsc] kiro:resources 寫了一層與兩層兩條 skill:// glob。預設只掃一層,jsc 的技能在 jsc-{domain}/{name}/SKILL.md 第二層,少了那一條一支都載不到;一層那一條留著,別人的技能不受影響。" + echo "[jsc] kiro:tools 明寫成 [\"*\"]。自訂 agent 沒宣告 tools 時可用工具會受限,模型連讀檔都做不到。" + if kiro_validate_ran "$kr_val"; then + echo "[jsc] kiro:已用 kiro-cli agent validate 驗過(輸出為空)。判準是**輸出**不是結束碼——那支指令四種情況一律回 0,看結束碼會做出一支永遠通過的檢查。" + else + echo "[jsc] kiro:kiro-cli agent validate 這一項**沒驗到**(它印的不是這個檔案的問題,通常是沒登入):$(printf '%s' "$kr_val" | tr '\n' ' ' | cut -c1-160)" + echo "[jsc] kiro:驗不了不等於驗不過,所以照樣放行;要補驗就登入後重跑接線。" + fi + echo "[jsc] kiro:合法事件只有 agentSpawn、userPromptSubmit、preToolUse、postToolUse、stop;sessionStart 與 sessionEnd 都不合法。舊版寫在最上層的 on/run/env 是**未知鍵,被靜默忽略**,validate 一個錯都不報,那份檔案卻什麼都沒做。" + echo "[jsc] kiro:版本前置檢查與部署後重啟閘門只注入警告,擋不下技能叫用,那是 CLI 的限制。要真的擋,只能靠使用者看到警告後自己停手。" + echo "[jsc] kiro:hook 觸發與 agent 設定讀取都沒有實跑驗證(本機未登入),接線內容與腳本邏輯已驗,觸發未驗。" + echo "[jsc] kiro:舊的 .kiro/hooks/ 接線已作廢,那個目錄不在 kiro 的設定目錄常數裡,一份都不會被讀。" exit 1 ;; + esac