docs(guidelines): 補上助理運行閘門與 fail-closed 閘門的專屬規則

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

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

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

Who:
助理閘門實作完之後,把當中的判斷收進準則,讓下一道同類閘門有依據。
This commit is contained in:
2026-09-01 15:27:06 +08:00
parent 3beca714fc
commit 7b6b9076ea
4 changed files with 77 additions and 3 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-meta", "name": "jsc-meta",
"version": "0.2.6", "version": "0.2.7",
"description": "技能組自我管理:新建、更新、刪除技能與技能準則", "description": "技能組自我管理:新建、更新、刪除技能與技能準則",
"skills": "./skills", "skills": "./skills",
"author": { "author": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-meta", "name": "jsc-meta",
"version": "0.2.6", "version": "0.2.7",
"description": "技能組自我管理:新建、更新、刪除技能與技能準則", "description": "技能組自我管理:新建、更新、刪除技能與技能準則",
"skills": "./skills", "skills": "./skills",
"jsc": { "jsc": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-meta", "name": "jsc-meta",
"version": "0.2.6", "version": "0.2.7",
"description": "技能組自我管理:新建、更新、刪除技能與技能準則", "description": "技能組自我管理:新建、更新、刪除技能與技能準則",
"skills": "./skills/", "skills": "./skills/",
"jsc": { "jsc": {
+74
View File
@@ -152,6 +152,8 @@ PR 開立、更新、留言修正的收尾回報格式只看 [`references/pr-rep
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` | | `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
| `JSC_PR_WATCH_INTERVAL` | `jsc-gitea/tools/pr-watch.sh` 輪詢 PR 狀態的間隔秒數 | 預設 60 | | `JSC_PR_WATCH_INTERVAL` | `jsc-gitea/tools/pr-watch.sh` 輪詢 PR 狀態的間隔秒數 | 預設 60 |
| `JSC_RESTART_GATE` | 部署後重啟閘門的開關,`off` 關閉整道閘門 | 閘門開啟 | | `JSC_RESTART_GATE` | 部署後重啟閘門的開關,`off` 關閉整道閘門 | 閘門開啟 |
| `JSC_ASSISTANT_GATE` | 助理運行閘門的開關,`off` 關閉整道閘門 | 閘門開啟 |
| `JSC_ASSISTANT_HEARTBEAT_TTL` | 助理心跳的過期門檻秒數。排程週期由這個值推導 | 預設 300;壞值退回預設 |
頁面類型只讀自己的 `JSC_WIKI_REPO_{TYPE}`。只有該變數未設定時,才退回 `JSC_WIKI_REPO`。不得跨類型代用。 頁面類型只讀自己的 `JSC_WIKI_REPO_{TYPE}`。只有該變數未設定時,才退回 `JSC_WIKI_REPO`。不得跨類型代用。
@@ -259,6 +261,78 @@ kiro 是唯一真的擋不了的,verdict 據實寫 `degraded`,不寫 `wired`
**清單認的是技能名,不是呼叫鏈。** 豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。後三支(`jsc-ask:ask`、`jsc-git:pr`、`jsc-git:commit`)自己不是收尾規則的主體,是為了讓前七支走得完才補進來的。`version-guard.sh` 的豁免清單當年也是為同一個原因收進 `jsc-ask:ask`。新增豁免技能時要一併想它會呼叫誰。 **清單認的是技能名,不是呼叫鏈。** 豁免技能轉呼叫的下一層若不在清單上,那一層照樣會被擋。後三支(`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 頁命名總表
所有 wiki 頁面一律採雙層命名。`MAINTAIN` 是唯一只有目錄頁的類型,理由見表下: 所有 wiki 頁面一律採雙層命名。`MAINTAIN` 是唯一只有目錄頁的類型,理由見表下: