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

42 KiB
Raw Blame History

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 的技能都引用那份文件,不在技能內各自抄欄位。

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。
  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。

狀態檔為什麼一支 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 就沒有路徑