# 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}{技能名}`。解析不出就印空字串並退出 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 就沒有路徑