Files
meta/references/guidelines.md
T
jiantw83 7b6b9076ea docs(guidelines): 補上助理運行閘門與 fail-closed 閘門的專屬規則
What:
- 新增「助理運行閘門」一節:規格表、心跳判定的六碼處置、豁免清單十一支。
- 節內另立「fail-closed 閘門的專屬規則」四條,那是準則現在完全沒有的東西。
- 環境變數表補上閘門開關與心跳門檻兩列。
- 三份 manifest 的版本一起提升。

Why:
- 整組 hook 的通則是資料不足就放行,這一道相反。例外不點名,後來的人會以為可以隨便再開一道 fail-closed 的閘門,而那種閘門開錯就是整組技能鎖死。
- 豁免清單與腳本檔頭是同一件事實。準則沒有那張表,兩邊就會各走各的,改一支忘了另一支。

How:
- 四條專屬規則裡有兩條是這一輪實作時才想清楚的。逃生門的判斷要擺在載入共用函式庫之前——函式庫讀不到時 sh 會就地結束並回擋人的那個碼,逃生門也跟著跑不到,人就繞不過去;這一條只對 fail-closed 成立。豁免清單只收解鎖路徑,方向與重啟閘門相反,那一道解鎖靠閘門外的動作,這一道解鎖靠跑一支技能。
- 「清單認技能名不認呼叫鏈」那一條補了一個更狠的實例:巡檢要先把結果寫上監控頁才寫心跳,只豁免助理自己會做出自咬環。所以新增豁免技能時不只要想它會呼叫誰,還要想那條呼叫鏈上有沒有一步是解鎖條件本身的前置。
- 心跳判定回「檔案系統問不出來」時放行不擋,理由與代價都寫進去了。那一碼與「時間戳壞掉」的差別在有沒有出路。

Who:
助理閘門實作完之後,把當中的判斷收進準則,讓下一道同類閘門有依據。
2026-09-01 15:27:06 +08:00

421 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# JSC 技能準則
本文件是 jsc 技能組的唯一準則來源。`skill-new`、`skill-update`、`skill-delete` 與技能審核都必須依此檢查。
## 命名
1. Plugin 名稱:`jsc-{domain}`,domain 用一個簡短的英文代表詞(例:`ask`、`sdlc`、`gitea`)。
2. Marketplace 統一為 `jsc`,正本在 `https://gitea.jsc.idv.tw/plugins/meta.git`;安裝 token 一律 `jsc-{domain}@jsc`。**每個 domain 存取庫都帶同一份 marketplace 檔**(`.claude-plugin/marketplace.json` 與 `.agents/plugins/marketplace.json`,內容與正本完全一致),所以任何一個 repo 都能當註冊入口,互相覆蓋沒關係。
3. Skill 名稱:小寫、數字、連字號(`-`),最長 64 字元。名稱即指令(`/jsc-{domain}:{name}`)。
4. 每個 domain 是一個獨立存取庫 `https://gitea.jsc.idv.tw/plugins/{domain}.git`。新 domain 必須依 `https://gitea.jsc.idv.tw/plugins/template` 的結構建立新存取庫(三份 plugin manifest、`skills/`、`README.md`、`AGENTS.md`),並把 plugin 條目(URL 來源指向新存取庫)加入 `plugins/meta` 正本的兩份 marketplace 檔,再把更新後的檔案**同步到所有 domain 存取庫**(含新存取庫自己)。
5. Git 分支名**只允許 ASCII**(`a-z0-9` 與 `/`、`-`);中文需求或標題先翻譯成英文短語再 slug 化。
6. Manifest 版本號從 `0.0.1` 開始,三份 manifest 同步 bump;`minor` 與 `patch` 不得超過 `9`,滿 `9` 就往左進位,`major` 可以超過 `9`。
## PR 分支階梯
所有存取庫的 PR 一律照階梯逐級上推,**禁止越級**。
| 類型 | 階梯 |
| --- | --- |
| `feat`、`docs`、`style`、`refactor`、`perf`、`test`、`chore`、`revert` | `{類型}/{子功能}` → `{類型}/{功能}/main` → `develop` → `master` |
| `fix` | `fix/{修改}` → `develop` → `master` |
1. `{子功能}` = `{功能}/{子功能內容中文簡述}`,可以多層,例如 `{A}/{A的子功能B}/{B的子功能C}/{C的子功能}`。
2. `{功能}` = 功能內容中文簡述;`{修改}` = 修改內容中文簡述。
3. 中文簡述先交給 `jsc-git/tools/slugify.sh` 轉成 ASCII slug,再組成分支名;分支名本身的字元限制見「命名」第 5 條。
4. base 一律由 `jsc-git/tools/base-branch.sh --derive` 推導。推不出唯一合法基底就中止並詢問使用者,不猜,也不退回 `develop`。
5. 功能主幹 `{類型}/{功能}/main` 不在 origin 上時,自動從 `develop` 建立並推上去,收尾要回報建立了哪一條分支。
6. 階梯最後一級不能省:`develop` 併進 `master` 才會生效,marketplace 與 `version-guard.sh` 都讀存取庫的**預設分支**。
PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-report.md`](pr-report.md)。所有會產生 PR 的技能都引用那份文件,不在技能內各自抄欄位。
## Description 規則
1. frontmatter 的 `description` 為一行英文,不超過 5 句或 5 個步驟——**兩個上限滿足任一個就算通過**,句數與步驟數都超過才要精簡。
2. 使用專有名詞、概念或指引詞(例:WBS、TDD、decision tree、STE100)取代解釋。
3. 必須寫清楚觸發時機(何時用、何時不用),這是各 CLI 自動載入的唯一依據。
4. 複雜流程透過**組合其他技能**實現,不在單一 description 裡塞流程。
## Manifest 相依版本
1. Plugin 需要另一個 jsc plugin 的技能、工具、hooks、references 或 templates 才能完成自己的流程時,必須在三份 manifest 寫入 `jsc.requires`。
2. `jsc.requires` 是物件。鍵是完整 plugin 名稱,格式為 `jsc-{domain}`。值是最低版本,格式為 `>=x.y.z`。
3. 沒有跨 plugin 相依時不寫 `jsc.requires`。不要留下空物件。
4. 只宣告 jsc plugin 對 jsc plugin 的相依。系統指令、語言執行環境與第三方套件寫在 README 或工具說明,不寫進這個欄位。
5. `jsc-cli/tools/check-requires.sh` 是**部署端**相依版本回報的唯一程式來源。`jsc-cli/tools/deploy.sh update` 必須在更新每個 domain 前呼叫它,**版本不符時照樣更新那個 domain**,只在回報裡寫明缺哪一個 plugin 的哪一版,不得靜默略過這段回報。
**為什麼不跳過。** 跳過更新會讓落後的 domain 永遠更新不到:它落後所以被跳過,被跳過所以永遠落後,更新指令跑幾次都一樣,只能手動拆。相依版本不符要擋的是「拿舊版去跑」,不是「把舊版換成新版」,更新本身正是解除落後的唯一路徑,擋它等於自鎖。
阻擋改由 `jsc-hooks/hooks/version-guard.sh` 在技能被呼叫的當下執行,規則見「版本前置檢查」。那個時點才擋得住真正會出事的動作,也不會擋掉更新路徑。
## 強制力層級
1. 規則的實現優先順序:**hook > prompt**。凡是可以由 hook 強制的規則(語言、計時、統計),一律下放 hook,SKILL.md 只保留 hook 無法涵蓋的指引。
2. 有標準輸入與輸出的流程一律下放到 `tools/` 腳本,SKILL.md 只描述何時呼叫與參數。
3. 主 agent 不需要處理細節的流程,SKILL.md 必須明確要求建立 sub agent 處理(關鍵字:「必須以 sub agent 執行」)。
## Hook 規則
1. 所有 hook 專屬存放於 `jsc-hooks`,**不可散落在其他 domain**。
2. Hook 腳本實作優先順序:**shell > nodejs > python**。
3. Hook 必須適用於 claude / codex / copilot / antigravity / kiro 五種 CLI:
- 五支 CLI 的 hook 負載形態各不相同,沒有哪一支是基準格式。腳本吃 stdin JSON,也吃環境變數,缺欄位時安靜降級(exit 0)。
- 各 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 的結束碼語意兩邊文件都沒寫,擋不擋得住純靠運氣。
## 技能設計
1. 每個技能必須有單一明確目標,不可與既有技能重複;能複用就複用(呼叫其他技能或工具)。
2. 需要操作 gitea 且輸入輸出明確的技能,一律透過 `jsc-gitea/tools/gitea.sh`(curl + `GITEA_TOKEN`)或 `tea` CLI,不可自行拼 API 呼叫。gitea.sh 在 token 未設定或收到 401/403 時,會自動改用 tea 的登入金鑰重試一次。
3. 需要問使用者的技能,一律透過 `jsc-ask:ask` 的決策樹規則:選項式提問、每個選項標明影響範圍、問到沒有疑慮為止、已有紀錄的答案不再問。
## 語言
1. **所有非程式碼輸出**一律 STE100 繁體中文、UTF-8、無亂碼、無簡體字,帶擬人台灣感:短句、一句一指令、主動語態、術語一致、台灣用語、全形標點、去 AI 味、直接講重點。範圍涵蓋程式碼註解、commit 訊息、PR 描述、wiki 頁、對使用者的回報、README 與各種文件;程式碼本身(識別字、關鍵字、API 名稱)不受此規則限制。完整適用範圍表、編碼要求與替換表的唯一來源:[`references/ste100.md`](ste100.md)。
2. **技能文件(SKILL.md)整份為英文**:frontmatter 與內文都是。風格比照 STE100:短句、祈使句、術語一致。
3. 技能文件裡「要原樣輸出的繁中字面內容」保留繁中:wiki 狀態字串(例:已分析、未完成)、hook 注入文字、要寫進 wiki 或 commit 的文案。技能執行時產生的 commit 訊息、PR 描述、wiki 頁內容仍依第 1 條輸出繁中。
4. README、AGENTS.md、`templates/`、`references/` 為 STE100 繁體中文。中文並列項用頓號「、」,不用半形「/」;英文項目的並列(CLI 名、頁名前綴)可用「/」。
## 撰寫規範(SKILL.md 內文)
1. 每個步驟以**可檢核的完成條件**結尾(例:「成功標上工作證才可以進入下一步」),不用模糊語(「理解後」「適當地」)。
2. 用**正向敘述**寫目標行為;禁止句只留給無法正向表達的硬性護欄。
3. 每個意義只有**單一真實來源**:環境可查的資訊(指令、設定、目錄結構)不要抄進技能,只寫環境查不到的慣例、原因與陷阱。
4. **漸進揭露**:所有分支都需要的內容留在 SKILL.md;只有部分分支需要的參考資料下放 `references/`,以一行指引指過去。
5. 善用**引導詞**(WBS、CPM、TDD、seam、STE100 等既有概念)取代整段解釋。
## 技能行為清單
每個 domain 都要有一份技能行為清單,記下每支技能實際做的事,供稽核與驗證比對。
| 項目 | 規則 |
| --- | --- |
| 位置 | 每個 domain 存取庫的 `references/behaviors.md`,UTF-8 無 BOM,內容為 STE100 繁體中文 |
| 第一行 | `# jsc-{domain} 技能行為清單` |
| 節 | 每支技能一個 `## {技能名}` 節,名稱與 `skills/` 底下的目錄名逐字相同,節數與技能支數一樣,排列照目錄名的字典序 |
| 表格 | 每節恰好一張表,表頭兩欄依序是「項目」與「內容」,五列依序為 觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,每一列的「內容」欄都不得空白 |
| 寫什麼 | 寫技能實際的行為:什麼情況會用、什麼情況不該用、依序做了哪些事、呼叫哪些腳本與技能、做到什麼程度算跑完、跑完在環境裡留下哪些查得到的跡象。不要抄 `description` 的行銷語 |
| 純唯讀的技能 | 「可驗證跡象」欄寫「無寫入跡象,只有回報內容」,不得留白 |
| 更新時機 | 技能異動時在**同一個 PR 內**一起更新:新增技能就加一節、刪除就移除該節、改行為就改該節 |
| 檢查腳本 | `jsc-meta/tools/check-behaviors.sh {domain-path}` |
`check-behaviors.sh` 的結束碼分流:
| 結束碼 | 意義 |
| --- | --- |
| 0 | 行為清單與 `skills/` 相符,五個欄位齊全且內容欄非空 |
| 1 | 不符:缺節、多節、順序不對、表格不對、缺欄位或欄位空白,逐項印在 stderr,照著修再重跑 |
| 2 | 用法錯誤:本腳本只吃一個參數 |
| 3 | 找不到 `references/behaviors.md`、找不到 `skills/`,或 `skills/` 底下一支 `SKILL.md` 都沒有。**什麼都沒查,不等於通過**,先補齊檔案再重跑 |
**為什麼一個 domain 一份,不集中在 `jsc-meta`。** 技能改動與行為清單放同一個存取庫,才進得了同一個 PR;審的人在一頁 diff 上就看得出行為改了、清單也改了。集中在 meta 的話,改一支技能要開兩條 PR,一條在 domain、一條在 meta,兩條互相等待,先併的那條讓清單與技能對不上,稽核抓到的是自己造出來的漂移。跨存取庫的東西沒有原子性,同一份事實就不要拆兩邊放。
## 環境變數
| 變數 | 用途 | 未設定時 |
| --- | --- | --- |
| `GITEA_HOST` | Gitea 站台(例:`https://gitea.jsc.idv.tw`) | 詢問使用者 |
| `GITEA_TOKEN` | Gitea API token | 改用 tea 登入金鑰(`tea login list`);tea 也沒有才詢問使用者 |
| `JSC_WIKI_REPO_QUESTION` | `QUESTION_CONTENTS`、`QUESTION_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_PLAN` | `PLAN_CONTENTS`、`PLAN_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_ANALYZE` | `ANALYZE_CONTENTS`、`ANALYZE_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_DELIVER` | `DELIVER_CONTENTS`、`DELIVER_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_MAINTAIN` | `MAINTAIN_CONTENTS` 所在的 `{owner}/{repo}`(本類型只有目錄頁) | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_REPO` | `REPO_CONTENTS`、`REPO_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_LOG` | `LOG_CONTENTS`、`LOG_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_LEARN` | `LEARN_CONTENTS`、`LEARN_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_ERROR` | `ERROR_CONTENTS`、`ERROR_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_CHECK` | `CHECK_CONTENTS`、`CHECK_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_REPORT` | `REPORT_CONTENTS`、`REPORT_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_SKILLSET` | `SKILLSET_CONTENTS`、`SKILLSET_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO_TOOLING` | `TOOLING_CONTENTS`、`TOOLING_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO` |
| `JSC_WIKI_REPO` | 未逐類設定時的共用 wiki `{owner}/{repo}` | 詢問使用者 |
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
| `JSC_PR_WATCH_INTERVAL` | `jsc-gitea/tools/pr-watch.sh` 輪詢 PR 狀態的間隔秒數 | 預設 60 |
| `JSC_RESTART_GATE` | 部署後重啟閘門的開關,`off` 關閉整道閘門 | 閘門開啟 |
| `JSC_ASSISTANT_GATE` | 助理運行閘門的開關,`off` 關閉整道閘門 | 閘門開啟 |
| `JSC_ASSISTANT_HEARTBEAT_TTL` | 助理心跳的過期門檻秒數。排程週期由這個值推導 | 預設 300;壞值退回預設 |
頁面類型只讀自己的 `JSC_WIKI_REPO_{TYPE}`。只有該變數未設定時,才退回 `JSC_WIKI_REPO`。不得跨類型代用。
## 版本前置檢查
技能組的每一支技能在被呼叫前都要過兩道版本檢查:本機載入版本沒有落後遠端發佈版本,以及這支技能所屬 plugin 宣告的 `jsc.requires` 每一項都吃得到。兩道判定都在程式層,由 `jsc-hooks` 的 `version-guard.sh` 執行,**不靠技能內文自我約束**——寫在內文的規則,模型可以無視。
兩道都只擋「確定落後」一種情況。查不到任何一項基礎資訊就安靜放行(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 的限制,不是我們接錯。
| 項目 | 規則 |
| --- | --- |
| 比對對象 | 遠端發佈版本(存取庫**預設分支**的 `plugin.json`,經 `jsc-gitea/tools/gitea.sh` 讀取,不寫死分支名)對本機**實際載入**版本 |
| 實際載入版本 | 只認 `installed_plugins.json` 的 `installPath` 底下那份 `plugin.json`。註冊在 `installed_plugins.json` 的 `version` 欄位**不當備援**——註冊值可能比實際載入的版本新,拿它來比對會放過真正被載入的舊版 |
| 落後 | 擋下該次技能呼叫(exit 2),並印出更新指令 |
| 相等或超前 | 放行。開發技能組時本機本來就會超前預設分支,擋下去維護者自己動不了 |
| 查不到本機載入版本 | **放行**(exit 0,安靜降級)。讀不到 `installed_plugins.json`、裡面沒有該 plugin 的條目、取不到 `installPath`、`installPath` 底下那份 `plugin.json` 讀不到,四種都算這一列,不退回註冊欄位 |
| 解不出 Gitea 站台 | **放行**(exit 0,安靜降級) |
| 查不到遠端版本 | **放行**(exit 0,安靜降級)。缺基礎設施不等於落後,擋下去會把四支非 Claude CLI 整批鎖死 |
| 相依版本落後 | 這支技能所屬 plugin 的 `jsc.requires` 有一項落後就擋下該次呼叫(exit 2),印出缺哪一個 plugin 的哪一版與更新指令。版本比對由 `version-guard.sh` 自己實作,不呼叫 `jsc-cli/tools/check-requires.sh`:hook 專屬存放於 `jsc-hooks`(見「Hook 規則」第 1 條),而且 `jsc-cli` 已宣告相依 `jsc-hooks`,反過來呼叫會做出循環相依。兩支的比法要保持一致,改動任一支就回頭核對另一支 |
| 相依版本相等或超前 | 放行。每一項都吃得到才算過 |
| 查不到相依資訊 | **放行**(exit 0,安靜降級)。解不出這支技能所屬 plugin 的安裝路徑、讀不到它的 manifest、manifest 沒有 `jsc.requires`、查不到某一項相依 plugin 的本機載入版本,四種都算這一列 |
| 逃生門 | `JSC_VERSION_GUARD=off`(離線工作用),快取秒數 `JSC_VERSION_TTL`(預設 600) |
**豁免清單**(永遠放行,改動前想清楚後果):
| 技能 | 為什麼不能擋 |
| --- | --- |
| `jsc-cli:deploy` | 更新整組技能的入口。擋了就沒有任何方法更新,形成死鎖 |
| `jsc-hooks:hooks-install` | 更新後要重新接線,擋了會讓更新做一半卡住 |
| `jsc-hooks:repair` | hook 壞掉時的唯一修復路徑。擋了就修不好 hook |
| `jsc-cli:models` | SDLC 階段閘門依賴它產生 `model-tags.tsv` |
| `jsc-meta:*` | 開發技能組本身的工具,擋了就修不了技能組 |
| `jsc-ask:ask` | 上面幾支都要問使用者 |
| `jsc-gitea:wiki` | 上面幾支的收尾要寫 wiki |
共 7 項。**兩道檢查共用這一份清單,不另立一份。** 豁免的理由兩道完全一樣:這幾支是解除落後的唯一路徑,擋了就沒有東西能把版本補上來。分成兩份只會兩邊漂移,改了一份、忘了另一份,`deploy` 照樣被相依版本擋死。這張表的唯一真實來源是 `jsc-hooks/hooks/version-guard.sh` 的檔頭與豁免清單,兩邊要逐項對齊。
相依版本檢查移到這裡,是因為 `deploy.sh update` 原本會跳過不符的 domain,跳過就永遠更新不到,理由見「Manifest 相依版本」第 5 條。更新照跑、呼叫才擋,落後的 domain 才有路徑補上來。
`hooks-install` 要據實回報每一支的 verdict:claude、codex、copilot、antigravity 是 `wired`,kiro 是 `degraded`。不得暗示每個 CLI 都擋得住,也不得反過來暗示只有 claude 有保護。
## 部署後重啟閘門
部署換掉的是磁碟上的技能檔,目前工作階段載入的還是舊版。這段落差期間跑技能,改動看起來沒生效,人會以為部署失敗又重跑一次。
| 項目 | 規則 |
| --- | --- |
| 狀態檔 | `$JSC_HOME/restart-required.d/{cli}`,**一支 CLI 一份**,由 `jsc-cli:deploy` 收尾寫入 |
| 清除時機 | 重啟 CLI 之後由 `jsc-hooks` 清除**自己那一份**,不必手動刪 |
| 該 CLI 那份存在時 | 擋下這支 CLI 的 jsc 技能呼叫,印出要重啟哪一支與狀態檔路徑。kiro 擋不了,改注入警告 |
| 該 CLI 那份不存在時 | 放行。別支 CLI 的狀態檔不影響這一支 |
| 判定位置 | 程式層,由 `jsc-hooks/hooks/restart-gate.sh` 執行,不靠技能內文自我約束 |
| 接線位置 | 與版本前置檢查完全相同,逐支見「版本前置檢查」的接線位置表 |
| 逃生門 | `JSC_RESTART_GATE=off` |
**這道閘門在五支 CLI 上的能力,跟版本前置檢查一模一樣。** claude、codex、copilot、antigravity 都有能阻擋的 pre-tool 事件,接上去就真的擋得住,verdict 是 `wired`;kiro 的技能叫用不走工具管線,攔不到,只能在 `userPromptSubmit` 注入警告,verdict 是 `degraded`。這一節以前跟著「只有 claude 有 pre-tool hook」那個錯誤結論走,所以重啟閘門也在四支 CLI 上長期失效:部署完照樣跑舊版技能,沒有任何東西擋,也沒有任何東西報錯。**兩道閘門共用同一套接線,就共用同一份事實表**,不要在這一節另寫一份能力描述——寫兩份就會只改一份,另一份繼續錯著。
**豁免清單**(狀態檔存在也放行):
| 技能 | 為什麼不能擋 |
| --- | --- |
| `jsc-cli:deploy` | 部署本身的入口。擋了就沒有方法重跑部署,形成死鎖 |
| `jsc-hooks:hooks-install` | 部署後要重新接線,擋了會讓部署做一半卡住 |
| `jsc-hooks:repair` | hook 壞掉時的唯一修復路徑。擋了就修不好 hook |
| `jsc-gitea:wiki` | 寫 `SKILLSET_{HASH}` 異動報告與工作日誌的唯一路徑 |
| `jsc-log:worklog` | 部署後還要結清工作日誌 |
| `jsc-log:learn` | 部署後還要記這次的教訓 |
| `jsc-meta:*` | 開發技能組本身的工具,擋了就修不了技能組 |
| `jsc-ask:ask` | 上面幾支都要問使用者。擋了 `deploy` 連 install 或 update 都問不出來 |
| `jsc-git:pr` | 報告與異動的收尾要開 PR,擋了收尾做不完 |
| `jsc-git:commit` | 同上,`pr` 的第一步就是它 |
豁免這幾支的理由是同一件事:`jsc-meta` 四支異動技能的收尾要求把驗證結果寫進 `SKILLSET_{HASH}`,還要結清工作日誌,而這條路徑必經 `jsc-gitea:wiki` 與 `jsc-log`。全擋的話,部署一跑完就沒有路徑寫完報告,重啟閘門與報告要求互相打死。閘門不自鎖的通則見「審核檢查清單」的流程檢查第 4 項。共 10 項;這張表的唯一真實來源是 `jsc-hooks/hooks/restart-gate.sh` 的檔頭與豁免清單,兩邊要逐項對齊。
**豁免清單不是自鎖的通解。** 部署剛跑完就要在同一個工作階段叫用剛做好的技能,那支技能本來就不該靠豁免過關——豁免只擋得住這一道閘門,擋不住「行程還載著舊版」這個事實,驗證結果會是舊版的行為。正解是換一個工作階段:由 `jsc-cli/tools/detect-clis.sh` 開出的新 CLI 行程去叫用,新行程自己會清掉那份狀態檔,載到的也是新版。做法見 [`deploy-verify.md`](deploy-verify.md)。
**狀態檔為什麼一支 CLI 一份。** 這道閘門管的是「這支 CLI 的行程還在跑舊版」,那是每支 CLI 各自的事實。初版用全機器單一檔案,2026-08-27 部署時實測出兩個後果:並行部署互相覆蓋,`cli=` 與 `domains=` 只留最後一支;更嚴重的是清除也是全域的,**任一支 CLI 重啟就解除全部五支的閘門**,其餘四支沒重啟卻不再被擋,這道閘門在多 CLI 環境等於半失效。改成 per-CLI 之後,`require` 寫自己那份、判定只看自己那份、`clear` 只刪自己那份,`report` 才列得出「哪幾支還沒重啟」。**跨 CLI 或跨工作階段的狀態檔,設計時先問清楚那個事實屬於誰**——同一類錯誤在工作包歸屬狀態檔上也踩過,解法見 `jsc-sdlc/tools/wp-gate.sh` 的 `claim` 與 `owns`:`claim` 在領包當下把歸屬登錄成「這個存取庫目前是哪一包」,`owns` 查驗每支 PR 是不是自己那一包的,兩層各管一件事實,狀態檔也刻意不綁工作階段。
**清單認的是技能名,不是呼叫鏈。** 豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。後三支(`jsc-ask:ask`、`jsc-git:pr`、`jsc-git:commit`)自己不是收尾規則的主體,是為了讓前七支走得完才補進來的。`version-guard.sh` 的豁免清單當年也是為同一個原因收進 `jsc-ask:ask`。新增豁免技能時要一併想它會呼叫誰。
## 助理運行閘門
技能與 hook 每跑一次就留下事件,助理負責把事件收攏、判斷健康狀態、寫進監控頁。助理沒在跑的時候,技能會以為背景有人收尾,實際上沒有。這道閘門把那個落差擋在門外。
| 項目 | 規則 |
| --- | --- |
| 狀態檔 | `$JSC_HOME/assistant/heartbeat`,欄位 `ts`、`pid`、`cli`、`session` |
| 誰寫心跳 | `jsc-assist:assistant` 的巡檢**跑完那一輪**才寫。**不是**由系統排程直接寫 |
| 判定位置 | 程式層 `jsc-hooks/hooks/assistant-gate.sh`,不靠技能內文自我約束 |
| 接線位置 | `PreToolUse`,matcher=Skill。能力事實比照「版本前置檢查」那張表,不另寫一份 |
| 放行條件 | 心跳新鮮;或技能名不是 `jsc-{domain}:{name}`;或取不到技能名 |
| 擋下條件 | 心跳不存在、已過期,或 `ts` 讀不出來 |
| 新鮮的判準 | 檔案存在,且 `ts` 距現在小於門檻。門檻預設 300 秒,`JSC_ASSISTANT_HEARTBEAT_TTL` 可覆寫。**不看 pid 存活**——五支 CLI 與容器裡的行程互相看不到彼此的 pid |
| 逃生門 | `JSC_ASSISTANT_GATE=off` |
**心跳為什麼由巡檢寫,不由排程寫。** 排程直接寫的話,心跳新鮮只證明排程活著。巡檢整個壞掉、每輪都失敗,心跳照樣新鮮,閘門照樣放行,而且沒有任何錯誤訊息。改成巡檢收尾才寫,心跳新鮮才等於上一輪真的跑完了,閘門判的才是工作訊號。
**排程週期由門檻推導,不各寫死一個數字。** 門檻是讀取端的設定,心跳檔裡不存它,所以兩邊各寫一個數字一定會撞:門檻五分鐘、巡檢十五分鐘,心跳永遠是過期的。週期取「漏掉一輪還算新鮮、漏掉兩輪才過期」的最大值。要拉長巡檢週期就調大門檻。
**心跳只看這一輪有沒有把結果記下來,不看巡檢項目的成敗。** 項目有失敗但監控頁寫成了就寫心跳,頁上判定標警示;頁寫不成就中止,一定不寫。頁每輪都寫失敗卻照樣寫心跳,等於把上面那個無聲失效原封不動搬過去。
### `heartbeat.sh check` 的六碼處置
| 碼 | 意義 | 閘門的處置 |
| --- | --- | --- |
| 0 | 新鮮 | 放行 |
| 1 | 過期 | **擋**。跑過、現在停了 |
| 2 | 腳本沒跑起來 | 放行。判定機制自己壞了,不是「助理沒在跑」的證據 |
| 3 | 不存在 | **擋**。從沒啟動過 |
| 4 | `ts` 讀不出來 | **擋**。確定沒有可信心跳,絕不可以退回當成新鮮 |
| 5 | 檔案系統失敗 | 放行。理由見下 |
| 6 | 用法錯誤 | 放行。閘門固定送 `check`,收到 6 是呼叫端的缺陷 |
**5 為什麼放行。** `check` 這條路徑本來就不產生 5,5 只由 `write` 與 `clear` 產出,所以從 `check` 收到 5 意思是判定機制壞了,與 2、6 同一類。更實際的理由是:5 正是磁碟滿或權限壞的訊號,而那一刻助理自己也寫不出心跳;擋下去等於整組技能鎖死,出路只剩豁免那幾支,可是它們同樣要寫 `$JSC_HOME`,環境壞著也修不動。磁碟壞掉要人去清磁碟,不是把技能組鎖起來。代價是那種時候閘門會安靜放行,由 `jsc-cli:doctor` 抓。
**4 與 5 的差別在有沒有出路。** 4 是「檔案在、內容壞」,那是確定沒有可信心跳的證據,而且修法就在豁免清單裡(先 `stop` 再 `start`),擋得起。5 是「檔案系統問不出來」,擋了沒有出路。
**擋人訊息要分三種話講。** 心跳不存在是從沒啟動過、過期是跑過停了、`ts` 壞掉是檔案要重建。三種情況使用者要做的事不一樣,訊息混成一種就等於沒講。四件事一件都不能少:上次心跳什麼時候、怎麼啟動助理、哪幾支技能仍可用、逃生門怎麼開。
### fail-closed 閘門的專屬規則
整組 hook 的通則是**資料不足就放行**。助理運行閘門是唯一的例外:沒心跳就是沒運行,照要求要擋。例外要在準則裡點名,不能讓後來的人以為可以隨便再開一道。
新增任何 fail-closed 閘門一律照這四條:
1. **逃生門與豁免清單是上線前提,不是選配。** 任一樣被拿掉或改窄,那道閘門就不可以接線。代價講白:狀態檔寫不進去時全組停擺。
2. **逃生門的判斷要擺在載入 `lib.sh` 之前。** `lib.sh` 讀不到時 sh 會就地結束並回擋人的那個碼,逃生門也跟著跑不到,人就繞不過去。fail-open 閘門沒有這個問題,這一條只對 fail-closed 成立。
3. **豁免清單只收解鎖路徑**,不收收尾規則。這一點與「部署後重啟閘門」的方向相反:那一道解鎖靠閘門外的動作(重新啟動),所以收尾規則要能寫得完;這一道解鎖靠跑一支技能,清單收寬了閘門就等於沒有。
4. **接線的前提是解鎖條件已經成立。** 助理還沒跑起來、心跳還沒穩定就接線,等於當場把整組技能擋死,只剩豁免那幾支。
助理運行閘門的豁免清單如下。這張表的唯一真實來源是 `jsc-hooks/hooks/assistant-gate.sh` 的檔頭與豁免清單,兩邊要逐項對齊:
| 技能 | 為什麼豁免 |
| --- | --- |
| `jsc-assist:*` | 啟動助理本身就是一次技能呼叫。少了這一條,助理永遠啟動不了,整組技能鎖死 |
| `jsc-hooks:repair` | 修 hook 的唯一路徑 |
| `jsc-hooks:hooks-install` | 重新接線的唯一路徑 |
| `jsc-cli:doctor` | 環境壞掉時的診斷入口,這道閘門放行的那幾種情況都靠它抓 |
| `jsc-cli:setup` | 修設定 |
| `jsc-cli:deploy` | 部署 |
| `jsc-cli:models` | `setup` 對「模型標籤檔不見」那一項的修法就是呼叫它 |
| `jsc-gitea:wiki` | 巡檢要先把結果寫上監控頁才寫心跳。理由見下 |
| `jsc-ask:ask` | 上面幾支都要問使用者 |
| `jsc-git:commit` | `repair` 的收尾要開 PR,`pr` 的第一步就是它 |
| `jsc-git:pr` | 同上 |
**自咬環:`jsc-gitea:wiki` 為什麼一定要收。** 巡檢跑完要先把結果寫進監控頁,寫不成就中止、不寫心跳。只豁免 `jsc-assist:*` 的話會變成「沒心跳 → 擋 wiki → 巡檢跑不完 → 還是沒心跳」,自己咬住自己,永遠解不開。
這比「清單認技能名,不是呼叫鏈」那一條更進一步:新增豁免技能時不只要想它會呼叫誰,還要想**那條呼叫鏈上有沒有一步是解鎖條件本身的前置**。是的話,那一步非收不可。
**刻意不收的那幾支**:`jsc-log:worklog`、`jsc-log:learn`、`jsc-meta:*`。它們是部署收尾規則的主體,與「把助理啟動起來」無關,不在解鎖路徑上。
## Wiki 頁命名總表
所有 wiki 頁面一律採雙層命名。`MAINTAIN` 是唯一只有目錄頁的類型,理由見表下:
| 類型 | 目錄頁 | 內容頁 | 用途 | 擁有者 |
| --- | --- | --- | --- | --- |
| `QUESTION` | `QUESTION_CONTENTS` | `QUESTION_{HASH}` | 問詢目錄、單一存取庫的問詢紀錄 | jsc-ask |
| `PLAN` | `PLAN_CONTENTS` | `PLAN_{HASH}` | 計畫目錄、計畫頁 | jsc-sdlc |
| `ANALYZE` | `ANALYZE_CONTENTS` | `ANALYZE_{HASH}` | 分析目錄、分析頁 | jsc-sdlc |
| `DELIVER` | `DELIVER_CONTENTS` | `DELIVER_{HASH}` | 交付目錄、工作包交付文件 | jsc-sdlc |
| `MAINTAIN` | `MAINTAIN_CONTENTS` | 無 | 維護登記目錄:登記進維護期的專案、維護方式、起訖日期與前次維護時間 | jsc-sdlc |
| `REPO` | `REPO_CONTENTS` | `REPO_{HASH}` | 盤點目錄、存取庫盤點頁 | jsc-sdlc |
| `LOG` | `LOG_CONTENTS` | `LOG_{HASH}` | 日誌目錄、工作日誌頁 | jsc-log |
| `LEARN` | `LEARN_CONTENTS` | `LEARN_{HASH}` | 教訓目錄、技能教訓頁 | jsc-log |
| `ERROR` | `ERROR_CONTENTS` | `ERROR_{HASH}` | 異常目錄、異常頁 | jsc-hooks |
| `CHECK` | `CHECK_CONTENTS` | `CHECK_{HASH}` | 體檢目錄、執行環境體檢頁 | jsc-cli |
| `REPORT` | `REPORT_CONTENTS` | `REPORT_{HASH}` | 報表目錄、工作報表頁(年、月、週、日各一頁) | jsc-log |
| `SKILLSET` | `SKILLSET_CONTENTS` | `SKILLSET_{HASH}` | 技能組異動目錄、技能組異動報告頁(新增、更新、刪除、批次更新之後的驗證結果與改動清單) | jsc-meta |
| `TOOLING` | `TOOLING_CONTENTS` | `TOOLING_{HASH}` | 技能盤點目錄、單機單 CLI 的技能盤點頁:一台機器上某一支 CLI 的已安裝 plugin 與版本、可用技能、hook 接線狀態 | jsc-meta |
| `MONITOR` | `MONITOR_CONTENTS` | `MONITOR_{HASH}` | 助理巡檢的監控頁。技能與 hook 每跑一次就留下事件,助理把事件收攏、判斷健康狀態、寫進這裡 | jsc-assist |
`MAINTAIN` 沒有內容頁。維護登記全部寫在 `MAINTAIN_CONTENTS` 的表格裡:`jsc-sdlc:implement` 只往那一頁附加登記,`jsc-sdlc:maintain` 只讀那一頁再回寫「前次維護時間」,兩支都沒有產生 `MAINTAIN_{HASH}` 的步驟,`jsc-sdlc/templates/` 也沒有對應範本。總表以前列著這個內容頁,照著找只會找到一個不存在的頁。要補內容頁就先補技能步驟與範本,不能只在總表上寫著。
`{HASH}` 一律為 `{owner}/{repo}`(必要時加上主題字串)的 SHA-1 前 8 碼,大寫。
若第一碼是 `0-9`、`A`、`B`、`C`,就改成 `H` 加上原 SHA-1 前 7 碼,總長仍維持 8 碼。
同一規則套用到所有目錄頁與內容頁。
`REPORT` 用得到那個主題字串:雜湊來源為 `{owner}/{repo}/{期間}`,期間是 `daily`、`weekly`、`monthly`、`yearly` 其中之一。
年、月、週、日各自一頁,每頁內依期間累積分節。
`SKILLSET` 的雜湊來源就是被改動的 domain 存取庫 `{owner}/{repo}`,算法同上,由同一支 `jsc-gitea/tools/hash-id` 產生。
頁內**累積**歷次異動:每次異動附加一節,不覆蓋舊紀錄。要看一支技能改過幾次,就在同一頁上翻。
`CHECK` 記的是一台執行環境,不是一個存取庫,所以雜湊來源為 `{主機名}/{登入帳號}`。
8 碼與 `H` 前綴的算法完全相同,由同一支 `jsc-gitea/tools/hash-id` 產生。
在沒有存取庫的目錄也跑得出體檢,是這個例外存在的原因。
機器層的雜湊來源不只這一個,`MONITOR` 也照同一組取值,規則見下。
`TOOLING` 記的也是機器層事實,雜湊來源再多一段:`{主機名}/{工具名稱}/{登入帳號}`。
`{工具名稱}` 是 CLI 代號,取自 `jsc-cli/tools/detect-clis.sh` 輸出的第一欄,值為 `claude`、`codex`、`copilot`、`antigravity`、`kiro` 其中之一。
一台機器、一支 CLI、一個帳號各一頁;算法同上,由同一支 `jsc-gitea/tools/hash-id` 產生。
**為什麼要帶工具名稱。** 每支 CLI 各有自己的已安裝 plugin 集合,也各有自己的 hook 接線狀態,那是五組互相獨立的事實。
少了中間那一段,同一台機器上五支 CLI 會算出同一個雜湊,五份盤點互相覆蓋,最後只剩最後寫入的那一支,讀的人卻看不出被蓋掉。
帶上工具名稱,一支 CLI 就有一頁,換一支 CLI 重跑也不會動到別支的頁。
`MONITOR` 的雜湊來源比照 `CHECK`,取 `{主機名}/{登入帳號}`。
助理巡檢的是一台機器,不是一個存取庫。
一台機器一頁,換一支 CLI 不另開頁。
算法同上,由同一支 `jsc-gitea/tools/hash-id` 產生。
**為什麼不帶工具名稱。** 這一點與 `TOOLING` 相反。
`TOOLING` 一支 CLI 一頁,因為每支 CLI 各有自己的已安裝 plugin 與 hook 接線。
助理看的是整台機器一份心跳、一本待辦簿,不分 CLI,所以中間那一段不能加。
**監控頁一律附加,不覆寫。** 助理的寫入是背景行為,覆寫錯了沒人在現場。
`TOOLING` 內容頁是每次盤點覆寫整頁,兩者的寫入語意剛好相反,不要混用。
## 審核檢查清單
新增或更新技能後逐項檢查,任一不符就修正:
- [ ] 名稱符合命名規則,且與既有技能目標不重複
- [ ] description 為英文、≤ 5 句或 ≤ 5 步驟(滿足任一即通過)、含觸發時機(何時用、何時不用)
- [ ] 可 hook 的規則已下放 jsc-hooks;可工具化的流程已下放 tools/;SKILL.md 沒有保留可由標準輸入輸出執行的細節流程
- [ ] 細節流程已標示 MUST run as a sub agent
- [ ] gitea 操作透過 gitea.sh 或 tea
- [ ] wiki repo 與 Gitea 認證先讀目前 shell 繼承的環境變數;只有缺值或無法解析時才詢問;頁面類型不得跨用其他 `JSC_WIKI_REPO_{TYPE}`
- [ ] 問詢透過 jsc-ask 決策樹規則
- [ ] `tools/` 與 `hooks/` 內的 shell 腳本都通過 `sh -n`;技能直接呼叫的腳本都存在、可執行,且退出碼有分流
- [ ] hook 相關變更已用 `jsc-hooks/tools/wire-cli.sh smoke {cli}` 實測;沒有偵測到 CLI 時,至少跑 `smoke codex` 並標明是預設 hook smoke。行數讀腳本自己印的 `lines` 那一行,**技能與 README 都不得寫死數字**——腳本會自我斷言,抄一份數字進文件,加減判定路徑時就漂移,稽核反而被舊數字誤導
- [ ] 唯讀的稽核與體檢流程呼叫 `wire-cli.sh` 時帶 `JSC_READONLY=1`:打錯子命令就由程式擋下(exit 6),不靠呼叫端自我約束;`status` 與 `smoke` 不受影響
- [ ] SKILL.md 整份為英文(要原樣輸出的繁中字面除外);README、AGENTS、templates、references 為 STE100 繁中;UTF-8 無亂碼
- [ ] 所有非程式碼輸出(程式碼註解、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 每支 `skills/*/SKILL.md` 的 frontmatter 解析得動,`tools/lint-frontmatter.sh {domain-path}` 對該 domain 退出 0;退出 3 是「什麼都沒掃」,不算通過。frontmatter 有語法錯誤時,Antigravity 會**靜默丟棄整支技能**,沒有任何錯誤訊息,只有這支腳本抓得到
- [ ] 已同步更新該 domain 的 README「Skills 目錄」與三份 manifest 的 version
- [ ] PR 的 base 符合「PR 分支階梯」,沒有越級
流程檢查四項,對照技能自己的流程逐項確認:
- [ ] 技能內外引用的步驟編號、檔案路徑、節標題都真的存在,指標指得到。踩過的實例:規則搬到 `references/` 後指標指向空處,照著指過去只看到空白
- [ ] 每個步驟以可檢核的完成條件結尾,沒有「理解後」「適當地」這類模糊語
- [ ] 每個外部呼叫(腳本、API、其他技能)的失敗情況都有明寫怎麼辦,退出碼都有分流
- [ ] 技能自己裝的閘門不會擋掉解除那道閘門的唯一路徑(閘門不自鎖)。踩過的實例:工作包閘門若擋掉 `implement`,結清 PR 就沒有路徑