Files
jiantw83 5541a41ff7 docs(guidelines): 新增目錄頁條列與內容頁圖表優先的區分準則
What
- 「目錄頁專用存取庫」從四條規則擴成五條,新增的第 5 條把版面判準定下來:目錄頁一律大標題加條列,內容頁才維持圖表優先。
- 第 5 條裡另立一段講 `<key-col>` 怎麼決定:那是舊表格裡持有內容頁連結那一欄的序號,只在自動轉檔時用得到,序號一律照線上那一頁實際的欄位排法填。
- 目錄頁與內容頁的對照表多一列「版面」。
- 稽核檢查清單新增兩項,一項查目錄頁版面與範本是否照第 5 條,一項查 `<key-col>` 的填法。
- `MAINTAIN` 沒有內容頁那一段,說法從「寫在表格裡」改成「寫在條列區塊上,一個專案一個 H2 區塊」。
- 跨存取庫沒有原子性那一段,回報對象從「未寫入的目錄列」改成「沒寫進去的目錄頁區塊」。

Why
- 這條區分是整組技能之後寫 wiki 的判準。準則裡沒有正本,每支技能各自解讀,改完的範本過一輪又會長回表格。
- `<key-col>` 填錯的後果是靜默的:標題會轉成那一欄的純文字、跟鍵對不上,既有那一筆被當成新的附加到頁尾,同一筆變成兩個區塊,舊區塊從此再也更新不到,而且不會有任何錯誤訊息。這一輪稽核就抓到六個頁型填錯。
- 範本的欄位順序與線上那一頁常常不一樣,而自動轉檔跑的是線上那一頁,所以序號不能照範本推。
- 檢查清單沒有對應項,這條準則就只剩內文約束,沒有稽核時的把關。

How
- 第 5 條寫明目錄頁的三段版面、H2 標題就是鍵且寫成內容頁頁名、欄位格式 `- {欄位名}:{值}`、頁上不留 markdown 表格也不放 mermaid,舊表格頁由工具讀到就自動轉寫回、不另跑批次搬移也不得手工搬。
- `<key-col>` 那一段要求先把線上那一頁讀回來確認連結落在第幾欄再填,並寫明填錯的靜默後果;線上是空頁、沒有舊表格要轉時照範本填即可。
- 補上「為什麼分兩種」的理由:目錄頁是索引,只給人挑一筆點進去,條列式壞也只壞一塊;內容頁一頁講一件事的全貌,流程與比較拿圖表最省讀者的力氣。

Who
- 全部十四種頁型的目錄頁與寫這些頁的每支技能都受這條準則約束。
- 稽核技能多兩項要判的檢查項。
2026-09-02 17:21:02 +08:00

55 KiB
Raw Permalink 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 的行銷語
純唯讀的技能 「可驗證跡象」欄寫「除了收尾的 skill-end 事件以外沒有寫入跡象,只有回報內容」,不得留白。收尾事件是每支技能都有的那一筆,唯讀技能也不例外
更新時機 技能異動時在同一個 PR 內一起更新:新增技能就加一節、刪除就移除該節、改行為就改該節
收尾事件 「可驗證跡象」那一列要寫到收尾的 skill-end 事件或 events.jsonl,規則見「執行狀態回報」一節
檢查腳本 jsc-meta/tools/check-behaviors.sh {domain-path}

check-behaviors.sh 的結束碼分流:

結束碼 意義
0 行為清單與 skills/ 相符,五個欄位齊全、內容欄非空,且每一節的「可驗證跡象」都寫了收尾的 skill-end 事件
1 不符:缺節、多節、順序不對、表格不對、缺欄位、欄位空白,或「可驗證跡象」沒寫到收尾的 skill-end 事件,逐項印在 stderr,照著修再重跑
2 用法錯誤:本腳本只吃一個參數
3 找不到 references/behaviors.md、找不到 skills/,或 skills/ 底下一支 SKILL.md 都沒有。什麼都沒查,不等於通過,先補齊檔案再重跑

為什麼一個 domain 一份,不集中在 jsc-meta。 技能改動與行為清單放同一個存取庫,才進得了同一個 PR;審的人在一頁 diff 上就看得出行為改了、清單也改了。集中在 meta 的話,改一支技能要開兩條 PR,一條在 domain、一條在 meta,兩條互相等待,先併的那條讓清單與技能對不上,稽核抓到的是自己造出來的漂移。跨存取庫的東西沒有原子性,同一份事實就不要拆兩邊放。

執行狀態回報

技能與 hook 每跑一次都要在本機事件流留下結果,助理巡檢再排空、彙整、寫監控頁。 事件流是 $JSC_HOME/usage/events.jsonl,一次一行,只增不改。

項目 規則
誰寫 start jsc-hooks/hooks/skill-usage.sh。技能被叫用的當下就寫,SKILL.md 一個字都不必改
誰寫 end 技能自己在收尾步驟寫,一次執行一筆
怎麼寫 {jsc-hooks 路徑}/tools/report-status.sh skill-end jsc-{domain}:{技能名} {status} {結束碼} [detail]
路徑怎麼解 沿用該技能原本呼叫別的 plugin 腳本的那一套,不另外發明一種
找不到腳本 安靜跳過,照常收尾。回報機制不在場,不可以讓被回報的技能跟著失敗
回報自己失敗 一樣吞掉。這支腳本的三個記錄子命令一律回 0,呼叫端不得因為它的結束碼改變自己的結局
detail 選填,單行,最多 200 字。長內容另存別處,不要塞進這一行
寫進行為清單 該技能在 references/behaviors.md 的「關鍵步驟」「完成條件」「可驗證跡象」三列都要提到這一筆事件
檢查腳本 jsc-meta/tools/check-behaviors.sh {domain-path} 斷言「可驗證跡象」那一列寫到 skill-end 或 events.jsonl

status 五選一,SKILL.md 要逐項寫清楚這支技能什麼情況選哪一個:

status 什麼時候用
ok 完成條件全部達成
blocked 被閘門或前置條件擋下,沒有做事。例如版本前置檢查擋下、相依 PR 未合併
failed 做到一半失敗。例如 API 回非預期狀態、寫入失敗
degraded 做完了但有部分沒達成。例如內容頁寫成功、目錄頁沒更新
aborted 使用者中止,或前提不成立而主動停止

end 為什麼不能由 hook 代勞。 hook 接在技能工具呼叫之後就觸發,那一刻技能的實際工作 還在後面的模型輪次,成敗根本還沒發生。hook 在原理上看不到結果,寫得出來的只有「開始跑了」。 所以 start 是免費的,end 躲不掉要由技能自己寫。

有 start 沒有配對的 end,就是中止。 這正是這條規則要補的洞:現行紀錄只記「被叫用」, 跑完整輪的技能與開場就停的技能長得一模一樣。收尾少寫這一筆,那支技能每一次都會被算成中止, 而且不會有任何錯誤訊息——助理讀到的是一串沒有結局的技能,看起來像整組技能都在半路死掉。

環境變數

變數 用途 未設定時
GITEA_HOST Gitea 站台(例:https://gitea.jsc.idv.tw) 詢問使用者
GITEA_TOKEN Gitea API token 改用 tea 登入金鑰(tea login list);tea 也沒有才詢問使用者
JSC_WIKI_REPO_CONTENTS 全部 *_CONTENTS 目錄頁所在的 {owner}/{repo},十四種型別的目錄頁共用這一組 退回 JSC_WIKI_REPO;再沒有就 exit 3。刻意不退回型別變數
JSC_WIKI_REPO_QUESTION QUESTION_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_PLAN PLAN_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_ANALYZE ANALYZE_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_DELIVER DELIVER_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_MAINTAIN 保留給 MAINTAIN 的內容頁。本類型目前只有目錄頁,目錄頁走 CONTENTS,所以這個變數現在解不到任何一頁 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_REPO REPO_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_LOG LOG_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_LEARN LEARN_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_ERROR ERROR_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_CHECK CHECK_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_REPORT REPORT_{HASH} 內容頁所在的 {owner}/{repo},也是 REPORT 雜湊來源的取值處 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_SKILLSET SKILLSET_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_TOOLING TOOLING_{HASH} 內容頁所在的 {owner}/{repo} 退回 JSC_WIKI_REPO
JSC_WIKI_REPO_MONITOR MONITOR_{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-gitea/tools/gitea.sh wiki-repo {TYPE} 執行:

  1. 內容頁只讀自己的 JSC_WIKI_REPO_{TYPE},只有該變數未設定時才退回 JSC_WIKI_REPO,再沒有就 exit 3。
  2. 目錄頁一律解 CONTENTS,鏈是 JSC_WIKI_REPO_CONTENTS → JSC_WIKI_REPO → exit 3。中間刻意不插型別變數:目錄頁全部落在同一個存取庫才找得齊,插了型別變數就等於十四個目錄頁散在十四處。

型別共十五種:十四種內容型別加上 CONTENTS。十五種都不得跨類型代用——拿 JSC_WIKI_REPO_PLAN 去寫 ANALYZE_{HASH} 不行,拿 JSC_WIKI_REPO_LOG 去寫 LOG_CONTENTS 也不行,後者要走 JSC_WIKI_REPO_CONTENTS。

版本前置檢查

技能組的每一支技能在被呼叫前都要過兩道版本檢查:本機載入版本沒有落後遠端發佈版本,以及這支技能所屬 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 的條列區塊上,一個專案一個 H2 區塊:jsc-sdlc:implement 只往那一頁附加登記,jsc-sdlc:maintain 只讀那一頁再回寫「前次維護時間」,兩支都沒有產生 MAINTAIN_{HASH} 的步驟,jsc-sdlc/templates/ 也沒有對應範本。總表以前列著這個內容頁,照著找只會找到一個不存在的頁。要補內容頁就先補技能步驟與範本,不能只在總表上寫著。

目錄頁專用存取庫

目錄頁與內容頁分屬不同存取庫,這是刻意的。

項目 目錄頁 內容頁
頁名 {TYPE}_CONTENTS {TYPE}_{HASH}
存取庫解析 一律解 CONTENTS:JSC_WIKI_REPO_CONTENTS → JSC_WIKI_REPO → exit 3 解自己的型別:JSC_WIKI_REPO_{TYPE} → JSC_WIKI_REPO → exit 3
帶雜湊 否 是
版面 大標題加條列:一筆一個 H2 區塊,欄位一行一條,不留 markdown 表格 圖表優先:mermaid 與表格優於散文

CONTENTS 因此是第十五種頁面類型,而且是唯一一種自己沒有頁的:沒有 CONTENTS_CONTENTS,也沒有 CONTENTS_{HASH}。 它只用來解存取庫,gitea.sh wiki-repo CONTENTS 是全部目錄頁的解析入口。 總表列的十四種是頁的分類,CONTENTS 是存取庫的分類,兩張清單長度不同是正常的。

五條規則,寫入前逐條核對:

  1. 任何 *_CONTENTS 頁都走 gitea.sh wiki-repo CONTENTS,十四種型別的目錄頁全部落在同一個存取庫。

  2. 目錄頁的解析鏈不退回型別變數。設了 JSC_WIKI_REPO_LOG 不會讓 LOG_CONTENTS 跟著搬過去。

  3. 文字加連結一律寫成 [{文字}]({連結})。 不分目錄頁或內容頁,也不分同存取庫或跨存取庫,全部只有這一種寫法。[[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 連結全面取消。網址取自 jsc-gitea/tools/gitea.sh wiki-url,不自行組路徑。

    為什麼只留一種寫法。 [[...]] 只在目前這個 wiki 內解析。寫錯不會報錯,畫面上看起來像正常文字或死連結,巡不到也修不了。兩種寫法並存就得逐處判斷兩端各自解到哪個存取庫;統一成一種,這個判斷消失。

  4. 連結先驗證連得到,才可以寫進文件。 寫入任何文件前,把要放進去的每一個連結交給 jsc-gitea/tools/link-check.sh,結束碼 0 才寫入。有任何一筆連不到就不寫入,把連不到的清單回報給呼叫端。結束碼分流以那支腳本的檔頭為準:0 才寫入,1 不得寫入並回報死連結那幾筆,2 與 3 補齊參數或 GITEA_HOST 再呼叫,7 停下來回報金鑰問題。

    為什麼驗證走 API,不看網頁狀態碼。 私有存取庫的網頁網址對未登入請求一律回 404。拿網頁狀態碼判斷,會把還在的頁判成死連結,接著整批被刪掉或改寫。金鑰失效那一種也要與死連結分開回報,理由一樣:一次金鑰過期就會把整批好頁判成壞的。

  5. 目錄頁一律「大標題加條列」,內容頁才維持圖表優先。 這條區分是全技能組的判準,每支技能寫 wiki 前先看自己寫的是哪一種頁。

    目錄頁的版面固定三段:H1 頁名、> 引言、然後每一筆紀錄一個 H2 區塊。H2 標題就是那一筆的鍵,寫成對應的內容頁頁名 {TYPE}_{HASH},標題不放連結、不放網址、不加前後綴、不加日期。欄位在標題底下一行一條,格式 - {欄位名}:{值},全形冒號,順序照原欄位從左到右,一欄一條,鍵那一欄照樣留一條,資料才不會少。區塊之間空一行,H2 與第一條之間空一行。目錄頁不留任何 markdown 表格,也不放 mermaid。舊頁還是表格時由 jsc-gitea/tools/wiki-contents.sh 讀到就自動轉成條列後寫回,不另跑批次搬移,也不得手工搬。

    <key-col> 怎麼決定:照線上那一頁實際的欄位排法,不是照範本。 wiki-contents.sh upsert <TYPE> <key-col> <key> <entry-file> [template-file] 的 <key-col> 填的是舊表格裡持有「內容頁連結」那一欄的序號,只在舊頁還是表格、需要自動轉檔時才用得到:轉檔時工具從那一欄的連結網址取最後一段路徑當 H2 標題。序號一律先把線上那一頁讀回來(gitea.sh wiki-get {CONTENTS 存取庫} {TYPE}_CONTENTS)、看連結實際落在第幾欄再填。不得照 templates/ 裡的欄位排法推:範本的欄位順序與線上那一頁常常不一樣,自動轉檔跑的是線上那一頁。填錯欄的後果是靜默的——標題會轉成那一欄的純文字(例如 plugins/ask),跟鍵 {TYPE}_{HASH} 對不上,既有那一筆被當成新的附加到頁尾,同一筆變兩個區塊,舊區塊從此再也更新不到,而且不會有任何錯誤訊息。線上是空頁、沒有舊表格要轉時,這個參數影響不到結果,照範本填即可。

    內容頁反過來:圖表優先,mermaid 與表格優於散文,這一條只針對內容頁,繼續有效。

    為什麼分兩種。 目錄頁是索引,每一筆的欄位一樣多、只給人挑一筆點進去;表格一寬就得橫向捲,欄位一多就對不上表頭,而且併行寫入時只要有人少打一根豎線,整張表就散掉,別人那一筆跟著看不見。條列式一筆一個區塊,寫入端只換自己那一塊,壞掉也只壞自己那一塊。內容頁要的是另一件事:一頁講一件事的全貌,流程與比較拿圖表最省讀者的力氣,所以圖表優先留在內容頁。

為什麼要分開。 目錄頁是全部使用者共用的索引,內容頁按專案或機器分散在各自的存取庫。混在一起的話,換一個專案就換一份索引,「這台機器有哪些頁」永遠問不到完整答案。索引集中一處、內容各自落地,才查得到全貌。

代價寫明:跨存取庫沒有原子性。內容頁寫成功、目錄頁寫失敗時,據實回報那個沒寫進去的目錄頁區塊與完整內容,不得反過來先寫目錄頁。

{HASH} 一律為 {owner}/{repo}(必要時加上主題字串)的完整 SHA-1,40 碼十六進位,a-f 一律轉大寫。 不截短、不加前綴:截短過的舊頁名以 jsc-gitea/tools/migrate-wiki.sh 遷移。 由 jsc-gitea/tools/hash-id 產生,空輸入 exit 2。 同一規則套用到所有內容頁;目錄頁不帶雜湊。

REPORT 用得到那個主題字串:雜湊來源為 {owner}/{repo}/{期間},期間是 daily、weekly、monthly、yearly 其中之一。 這裡的 {owner}/{repo} 是 REPORT wiki 存取庫,也就是 gitea.sh wiki-repo REPORT 解出來的那一組值,不是被統計的那個程式碼存取庫。 兩者取錯會算出不同雜湊,同一份報表就散成兩頁,而且兩頁都寫得成功、都看不出錯。 年、月、週、日各自一頁,每頁內依期間累積分節。

SKILLSET 的雜湊來源就是被改動的 domain 存取庫 {owner}/{repo},算法同上,由同一支 jsc-gitea/tools/hash-id 產生。 頁內累積歷次異動:每次異動附加一節,不覆蓋舊紀錄。要看一支技能改過幾次,就在同一頁上翻。

CHECK 記的是一台執行環境,不是一個存取庫,所以雜湊來源為 {主機名}/{登入帳號}。 算法同上,由同一支 jsc-gitea/tools/hash-id 產生。 在沒有存取庫的目錄也跑得出體檢,是這個例外存在的原因。 機器層的雜湊來源不只這一個,MONITOR 也照同一組取值,規則見下。

{主機名} 一律取短主機名,不含網域。 CHECK、TOOLING、MONITOR 三種機器層頁面共用這一條。 取值方式固定為:hostname 的輸出取第一個點以前的那一段,全部轉小寫;取不到就退回 uname -n 再做同樣的截取。 理由是同一台機器只能算出同一個雜湊。這幾張頁的來源不只一處:CHECK 由模型自己填,MONITOR 由 jsc-assist/tools/patrol.sh 用程式算。 同一台機器上,程式拿到 FQDN(web01.jsc.idv.tw)、模型填短主機名(web01),兩邊就各開一張頁,各寫各的,兩張都寫得成功,也都看不出被分裂。 截到第一個點以前,兩條路徑才收斂到同一個值。

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 產生。 patrol.sh 與模型填值兩條路徑都要照這一條截取,改動任一邊就回頭核對另一邊。

為什麼不帶工具名稱。 這一點與 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}
  • 目錄頁一律解 CONTENTS 存取庫(gitea.sh wiki-repo CONTENTS),內容頁解自己的型別;所有連結一律寫成 [{文字}]({連結}),網址取自 gitea.sh wiki-url,不用 [[頁名]]
  • 目錄頁寫成「大標題加條列」:一筆一個 H2 區塊、標題是內容頁頁名 {TYPE}_{HASH}、欄位一行一條 - {欄位名}:{值}、頁上沒有 markdown 表格;內容頁維持圖表優先(mermaid 與表格優於散文)。技能內文與 templates/ 的目錄頁樣板都照這一條,寫入一律走 jsc-gitea/tools/wiki-contents.sh upsert,不手工改頁。規則見「目錄頁專用存取庫」第 5 條
  • wiki-contents.sh upsert 的 <key-col> 是「舊表格裡持有內容頁連結那一欄的序號」,只供自動轉檔用;序號照線上那一頁實際的欄位排法填,先把線上頁讀回來確認,不照 templates/ 的欄位排法推。規則見「目錄頁專用存取庫」第 5 條的 <key-col> 段
  • 文件裡的連結都經過 jsc-gitea/tools/link-check.sh 驗證(結束碼 0 才寫入)且格式為 [{文字}]({連結});tools/check-link-format.sh {domain-path} 對該 domain 退出 0,退出 3 是「什麼都沒掃」,不算通過
  • 頁名樣式三處一致:jsc-gitea/tools/page-name.sh(正本)、jsc-hooks/hooks/comment-scope.sh、jsc-log/tools/worklog-pending.sh,tools/check-page-name.sh {root} 退出 0;退出 3 是「什麼都沒查」,不算通過。三處刻意不共用函式,因為 hook 必須自足,不得在執行期相依別的 plugin 路徑
  • 問詢透過 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 是「什麼都沒查」,不算通過
  • 每支技能的收尾步驟都呼叫 {jsc-hooks 路徑}/tools/report-status.sh skill-end jsc-{domain}:{技能名} {status} {結束碼},status 五選一且 SKILL.md 寫明哪一種情況選哪一個,找不到腳本安靜跳過、不讓技能跟著失敗;該技能的「關鍵步驟」「完成條件」「可驗證跡象」三列都寫到這一筆事件。規則見「執行狀態回報」
  • 該 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 就沒有路徑