Merge pull request '收攏 frontmatter 檢查與五支 CLI 的 hook 接線準則改寫' (#51) from feat/cli-hook-rewire/main into develop
This commit was merged in pull request #51.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-meta",
|
"name": "jsc-meta",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-meta",
|
"name": "jsc-meta",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
### `skill-check`
|
### `skill-check`
|
||||||
|
|
||||||
例行稽核——沒有變更需求時,同步存取庫之後併行跑三組:`lint-scripts.sh` 加 `check-behaviors.sh` 加 hook smoke、準則審核檢查清單、流程與成本優化審查。優化面向包含可平行化、可下放工具、重複來回、冗餘步驟、過早或過晚的閘門與可省的成本;不符項目與優化建議分開回報,逐項決策樹確認後才套用,最後逐 repo 開 PR。有變更需求改用 skillset-update。
|
例行稽核——沒有變更需求時,同步存取庫之後併行跑三組:`lint-scripts.sh` 加 `lint-frontmatter.sh` 加 `check-behaviors.sh` 加 hook smoke、準則審核檢查清單、流程與成本優化審查。優化面向包含可平行化、可下放工具、重複來回、冗餘步驟、過早或過晚的閘門與可省的成本;不符項目與優化建議分開回報,逐項決策樹確認後才套用,最後逐 repo 開 PR。有變更需求改用 skillset-update。
|
||||||
|
|
||||||
### `ste100-sync`
|
### `ste100-sync`
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| `tools/plugins-root.sh` | 推導技能組工作目錄的根,六支腳本共用。以 plugin 形式安裝時「腳本上兩層」會落在快取目錄,所以推導規則抽出來;推不出來 exit 1 並指名要設 `JSC_PLUGINS_ROOT` |
|
| `tools/plugins-root.sh` | 推導技能組工作目錄的根,六支腳本共用。以 plugin 形式安裝時「腳本上兩層」會落在快取目錄,所以推導規則抽出來;推不出來 exit 1 並指名要設 `JSC_PLUGINS_ROOT` |
|
||||||
| `tools/ste100-lint.sh` | 語言規則的機檢工具:中國用語、中文句內半形標點、AI 套話、簡體字、中文並列斜線;命中 exit 1,沒給檢查對象 exit 2 |
|
| `tools/ste100-lint.sh` | 語言規則的機檢工具:中國用語、中文句內半形標點、AI 套話、簡體字、中文並列斜線;命中 exit 1,沒給檢查對象 exit 2 |
|
||||||
| `tools/lint-scripts.sh` | 一個 domain 的腳本檢查三合一:`sh -n` 語法、執行權限、檔頭結束碼宣告;有不合格 exit 1,沒有腳本可掃 exit 3(**不等於通過**) |
|
| `tools/lint-scripts.sh` | 一個 domain 的腳本檢查三合一:`sh -n` 語法、執行權限、檔頭結束碼宣告;有不合格 exit 1,沒有腳本可掃 exit 3(**不等於通過**) |
|
||||||
|
| `tools/lint-frontmatter.sh` | 一個 domain 每支 `skills/*/SKILL.md` 的 frontmatter 解析檢查:分隔線成對、必要鍵齊全、未加引號的純量不含「冒號加空白」也不以 YAML 特殊字元起頭、引號收得起來。不相依任何 YAML 套件。不合格 exit 1(清單在 stderr),用法錯誤 exit 2,沒有 SKILL.md 可掃 exit 3(**不等於通過**)。frontmatter 壞掉時 Antigravity 會**靜默丟棄整支技能**,沒有任何錯誤訊息 |
|
||||||
| `tools/check-behaviors.sh` | 比對一個 domain 的 `references/behaviors.md` 與 `skills/`:節對技能、字典序、每節一張表、五個欄位齊全且內容欄非空;不符 exit 1,用法錯誤 exit 2,找不到清單或找不到技能 exit 3(**不等於通過**) |
|
| `tools/check-behaviors.sh` | 比對一個 domain 的 `references/behaviors.md` 與 `skills/`:節對技能、字典序、每節一張表、五個欄位齊全且內容欄非空;不符 exit 1,用法錯誤 exit 2,找不到清單或找不到技能 exit 3(**不等於通過**) |
|
||||||
| `tools/deploy-route.sh` | 判定改動有沒有進存取庫的預設分支,決定走部署路線(exit 0)或工作樹路線(exit 3);判不出來 exit 1,**不等於工作樹路線** |
|
| `tools/deploy-route.sh` | 判定改動有沒有進存取庫的預設分支,決定走部署路線(exit 0)或工作樹路線(exit 3);判不出來 exit 1,**不等於工作樹路線** |
|
||||||
| `tools/sync-domains.sh` | 依 Gitea 正本 marketplace 把所有 domain 存取庫 clone 或 pull 到本機,印出 `domain<TAB>path`;**只有 exit 0 代表全部到位且最新**,exit 3 代表有存取庫跳過或 pull 失敗(stderr 列路徑),exit 2 代表有 domain clone 失敗 |
|
| `tools/sync-domains.sh` | 依 Gitea 正本 marketplace 把所有 domain 存取庫 clone 或 pull 到本機,印出 `domain<TAB>path`;**只有 exit 0 代表全部到位且最新**,exit 3 代表有存取庫跳過或 pull 失敗(stderr 列路徑),exit 2 代表有 domain clone 失敗 |
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-meta",
|
"name": "jsc-meta",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
"description": "技能組自我管理:新建、更新、刪除技能與技能準則",
|
||||||
"skills": "./skills/",
|
"skills": "./skills/",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 觸發時機 | 手上沒有異動需求,要對整組技能做例行或臨時稽核時用。帶著異動需求要改多支技能走 skillset-update、只改一支走 skill-update |
|
| 觸發時機 | 手上沒有異動需求,要對整組技能做例行或臨時稽核時用。帶著異動需求要改多支技能走 skillset-update、只改一支走 skill-update |
|
||||||
| 關鍵步驟 | 先跑 sync-domains.sh 同步全部 domain 存取庫、再平行跑三組審查(第一組跑腳本檢查、行為清單檢查與 hook smoke、第二組以 sub agent 逐 domain 對 guidelines 檢查清單稽核、第三組以 sub agent 分六個面向審查流程與成本)、合併三組結果並用決策樹逐項確認、以平行 sub agent 套用確認過的修正並跑 sync-skill-manifest.sh、跑 sync-marketplace.sh 同步兩份正本 marketplace、重跑三組驗證直到接受的修正全通過、每個受影響存取庫各開一條 PR |
|
| 關鍵步驟 | 先跑 sync-domains.sh 同步全部 domain 存取庫、再平行跑三組審查(第一組平行跑腳本檢查、frontmatter 檢查、行為清單檢查與 hook smoke,第二組以 sub agent 逐 domain 對 guidelines 檢查清單稽核,第三組以 sub agent 分六個面向審查流程與成本)、合併三組結果並用決策樹逐項確認(第二組留白的五項由第一組的結論補上)、以平行 sub agent 套用確認過的修正並跑 sync-skill-manifest.sh、跑 sync-marketplace.sh 同步兩份正本 marketplace、重跑三組驗證直到接受的修正全通過、每個受影響存取庫各開一條 PR |
|
||||||
| 外部呼叫 | tools/sync-domains.sh、tools/lint-scripts.sh、tools/check-behaviors.sh、tools/sync-skill-manifest.sh、tools/sync-marketplace.sh、jsc-cli/tools/detect-clis.sh、jsc-hooks/tools/wire-cli.sh smoke、jsc-ask:ask、jsc-git:pr |
|
| 外部呼叫 | tools/sync-domains.sh、tools/lint-scripts.sh、tools/lint-frontmatter.sh、tools/check-behaviors.sh、tools/sync-skill-manifest.sh、tools/sync-marketplace.sh、jsc-cli/tools/detect-clis.sh、jsc-hooks/tools/wire-cli.sh smoke、jsc-ask:ask、jsc-git:pr |
|
||||||
| 完成條件 | 每個 domain 都有腳本檢查與行為清單檢查的結論、每個 domain 的檢查清單在合併後補齊、每項不合規與每項優化建議都有決策紀錄、接受的修正重驗通過、每個受影響存取庫都拿到 PR 網址 |
|
| 完成條件 | 每個 domain 都有腳本檢查、frontmatter 檢查與行為清單檢查的結論(frontmatter 檢查退出 3 是「什麼都沒掃」,不算通過)、每個 domain 的檢查清單在合併後補齊、每項不合規與每項優化建議都有決策紀錄、接受的修正重驗通過、每個受影響存取庫都拿到 PR 網址 |
|
||||||
| 可驗證跡象 | 受影響存取庫留下檔案改動、改到行為的技能連帶改寫該存取庫的 references/behaviors.md、README 的「Skills 目錄」重寫、三份 manifest 版本號提升、兩份 marketplace 檔逐位元一致、每個受影響存取庫一條 PR |
|
| 可驗證跡象 | 受影響存取庫留下檔案改動、改到行為的技能連帶改寫該存取庫的 references/behaviors.md、每個 domain 的 lint-frontmatter.sh 退出 0、README 的「Skills 目錄」重寫、三份 manifest 版本號提升、兩份 marketplace 檔逐位元一致、每個受影響存取庫一條 PR |
|
||||||
|
|
||||||
## skill-delete
|
## skill-delete
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,28 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
|
|||||||
1. 所有 hook 專屬存放於 `jsc-hooks`,**不可散落在其他 domain**。
|
1. 所有 hook 專屬存放於 `jsc-hooks`,**不可散落在其他 domain**。
|
||||||
2. Hook 腳本實作優先順序:**shell > nodejs > python**。
|
2. Hook 腳本實作優先順序:**shell > nodejs > python**。
|
||||||
3. Hook 必須適用於 claude / codex / copilot / antigravity / kiro 五種 CLI:
|
3. Hook 必須適用於 claude / codex / copilot / antigravity / kiro 五種 CLI:
|
||||||
- 腳本同時支援 stdin JSON(Claude 格式)與環境變數輸入,缺欄位時安靜降級(exit 0)。
|
- 五支 CLI 的 hook 負載形態各不相同,沒有哪一支是基準格式。腳本吃 stdin JSON,也吃環境變數,缺欄位時安靜降級(exit 0)。
|
||||||
- 各 CLI 的接線方式由 `jsc-hooks:hooks-install` 技能處理。
|
- 各 CLI 的接線位置、事件名與 matcher 由 `jsc-hooks:hooks-install` 技能處理,接線位置表見「版本前置檢查」。
|
||||||
|
- 負載解析與阻擋輸出不各寫一份,一律走下面兩支共用腳本。
|
||||||
|
|
||||||
|
### 技能名解析與阻擋輸出的共用腳本
|
||||||
|
|
||||||
|
| 腳本 | 用法 | 做什麼 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `jsc-hooks/hooks/skill-name.sh` | `skill-name.sh {claude\|codex\|copilot\|antigravity\|kiro}` | 從 stdin 讀該 CLI 的 hook 負載,印出一行 `{domain}<TAB>{技能名}`。解析不出就印空字串並退出 0,由呼叫端安靜放行 |
|
||||||
|
| `jsc-hooks/hooks/deny.sh` | `deny.sh {cli}`,訊息從參數或 stdin 進 | 依該 CLI 的阻擋形態輸出:claude、codex、copilot 走結束碼 2 加 stderr;antigravity 走 stdout `{"decision":"deny","reason":"..."}`,**不可靠結束碼**;kiro 擋不了,改印警告到 stdout 供注入並退出 0 |
|
||||||
|
|
||||||
|
各 CLI 的技能名取值來源:
|
||||||
|
|
||||||
|
| CLI | 取自 |
|
||||||
|
| --- | --- |
|
||||||
|
| claude | stdin JSON 的 `skill` 欄位,或環境變數 `JSC_SKILL` |
|
||||||
|
| codex | `tool_input.command` 裡的 `SKILL.md` 路徑 |
|
||||||
|
| copilot | `toolArgs` 裡的技能名。`toolArgs` 是字串化的 JSON,**要剝兩層** |
|
||||||
|
| antigravity | `toolCall.args.AbsolutePath`。args 鍵名是 PascalCase |
|
||||||
|
| kiro | `prompt` 開頭的 `/{技能名}` |
|
||||||
|
|
||||||
|
**為什麼要抽出來。** 三種負載形態(工具名、指令字串、檔案路徑)指向同一件事:從負載取出 domain 與技能名。同一套規則寫進兩支 hook 就會漂移——改了 `version-guard.sh`、忘了 `restart-gate.sh`,其中一道閘門就在某支 CLI 上安靜失效,而且失效不會報錯,跟 2026-08-31 抓到的接線缺陷是同一種病。抽成一支之後只有一份真實來源,CLI 換了負載形態也只改一個地方。阻擋輸出同理:五支 CLI 四種形態,寫散了就會有人拿 claude 的結束碼去擋 antigravity,而 antigravity 的結束碼語意兩邊文件都沒寫,擋不擋得住純靠運氣。
|
||||||
|
|
||||||
## 技能設計
|
## 技能設計
|
||||||
|
|
||||||
@@ -137,9 +157,36 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
|
|||||||
|
|
||||||
## 版本前置檢查
|
## 版本前置檢查
|
||||||
|
|
||||||
技能組的每一支技能在被呼叫前都要過兩道版本檢查:本機載入版本沒有落後遠端發佈版本,以及這支技能所屬 plugin 宣告的 `jsc.requires` 每一項都吃得到。兩道判定都在程式層,由 `jsc-hooks` 的 `version-guard.sh`(PreToolUse,matcher `Skill`)執行,**不靠技能內文自我約束**——寫在內文的規則,模型可以無視。
|
技能組的每一支技能在被呼叫前都要過兩道版本檢查:本機載入版本沒有落後遠端發佈版本,以及這支技能所屬 plugin 宣告的 `jsc.requires` 每一項都吃得到。兩道判定都在程式層,由 `jsc-hooks` 的 `version-guard.sh` 執行,**不靠技能內文自我約束**——寫在內文的規則,模型可以無視。
|
||||||
|
|
||||||
兩道都只擋「確定落後」一種情況。查不到任何一項基礎資訊就安靜放行(exit 0),不要求先修好環境:五支 CLI 只有 claude 讀得到本機載入版本,fail-closed 會把另外四支整批鎖死。
|
兩道都只擋「確定落後」一種情況。查不到任何一項基礎資訊就安靜放行(exit 0),不要求先修好環境:只有 claude 讀得到 `installed_plugins.json` 那份本機載入版本,fail-closed 會把另外四支整批鎖死。這是**版本讀得到讀不到**的限制,跟能不能阻擋是兩件事,不要混談。
|
||||||
|
|
||||||
|
### 五支 CLI 的接線位置
|
||||||
|
|
||||||
|
**五支裡有四支都有能阻擋的 pre-tool 事件,kiro 是唯一例外。** 以下每一列都經過執行檔抽出或本機實測。
|
||||||
|
|
||||||
|
| CLI | 事件與 matcher | 接線寫到哪 | 阻擋方式 | verdict |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| claude | `PreToolUse`,matcher `Skill` | `jsc-hooks/hooks/hooks.json` | 結束碼 2 加 stderr | wired |
|
||||||
|
| codex | `PreToolUse`,matcher 對 `tool_name` 做正規表示式比對,用 `Bash` 與 `Write\|Edit\|MultiEdit` | `.codex-plugin/plugin.json` 的 `hooks` 鍵 | 結束碼 2 加 stderr,或 stdout 回 `permissionDecision: deny` | wired |
|
||||||
|
| copilot | `PreToolUse`,matcher `skill`(小寫) | `$COPILOT_HOME/hooks/jsc-hooks.json` | stdout 回 `{"permissionDecision":"deny","permissionDecisionReason":"..."}`,或結束碼 2 | wired |
|
||||||
|
| antigravity | `PreToolUse` 加 `PreInvocation`,matcher `^view_file$` | `~/.gemini/config/hooks.json` 的 jsc 標記段落 | stdout 回 `{"decision":"deny","reason":"..."}` | wired |
|
||||||
|
| kiro | `userPromptSubmit`(hook 宣告在 agent 設定檔的 `hooks` 鍵) | `~/.kiro/agents/jsc.json`,並設 `chat.defaultAgent=jsc` | **擋不了**。只能把警告印到 stdout 供注入,退出 0 | degraded |
|
||||||
|
|
||||||
|
每一支的陷阱,接線與改動時逐條核對:
|
||||||
|
|
||||||
|
| CLI | 陷阱 |
|
||||||
|
| --- | --- |
|
||||||
|
| codex | Codex **沒有 `Skill` 工具**。技能是模型自己用 `Bash` 讀 `SKILL.md` 載進來的,matcher 寫 `Skill` 等於沒接。非受管 hook 要先審核,內容一改就重新標記待審 |
|
||||||
|
| copilot | command hook 是 **fail-closed**:崩潰或任何非零結束碼都算拒絕,但**逾時 fail-open**。所以那支腳本的每一條非預期路徑都要明確 `exit 0`。事件名 PascalCase 與 camelCase 都吃,兩種同時存在會**跑兩次** |
|
||||||
|
| antigravity | **沒有專用的技能工具**,系統提示要求模型用 `view_file` 讀 `SKILL.md`。matcher 的錨點一定要寫,`view_file` 不加錨點會誤中 `view_file_outline`。**結束碼語意兩邊文件都沒寫,絕對不可靠 exit code**。斜線指令與預載技能直接把 `SKILL.md` 全文注入訊息,不產生工具呼叫,那條路徑擋不住 |
|
||||||
|
| kiro | 技能**不走工具管線**,是 `ResolveSkill` 這個 agent 內部請求、由前端發起,所以 `preToolUse` 攔不到技能叫用;`userPromptSubmit` 的非零結束碼也不會擋下那一輪。合法 trigger 只有 `agentSpawn`、`userPromptSubmit`、`preToolUse`、`postToolUse`、`stop`,`sessionStart` 與 `sessionEnd` 不是合法事件。hook 只認 agent 設定檔的 `hooks` 鍵,`.kiro/hooks/` 不被讀 |
|
||||||
|
|
||||||
|
**未實測的部分要據實標明。** antigravity 與 kiro 的 hook 觸發都沒有實跑驗證——前者對話 quota 用盡、後者未登入。這兩支的接線位置與欄位結構是從執行檔抽出來的事實,但「hook 真的被觸發」還沒看到。回報時不得把這兩支混進「已驗證」的結論。
|
||||||
|
|
||||||
|
**為什麼要留這段。** 這一節以前寫著「只有 claude 接得上,其餘四支沒有 pre-tool hook」,那是錯的。四支全都有能阻擋的 pre-tool 事件,是我們接錯位置:codex 用了它根本沒有的 `Skill` matcher,copilot 與 antigravity 完全沒接,kiro 連接線位置、事件名、欄位結構三者都錯。錯誤的結論被寫進準則之後,就沒有人再去查——版本前置檢查與部署後重啟閘門因此在四支 CLI 上長期失效,而失效是安靜的:hook 沒被觸發不會報錯,閘門沒擋下來看起來就跟「沒有東西該擋」一樣。**一道護欄回報「這裡沒有能力」時,要先確認那是查證過的事實,不是沒查。**
|
||||||
|
|
||||||
|
kiro 是唯一真的擋不了的,verdict 據實寫 `degraded`,不寫 `wired` 也不寫 `failed`:那是 CLI 的限制,不是我們接錯。
|
||||||
|
|
||||||
| 項目 | 規則 |
|
| 項目 | 規則 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -171,7 +218,7 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
|
|||||||
|
|
||||||
相依版本檢查移到這裡,是因為 `deploy.sh update` 原本會跳過不符的 domain,跳過就永遠更新不到,理由見「Manifest 相依版本」第 5 條。更新照跑、呼叫才擋,落後的 domain 才有路徑補上來。
|
相依版本檢查移到這裡,是因為 `deploy.sh update` 原本會跳過不符的 domain,跳過就永遠更新不到,理由見「Manifest 相依版本」第 5 條。更新照跑、呼叫才擋,落後的 domain 才有路徑補上來。
|
||||||
|
|
||||||
沒有 pre-tool hook 的 CLI 接不上這道檢查,`hooks-install` 要據實回報,不得暗示每個 CLI 都有保護。
|
`hooks-install` 要據實回報每一支的 verdict:claude、codex、copilot、antigravity 是 `wired`,kiro 是 `degraded`。不得暗示每個 CLI 都擋得住,也不得反過來暗示只有 claude 有保護。
|
||||||
|
|
||||||
## 部署後重啟閘門
|
## 部署後重啟閘門
|
||||||
|
|
||||||
@@ -181,11 +228,14 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 狀態檔 | `$JSC_HOME/restart-required.d/{cli}`,**一支 CLI 一份**,由 `jsc-cli:deploy` 收尾寫入 |
|
| 狀態檔 | `$JSC_HOME/restart-required.d/{cli}`,**一支 CLI 一份**,由 `jsc-cli:deploy` 收尾寫入 |
|
||||||
| 清除時機 | 重啟 CLI 之後由 `jsc-hooks` 清除**自己那一份**,不必手動刪 |
|
| 清除時機 | 重啟 CLI 之後由 `jsc-hooks` 清除**自己那一份**,不必手動刪 |
|
||||||
| 該 CLI 那份存在時 | 擋下這支 CLI 的 jsc 技能呼叫,印出要重啟哪一支與狀態檔路徑 |
|
| 該 CLI 那份存在時 | 擋下這支 CLI 的 jsc 技能呼叫,印出要重啟哪一支與狀態檔路徑。kiro 擋不了,改注入警告 |
|
||||||
| 該 CLI 那份不存在時 | 放行。別支 CLI 的狀態檔不影響這一支 |
|
| 該 CLI 那份不存在時 | 放行。別支 CLI 的狀態檔不影響這一支 |
|
||||||
| 判定位置 | 程式層,由 `jsc-hooks` 執行,不靠技能內文自我約束 |
|
| 判定位置 | 程式層,由 `jsc-hooks/hooks/restart-gate.sh` 執行,不靠技能內文自我約束 |
|
||||||
|
| 接線位置 | 與版本前置檢查完全相同,逐支見「版本前置檢查」的接線位置表 |
|
||||||
| 逃生門 | `JSC_RESTART_GATE=off` |
|
| 逃生門 | `JSC_RESTART_GATE=off` |
|
||||||
|
|
||||||
|
**這道閘門在五支 CLI 上的能力,跟版本前置檢查一模一樣。** claude、codex、copilot、antigravity 都有能阻擋的 pre-tool 事件,接上去就真的擋得住,verdict 是 `wired`;kiro 的技能叫用不走工具管線,攔不到,只能在 `userPromptSubmit` 注入警告,verdict 是 `degraded`。這一節以前跟著「只有 claude 有 pre-tool hook」那個錯誤結論走,所以重啟閘門也在四支 CLI 上長期失效:部署完照樣跑舊版技能,沒有任何東西擋,也沒有任何東西報錯。**兩道閘門共用同一套接線,就共用同一份事實表**,不要在這一節另寫一份能力描述——寫兩份就會只改一份,另一份繼續錯著。
|
||||||
|
|
||||||
**豁免清單**(狀態檔存在也放行):
|
**豁免清單**(狀態檔存在也放行):
|
||||||
|
|
||||||
| 技能 | 為什麼不能擋 |
|
| 技能 | 為什麼不能擋 |
|
||||||
@@ -270,6 +320,7 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
|
|||||||
- [ ] SKILL.md 整份為英文(要原樣輸出的繁中字面除外);README、AGENTS、templates、references 為 STE100 繁中;UTF-8 無亂碼
|
- [ ] SKILL.md 整份為英文(要原樣輸出的繁中字面除外);README、AGENTS、templates、references 為 STE100 繁中;UTF-8 無亂碼
|
||||||
- [ ] 所有非程式碼輸出(程式碼註解、commit 訊息、PR 描述、wiki 頁、回報、文件)為繁體中文、UTF-8、無亂碼、無簡體字,且 `tools/ste100-lint.sh` 對該 domain 全綠
|
- [ ] 所有非程式碼輸出(程式碼註解、commit 訊息、PR 描述、wiki 頁、回報、文件)為繁體中文、UTF-8、無亂碼、無簡體字,且 `tools/ste100-lint.sh` 對該 domain 全綠
|
||||||
- [ ] 該 domain 的 `references/behaviors.md` 與 `skills/` 相符,`tools/check-behaviors.sh {domain-path}` 對該 domain 退出 0;退出 3 是「什麼都沒查」,不算通過
|
- [ ] 該 domain 的 `references/behaviors.md` 與 `skills/` 相符,`tools/check-behaviors.sh {domain-path}` 對該 domain 退出 0;退出 3 是「什麼都沒查」,不算通過
|
||||||
|
- [ ] 該 domain 每支 `skills/*/SKILL.md` 的 frontmatter 解析得動,`tools/lint-frontmatter.sh {domain-path}` 對該 domain 退出 0;退出 3 是「什麼都沒掃」,不算通過。frontmatter 有語法錯誤時,Antigravity 會**靜默丟棄整支技能**,沒有任何錯誤訊息,只有這支腳本抓得到
|
||||||
- [ ] 已同步更新該 domain 的 README「Skills 目錄」與三份 manifest 的 version
|
- [ ] 已同步更新該 domain 的 README「Skills 目錄」與三份 manifest 的 version
|
||||||
- [ ] PR 的 base 符合「PR 分支階梯」,沒有越級
|
- [ ] PR 的 base 符合「PR 分支階梯」,沒有越級
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: skill-check
|
name: skill-check
|
||||||
description: Routine compliance, script, hook, flow-efficiency, and cost-efficiency audit of the whole jsc skill set with no change request in hand. Sync every domain repo from the Gitea canonical marketplace, then run three parallel groups - lint-scripts.sh plus check-behaviors.sh plus hook smoke, the guidelines.md checklist audit, and a review of parallelism, tool extraction, repeated interaction, redundant checks, misplaced gates, and avoidable token, sub-agent, API, scan, or interaction cost. Confirm compliance fixes and optimization suggestions before applying them, re-check until accepted fixes pass, then open a PR per affected repo via jsc-git pr. Use for periodic or on-demand skill-set checks; not for applying a change request (use skillset-update) or editing one skill (use skill-update).
|
description: Routine compliance, script, hook, flow-efficiency, and cost-efficiency audit of the whole jsc skill set with no change request in hand. Sync every domain repo from the Gitea canonical marketplace, then run three parallel groups - lint-scripts.sh plus lint-frontmatter.sh plus check-behaviors.sh plus hook smoke, the guidelines.md checklist audit, and a review of parallelism, tool extraction, repeated interaction, redundant checks, misplaced gates, and avoidable token, sub-agent, API, scan, or interaction cost. Confirm compliance fixes and optimization suggestions before applying them, re-check until accepted fixes pass, then open a PR per affected repo via jsc-git pr. Use for periodic or on-demand skill-set checks; not for applying a change request (use skillset-update) or editing one skill (use skill-update).
|
||||||
---
|
---
|
||||||
|
|
||||||
# skill-check — audit compliance, flow efficiency, and cost efficiency
|
# skill-check — audit compliance, flow efficiency, and cost efficiency
|
||||||
@@ -14,12 +14,13 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references
|
|||||||
The three review groups of step 2 all read this synced tree, so the sync finishes first.
|
The three review groups of step 2 all read this synced tree, so the sync finishes first.
|
||||||
2. Run the three review groups over the synced repos. They are independent — every one only reads, none writes a file — so **launch all three in parallel** and merge their results in step 3.
|
2. Run the three review groups over the synced repos. They are independent — every one only reads, none writes a file — so **launch all three in parallel** and merge their results in step 3.
|
||||||
|
|
||||||
**Group 1 — validate scripts, behavior lists, and hooks.**
|
**Group 1 — validate scripts, frontmatter, behavior lists, and hooks.**
|
||||||
1. For every synced domain repo, run `tools/lint-scripts.sh {domain-path}`. One run per domain, and the runs go **in parallel** — no domain's verdict depends on another's. The tool covers three checks in one pass: `sh -n` syntax, executable bit, and an exit-code declaration in the file header. Route each exit code: 0 — the domain's scripts pass all three; 1 — the failing items are printed as `{file}:{check}:{detail}`, so report each one; 2 — usage error, the tool takes exactly one argument; 3 — nothing was scanned, because the path is missing or the domain has neither `tools/` nor `hooks/`. Record exit 3 as 「無腳本可掃」; a domain with no script directory is not a failure, but exit 3 is **never** a pass.
|
1. For every synced domain repo, run `tools/lint-scripts.sh {domain-path}`. One run per domain, and the runs go **in parallel** — no domain's verdict depends on another's. The tool covers three checks in one pass: `sh -n` syntax, executable bit, and an exit-code declaration in the file header. Route each exit code: 0 — the domain's scripts pass all three; 1 — the failing items are printed as `{file}:{check}:{detail}`, so report each one; 2 — usage error, the tool takes exactly one argument; 3 — nothing was scanned, because the path is missing or the domain has neither `tools/` nor `hooks/`. Record exit 3 as 「無腳本可掃」; a domain with no script directory is not a failure, but exit 3 is **never** a pass.
|
||||||
2. For every synced domain repo, run `tools/check-behaviors.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs — no domain's verdict depends on another's. It compares `references/behaviors.md` against `skills/`: section per skill, dictionary order, one table per section, five rows, no empty content cell. Route each exit code: 0 — that domain's behavior list matches; 1 — the mismatches are printed on stderr as `{檔案}:{技能名}:{說明}`, so report every one as a compliance failure with the skill it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because `references/behaviors.md` is missing, `skills/` is missing, or no `SKILL.md` was found. Record exit 3 as 「無清單可查」with the cause from stderr and carry it into the step 3 merge; a domain with no behavior list is a compliance failure, and exit 3 is **never** a pass.
|
2. For every synced domain repo, run `tools/lint-frontmatter.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs. It parses the frontmatter of every `skills/*/SKILL.md` without a YAML library — paired `---` delimiters, the required `name` and `description` keys, unquoted scalars carrying a colon-space or ending in a colon, unquoted scalars opening with `&`, `*`, `!`, `|`, `>`, `%`, `@` or a backtick, and quoted scalars that never close. Route each exit code: 0 — every SKILL.md in that domain parses; 1 — the failures are printed on stderr as `{檔案}:{鍵}:{說明}`, so report every one as a compliance failure with the file and key it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was scanned, because the domain path or `skills/` is missing, or `skills/` holds no `SKILL.md`. Record exit 3 as 「無 frontmatter 可掃」with the cause from stderr and carry it into the step 3 merge; exit 3 is **never** a pass. This check exists because a broken frontmatter makes Antigravity drop the whole skill with **no error message at all** — 34 skills on disk loaded as 28, and only a file-by-file comparison found it.
|
||||||
3. For every shell script directly named by a SKILL.md, confirm the skill routes every exit code the script's header declares. `lint-scripts.sh` proves the script exists and declares its codes; this check is the other half — that the caller branches on each of them. Report evidence as `skill file:line -> script path`.
|
3. For every synced domain repo, run `tools/check-behaviors.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs — no domain's verdict depends on another's. It compares `references/behaviors.md` against `skills/`: section per skill, dictionary order, one table per section, five rows, no empty content cell. Route each exit code: 0 — that domain's behavior list matches; 1 — the mismatches are printed on stderr as `{檔案}:{技能名}:{說明}`, so report every one as a compliance failure with the skill it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because `references/behaviors.md` is missing, `skills/` is missing, or no `SKILL.md` was found. Record exit 3 as 「無清單可查」with the cause from stderr and carry it into the step 3 merge; a domain with no behavior list is a compliance failure, and exit 3 is **never** a pass.
|
||||||
4. When the `jsc-hooks` domain is present, run `jsc-hooks/tools/wire-cli.sh smoke {cli}` for every CLI reported by `jsc-cli/tools/detect-clis.sh`; the per-CLI smokes run **in parallel**. When no CLI is detected, run `jsc-hooks/tools/wire-cli.sh smoke codex` as the minimum hook behavior check and label it 「預設 hook smoke」 in the report. Use `smoke`, not `purge` or rewiring actions, and set `JSC_READONLY=1` for the whole audit so a mistyped sub-command is refused in code (exit 6) instead of rewiring the machine; `status` and `smoke` are unaffected by that variable. Route each `smoke` exit code: 0 — the run passed its own assertions; 2 — usage error, so fix the CLI code and rerun; 4 — the smoke failed, which includes the script's own result-line count not matching what it expected. **Read the count from the script's `lines<TAB>{數量}` output line; never write the number into this skill.** The script counts its own result lines and asserts them, so a hardcoded number here goes stale the moment a hook or a decision path is added — an out-of-date count in a SKILL.md is exactly what misled the previous audit.
|
4. For every shell script directly named by a SKILL.md, confirm the skill routes every exit code the script's header declares. `lint-scripts.sh` proves the script exists and declares its codes; this check is the other half — that the caller branches on each of them. Report evidence as `skill file:line -> script path`.
|
||||||
5. When a hook or script smoke fails, route it as a compliance failure with script name, exit code, output summary, and proposed fix. Do not continue to report the affected hook as compliant.
|
5. When the `jsc-hooks` domain is present, run `jsc-hooks/tools/wire-cli.sh smoke {cli}` for every CLI reported by `jsc-cli/tools/detect-clis.sh`; the per-CLI smokes run **in parallel**. When no CLI is detected, run `jsc-hooks/tools/wire-cli.sh smoke codex` as the minimum hook behavior check and label it 「預設 hook smoke」 in the report. Use `smoke`, not `purge` or rewiring actions, and set `JSC_READONLY=1` for the whole audit so a mistyped sub-command is refused in code (exit 6) instead of rewiring the machine; `status` and `smoke` are unaffected by that variable. Route each `smoke` exit code: 0 — the run passed its own assertions; 2 — usage error, so fix the CLI code and rerun; 4 — the smoke failed, which includes the script's own result-line count not matching what it expected. **Read the count from the script's `lines<TAB>{數量}` output line; never write the number into this skill.** The script counts its own result lines and asserts them, so a hardcoded number here goes stale the moment a hook or a decision path is added — an out-of-date count in a SKILL.md is exactly what misled the previous audit.
|
||||||
|
6. When a hook or script smoke fails, route it as a compliance failure with script name, exit code, output summary, and proposed fix. Do not continue to report the affected hook as compliant.
|
||||||
|
|
||||||
**Group 2 — audit every skill of every domain against the guidelines.md audit checklist.** This group MUST run as a sub agent, one sub agent per domain repo, and those sub agents run **in parallel**. Each sub agent reports its findings: skill, failed checklist item, evidence (file:line), proposed fix. Cover the checklist's four flow checks by name, not only the naming and language items:
|
**Group 2 — audit every skill of every domain against the guidelines.md audit checklist.** This group MUST run as a sub agent, one sub agent per domain repo, and those sub agents run **in parallel**. Each sub agent reports its findings: skill, failed checklist item, evidence (file:line), proposed fix. Cover the checklist's four flow checks by name, not only the naming and language items:
|
||||||
- Every step number, file path and section title the skill references — inside itself and in other files — really exists (the pointer points at something).
|
- Every step number, file path and section title the skill references — inside itself and in other files — really exists (the pointer points at something).
|
||||||
@@ -27,7 +28,7 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references
|
|||||||
- Every external call (script, API, other skill) states what to do on failure and routes every exit code.
|
- Every external call (script, API, other skill) states what to do on failure and routes every exit code.
|
||||||
- No gate the skill installs blocks the only path that lifts that gate.
|
- No gate the skill installs blocks the only path that lifts that gate.
|
||||||
|
|
||||||
Four checklist items are **already decided by group 1** and must not be re-run here: `sh -n` on every `tools/` and `hooks/` script, script existence with the executable bit, the hook smoke, and the `references/behaviors.md` match. Tell each sub agent to skip those four and leave them blank; the main agent fills them in from the group 1 verdicts when merging in step 3. Re-scanning the same files in every domain sub agent buys nothing — group 1 already scanned them all, with the same tools, on the same synced tree.
|
Five checklist items are **already decided by group 1** and must not be re-run here: `sh -n` on every `tools/` and `hooks/` script, script existence with the executable bit, the hook smoke, the `references/behaviors.md` match, and the `lint-frontmatter.sh` verdict. Tell each sub agent to skip those five and leave them blank; the main agent fills them in from the group 1 verdicts when merging in step 3. Re-scanning the same files in every domain sub agent buys nothing — group 1 already scanned them all, with the same tools, on the same synced tree.
|
||||||
|
|
||||||
**Group 3 — a flow and cost optimization review**, kept separate from the compliance audit. Each aspect **MUST run as a sub agent**, and the six aspects run in parallel with each other and with groups 1 and 2:
|
**Group 3 — a flow and cost optimization review**, kept separate from the compliance audit. Each aspect **MUST run as a sub agent**, and the six aspects run in parallel with each other and with groups 1 and 2:
|
||||||
|
|
||||||
@@ -42,8 +43,8 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references
|
|||||||
|
|
||||||
Each optimization finding reports skill, aspect, evidence (file:line), current flow step count, proposed flow step count, what time or interaction it saves, what cost it saves, current cost driver, proposed cost driver, whether correctness decreases, and which protection would be weakened if any. Cost savings may be token volume, sub-agent count, API calls, file scans, full-repo audits, or user prompts. Keep optimization findings separate from compliance failures.
|
Each optimization finding reports skill, aspect, evidence (file:line), current flow step count, proposed flow step count, what time or interaction it saves, what cost it saves, current cost driver, proposed cost driver, whether correctness decreases, and which protection would be weakened if any. Cost savings may be token volume, sub-agent count, API calls, file scans, full-repo audits, or user prompts. Keep optimization findings separate from compliance failures.
|
||||||
|
|
||||||
Completion condition for all three groups: every domain has a `lint-scripts.sh` verdict and a `check-behaviors.sh` verdict, every script named by a SKILL.md has an exit-code-routing verdict, and every smoked CLI has a `smoke` exit code plus the `lines` value the script printed for it; every domain has a group 2 audit result that names a verdict for all checklist items — the four flow checks included, and the four group 1 items left blank for the step 3 merge rather than re-scanned; and every one of the six aspects has returned a verdict for every domain, 「無發現」 where an aspect found nothing.
|
Completion condition for all three groups: every domain has a `lint-scripts.sh` verdict, a `lint-frontmatter.sh` verdict and a `check-behaviors.sh` verdict, every script named by a SKILL.md has an exit-code-routing verdict, and every smoked CLI has a `smoke` exit code plus the `lines` value the script printed for it; every domain has a group 2 audit result that names a verdict for all checklist items — the four flow checks included, and the five group 1 items left blank for the step 3 merge rather than re-scanned; and every one of the six aspects has returned a verdict for every domain, 「無發現」 where an aspect found nothing.
|
||||||
3. Merge the three groups, then present compliance failures and optimization findings separately via the `jsc-ask:ask` decision tree. Merging means one thing in code: fill the four skipped checklist items of every group 2 sub agent report from the matching group 1 verdicts, so each domain ends with one complete checklist and no item counted twice.
|
3. Merge the three groups, then present compliance failures and optimization findings separately via the `jsc-ask:ask` decision tree. Merging means one thing in code: fill the five skipped checklist items of every group 2 sub agent report from the matching group 1 verdicts, so each domain ends with one complete checklist and no item counted twice.
|
||||||
- Compliance failure options: apply the proposed fix / skip / custom fix. Every option states its impact scope, for example skipping leaves the skill non-compliant until the next audit.
|
- Compliance failure options: apply the proposed fix / skip / custom fix. Every option states its impact scope, for example skipping leaves the skill non-compliant until the next audit.
|
||||||
- Optimization options: apply / defer / custom. Any suggestion that weakens a protection must name the protection it removes and must not be applied unless the user explicitly accepts that tradeoff. Cost optimization may move, merge, cache, or narrow checks; it must not delete a compliance check only because it is expensive.
|
- Optimization options: apply / defer / custom. Any suggestion that weakens a protection must name the protection it removes and must not be applied unless the user explicitly accepts that tradeoff. Cost optimization may move, merge, cache, or narrow checks; it must not delete a compliance check only because it is expensive.
|
||||||
|
|
||||||
@@ -56,5 +57,5 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references
|
|||||||
- Exit 0 — every copy holds identical bytes; the script verifies that itself.
|
- Exit 0 — every copy holds identical bytes; the script verifies that itself.
|
||||||
|
|
||||||
Completion condition: the script exits 0 and prints the touched paths.
|
Completion condition: the script exits 0 and prints the touched paths.
|
||||||
6. Re-run the group 1 script, behavior-list, and hook validation, re-check the guidelines.md audit checklist for every touched skill, then re-run the optimization aspect that produced each accepted optimization. These three re-runs are as independent as the first pass, so run them **in parallel** and merge them the same way step 3 did. On any compliance failure, **return to step 3**: confirm and fix again, until all accepted compliance fixes pass. On an accepted optimization that does not produce the promised step reduction or cost reduction, or still weakens correctness beyond the recorded decision, return to step 3 for a new decision. Completion condition: `tools/lint-scripts.sh` exits 0 or 3 for every domain, `tools/check-behaviors.sh` exits 0 for every domain, every hook smoke exits 0 with the `lines` count the script itself asserted, all checklist items pass, and every accepted optimization has a matching verification result.
|
6. Re-run the group 1 script, frontmatter, behavior-list, and hook validation, re-check the guidelines.md audit checklist for every touched skill, then re-run the optimization aspect that produced each accepted optimization. These three re-runs are as independent as the first pass, so run them **in parallel** and merge them the same way step 3 did. On any compliance failure, **return to step 3**: confirm and fix again, until all accepted compliance fixes pass. On an accepted optimization that does not produce the promised step reduction or cost reduction, or still weakens correctness beyond the recorded decision, return to step 3 for a new decision. Completion condition: `tools/lint-scripts.sh` exits 0 or 3 for every domain, `tools/lint-frontmatter.sh` exits 0 for every domain — exit 3 is 「什麼都沒掃」 and never counts as a pass — `tools/check-behaviors.sh` exits 0 for every domain, every hook smoke exits 0 with the `lines` count the script itself asserted, all checklist items pass, and every accepted optimization has a matching verification result.
|
||||||
7. Call `jsc-git:pr` once per affected domain repo to open a Push Request. Completion condition: every affected repo has a PR URL, and all URLs are reported in one table with the format in [`../../references/pr-report.md`](../../references/pr-report.md).
|
7. Call `jsc-git:pr` once per affected domain repo to open a Push Request. Completion condition: every affected repo has a PR URL, and all URLs are reported in one table with the format in [`../../references/pr-report.md`](../../references/pr-report.md).
|
||||||
|
|||||||
Executable
+168
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# lint-frontmatter.sh — 檢查一個 domain 存取庫每支 skills/*/SKILL.md 的 frontmatter 能不能安全解析。
|
||||||
|
#
|
||||||
|
# 用法: lint-frontmatter.sh <domain-path>
|
||||||
|
#
|
||||||
|
# 檢查五項(掃 {domain-path}/skills/*/SKILL.md):
|
||||||
|
# 1. 分隔線 — 第一行是「---」,而且找得到成對的收尾「---」。缺一邊,底下整段都不是
|
||||||
|
# frontmatter,鍵一個都讀不到。
|
||||||
|
# 2. 必要鍵 — 「name」與「description」都在,而且值不是空的。載入器靠這兩個鍵認技能。
|
||||||
|
# 3. 冒號 — 未加引號的純量不得含「冒號加空白」,也不得以冒號結尾。那在 YAML 是鍵的
|
||||||
|
# 分隔符號,解析器會把一行拆成兩個鍵,整份 frontmatter 當場語法錯誤。
|
||||||
|
# 4. 起頭字元 — 未加引號的純量不得以 & * ! | > % @ ` 起頭。這八個在 YAML 1.2 分別是錨點、
|
||||||
|
# 別名、標籤、區塊純量、指令與保留字元,起頭寫了就不是原本那串字。
|
||||||
|
# 5. 引號 — 加了引號的值要收得起來:單引號內部的「'」要寫成「''」,收尾引號之後除了
|
||||||
|
# 註解不得有殘餘。修這個缺陷的手法就是補單引號,補歪了照樣是語法錯誤。
|
||||||
|
#
|
||||||
|
# 為什麼要這支: 2026-08-31 抓到 6 支技能的 description 是未加引號的純量、內容含「冒號加空白」。
|
||||||
|
# 那在 YAML 是語法錯誤,Antigravity 讀到就**靜默丟棄整支技能**——磁碟上 34 支,它只認 28 支,
|
||||||
|
# 而且**完全沒有錯誤訊息**:載入器不報、CLI 不報、技能清單只是少了幾列。這種缺陷唯一的發現
|
||||||
|
# 途徑是逐檔比對磁碟數量與載入數量,人工比對 10 個 domain 每次稽核都要重做一遍,還會漏。
|
||||||
|
# 輸入輸出固定的判定就交給程式,別靠眼睛。
|
||||||
|
#
|
||||||
|
# 為什麼不用 YAML 套件: 本機沒有 pyyaml,而護欄不該把自己綁在一個不保證存在的相依上。這五項
|
||||||
|
# 都只需要 YAML 1.2 的 plain scalar 規則,自己判定就夠,也才跑得到每一台機器上。
|
||||||
|
#
|
||||||
|
# 輸出: 一行一個不合格項目,格式 {檔案}:{鍵}:{說明}(stderr);通過時在 stderr 印一行摘要。
|
||||||
|
# 結構性問題(分隔線、必要鍵、無法辨識的一行)的「鍵」欄寫 frontmatter。stdout 不印東西。
|
||||||
|
# 結束碼: 0=掃到 SKILL.md 且五項全過
|
||||||
|
# 1=有不合格項目(清單在 stderr)
|
||||||
|
# 2=用法錯誤(本腳本只吃一個參數)
|
||||||
|
# 3=domain 路徑不存在、找不到 {domain-path}/skills/,或 skills/ 底下一支 SKILL.md
|
||||||
|
# 都沒有——**什麼都沒掃**,不等於通過
|
||||||
|
set -u
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo 'usage: lint-frontmatter.sh <domain-path>' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -eq 1 ] || usage
|
||||||
|
DOMAIN=${1%/}
|
||||||
|
[ -n "$DOMAIN" ] || usage
|
||||||
|
|
||||||
|
SKILLS="$DOMAIN/skills"
|
||||||
|
[ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; }
|
||||||
|
[ -d "$SKILLS" ] || { echo "找不到 skills/:$SKILLS" >&2; exit 3; }
|
||||||
|
|
||||||
|
TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; }
|
||||||
|
trap 'rm -f "$TMP"' EXIT
|
||||||
|
|
||||||
|
find "$SKILLS" -mindepth 2 -maxdepth 2 -type f -name 'SKILL.md' 2>/dev/null \
|
||||||
|
| LC_ALL=C sort > "$TMP"
|
||||||
|
[ -s "$TMP" ] || { echo "skills/ 底下沒有任何 SKILL.md,無 frontmatter 可掃:$SKILLS" >&2; exit 3; }
|
||||||
|
|
||||||
|
BOM=$(printf '\357\273\277')
|
||||||
|
|
||||||
|
hit=0
|
||||||
|
total=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
total=$((total + 1))
|
||||||
|
awk -v f="$f" -v bom="$BOM" '
|
||||||
|
function trim(s) { gsub(/^[ \t\r]+/, "", s); gsub(/[ \t\r]+$/, "", s); return s }
|
||||||
|
function rep(k, m) { printf "%s:%s:%s\n", f, k, m; bad = 1 }
|
||||||
|
|
||||||
|
# 掃過一段引號括起來的值,回傳收尾引號之後的殘餘;收不起來就回哨兵值。
|
||||||
|
# 為什麼要自己逐字掃: 單引號的跳脫是「重複一次」、雙引號的跳脫是反斜線,兩套規則不同,
|
||||||
|
# 用正規表示式一次比對兩種只會在其中一種上判錯。
|
||||||
|
function scan_quoted(v, q, i, c, n) {
|
||||||
|
n = length(v)
|
||||||
|
i = 2
|
||||||
|
while (i <= n) {
|
||||||
|
c = substr(v, i, 1)
|
||||||
|
if (q == "\"" && c == "\\") { i += 2; continue }
|
||||||
|
if (c == q) {
|
||||||
|
if (q == "\047" && substr(v, i + 1, 1) == "\047") { i += 2; continue }
|
||||||
|
return substr(v, i + 1)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return "\001"
|
||||||
|
}
|
||||||
|
|
||||||
|
BEGIN { state = 0; bad = 0 }
|
||||||
|
{ sub(/\r$/, "") }
|
||||||
|
|
||||||
|
NR == 1 {
|
||||||
|
line = $0
|
||||||
|
sub("^" bom, "", line)
|
||||||
|
if (trim(line) != "---") {
|
||||||
|
rep("frontmatter", "第一行不是 ---,整份 frontmatter 讀不到,載入器會靜默丟棄這支技能")
|
||||||
|
state = 3 # 已判定並回報,END 不必再補話
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
state = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
# frontmatter 收尾。YAML 的文件結束標記「...」一樣算收尾。
|
||||||
|
state == 1 && (trim($0) == "---" || trim($0) == "...") { state = 2; next }
|
||||||
|
|
||||||
|
state == 1 {
|
||||||
|
if ($0 ~ /^[ \t]/) next # 縮排的續行或巢狀對應,判不出就不判
|
||||||
|
if ($0 ~ /^[ \t]*$/) next # 空行
|
||||||
|
if ($0 ~ /^#/) next # 註解
|
||||||
|
if ($0 ~ /^- /) next # 與鍵同縮排的序列項
|
||||||
|
|
||||||
|
if ($0 !~ /^[A-Za-z0-9_.-]+:([ \t]|$)/) {
|
||||||
|
rep("frontmatter", "這一行既不是鍵也不是續行,YAML 解析會在這裡中斷:" substr($0, 1, 40))
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
ci = index($0, ":")
|
||||||
|
k = substr($0, 1, ci - 1)
|
||||||
|
v = trim(substr($0, ci + 1))
|
||||||
|
|
||||||
|
if (k in seen) rep(k, "同一個鍵出現兩次,後面那份會靜默蓋掉前面那份")
|
||||||
|
seen[k] = 1
|
||||||
|
val[k] = v
|
||||||
|
|
||||||
|
if (v == "") next # 值在下一段,前面的縮排規則已經放過
|
||||||
|
|
||||||
|
q = substr(v, 1, 1)
|
||||||
|
if (q == "\047" || q == "\"") {
|
||||||
|
rest = scan_quoted(v, q)
|
||||||
|
if (rest == "\001") {
|
||||||
|
if (q == "\047") rep(k, "單引號沒有收尾,內部的 \047 要寫成 \047\047")
|
||||||
|
else rep(k, "雙引號沒有收尾")
|
||||||
|
} else {
|
||||||
|
rest = trim(rest)
|
||||||
|
if (rest != "" && substr(rest, 1, 1) != "#")
|
||||||
|
rep(k, "收尾引號之後還有內容,解析器會當成語法錯誤:" substr(rest, 1, 30))
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
# 以下都是未加引號的純量(plain scalar)。
|
||||||
|
if (index("&*!|>%@`", q) > 0)
|
||||||
|
rep(k, "未加引號的純量以 YAML 特殊字元「" q "」起頭,會被當成錨點、別名、標籤、區塊純量或指令")
|
||||||
|
|
||||||
|
if (index(v, ": ") > 0)
|
||||||
|
rep(k, "未加引號的純量含「冒號加空白」,YAML 會把它當成鍵的分隔符號,整份 frontmatter 語法錯誤;整串加單引號即可")
|
||||||
|
else if (substr(v, length(v), 1) == ":")
|
||||||
|
rep(k, "未加引號的純量以冒號結尾,YAML 會把它當成鍵的分隔符號;整串加單引號即可")
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
if (state == 0) {
|
||||||
|
rep("frontmatter", "檔案是空的,沒有 frontmatter")
|
||||||
|
} else if (state == 1) {
|
||||||
|
rep("frontmatter", "frontmatter 分隔線不成對,找不到收尾的 ---")
|
||||||
|
} else if (state == 2) {
|
||||||
|
if (!("name" in seen)) rep("frontmatter", "缺必要鍵 name")
|
||||||
|
else if (val["name"] == "") rep("name", "必要鍵的值是空的")
|
||||||
|
if (!("description" in seen)) rep("frontmatter", "缺必要鍵 description")
|
||||||
|
else if (val["description"] == "") rep("description", "必要鍵的值是空的")
|
||||||
|
}
|
||||||
|
exit bad
|
||||||
|
}
|
||||||
|
' "$f" >&2 || hit=1
|
||||||
|
done < "$TMP"
|
||||||
|
|
||||||
|
if [ "$hit" -eq 0 ]; then
|
||||||
|
echo "frontmatter 檢查通過:$total 支(分隔線、必要鍵、冒號、起頭字元、引號)" >&2
|
||||||
|
else
|
||||||
|
echo "frontmatter 檢查有不合格項目:共掃 $total 支,清單在上面" >&2
|
||||||
|
fi
|
||||||
|
exit $hit
|
||||||
Reference in New Issue
Block a user