Merge pull request '讓排程輪次自己帶齊環境與權限,監控頁改成固定三塊,文件與三份外掛清單一併升到 0.1.1' (#7) from fix/unattended-patrol-round-env-and-permissions into develop
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-assist",
|
"name": "jsc-assist",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-assist",
|
"name": "jsc-assist",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
1. 不做任何要問使用者的決策。背景巡檢時靜默套預設值,等於把逐項共識整條做掉。
|
1. 不做任何要問使用者的決策。背景巡檢時靜默套預設值,等於把逐項共識整條做掉。
|
||||||
2. 不參與閘門判定。閘門必須留在 hook:同步、不連網、毫秒級。助理只負責維持心跳。
|
2. 不參與閘門判定。閘門必須留在 hook:同步、不連網、毫秒級。助理只負責維持心跳。
|
||||||
3. 不寫程式碼存取庫、不 commit、不 push、不開 PR、不合併。
|
3. 不寫程式碼存取庫、不 commit、不 push、不開 PR、不合併。
|
||||||
4. 不覆寫 wiki 頁,一律只附加。助理的寫入是背景行為,覆寫錯了沒人在現場。
|
4. 監控頁只照固定三塊寫:最新一輪整塊換掉、摘要表保留近 24 輪、基本資料建頁之後不動;目錄頁只動自己那一列,別台機器的列一個字都不碰。軌跡留在摘要表,一輪一列,看得出是從哪一輪開始壞的;完整內容只留最新一輪,因為頁面要能讀完才有人讀。
|
||||||
5. 不刪除狀態檔、worktree 與 wiki 頁。破壞性操作留給人發動。
|
5. 不刪除狀態檔、worktree 與 wiki 頁。破壞性操作留給人發動。
|
||||||
6. 不自動執行自己提出的建議。建議與執行是兩件事,自動接下去等於整條流程沒人按過同意就跑完。
|
6. 不自動執行自己提出的建議。建議與執行是兩件事,自動接下去等於整條流程沒人按過同意就跑完。
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
### `assistant`
|
### `assistant`
|
||||||
|
|
||||||
助理主體,四個操作:`start` 啟動、`status` 查現況、`patrol` 跑一輪巡檢、`stop` 停止。心跳的寫入、判定與清除一律交給 `jsc-hooks` 的 `hooks/heartbeat.sh`,判定只有那一份;系統排程一律交給 `tools/schedule.sh`;一輪巡檢的流程交給 `tools/patrol.sh`。**心跳由巡檢寫,而且只由巡檢寫**:一輪跑完、結果寫上監控頁了,才寫那一次心跳,所以心跳新鮮等於「上一輪巡檢真的做完了」。`start` 先跑一輪巡檢,再裝上巡檢那一筆排程;巡檢週期由心跳的過期門檻算出來,兩個數字綁在一起。`patrol` 讀四項來源(使用統計、版本與重啟閘門、SDLC 階段鎖與工作包鎖、心跳自述),四項各自獨立,一項掛掉其餘三項照跑、照記,結果一律附加到 `MONITOR_{HASH}`、不覆寫。`status` 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
|
助理主體,四個操作:`start` 啟動、`status` 查現況、`patrol` 跑一輪巡檢、`stop` 停止。心跳的寫入、判定與清除一律交給 `jsc-hooks` 的 `hooks/heartbeat.sh`,判定只有那一份;系統排程一律交給 `tools/schedule.sh`;一輪巡檢的流程交給 `tools/patrol.sh`。工具一律用 `$JSC_HOME/current/{外掛名}` 那一組不帶版本的路徑叫,不用技能提示給的快取基底目錄——權限只放行 current 那一組。**心跳由巡檢寫,而且只由巡檢寫**:一輪跑完、結果寫上監控頁了,才寫那一次心跳,所以心跳新鮮等於「上一輪巡檢真的做完了」。`start` 先跑一輪巡檢,再裝上巡檢那一筆排程;巡檢週期由心跳的過期門檻算出來,兩個數字綁在一起。`patrol` 讀四項來源(使用統計、版本與重啟閘門、SDLC 階段鎖與工作包鎖、心跳自述),四項各自獨立,一項掛掉其餘三項照跑、照記,結果寫上 `MONITOR_{HASH}`:那頁固定三塊,基本資料不動、最新一輪整塊換掉、摘要表保留近 24 輪,一輪一列。`status` 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
|
||||||
|
|
||||||
<!-- JSC-SKILLS:END -->
|
<!-- JSC-SKILLS:END -->
|
||||||
|
|
||||||
@@ -43,11 +43,11 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
| 檔案 | 用途 |
|
| 檔案 | 用途 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `tools/schedule.sh` | 助理系統排程的安裝、移除與查現況。三個子命令 `install`、`remove`、`status`,只裝 `patrol` 這一筆——心跳由巡檢自己寫,`install heartbeat` 一律回 6,舊版遺留的心跳條目由 `install patrol` 順手清掉。巡檢週期由心跳的過期門檻算出來(`2 × 週期 × 60 < 門檻`,再取能整除一小時的分鐘數):門檻 300 秒是每 2 分鐘一輪,門檻 1800 秒是每 12 分鐘一輪。Linux、WSL 與 macOS 走 crontab,Windows 走 schtasks。條目行尾帶固定標記 `# jsc-assist:assistant {工作}`,只動自己那一筆,別人的排程一行都不碰。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron。`--dry-run` 只印組出來的條目與寫回後的內容,什麼都不動 |
|
| `tools/schedule.sh` | 助理系統排程的安裝、移除與查現況。三個子命令 `install`、`remove`、`status`,只裝 `patrol` 這一筆——心跳由巡檢自己寫,`install heartbeat` 一律回 6,舊版遺留的心跳條目由 `install patrol` 順手清掉。巡檢週期由心跳的過期門檻算出來(`2 × 週期 × 60 < 門檻`,再取能整除一小時的分鐘數):門檻 300 秒是每 2 分鐘一輪,門檻 1800 秒是每 12 分鐘一輪。Linux、WSL 與 macOS 走 crontab,Windows 走 schtasks。條目行尾帶固定標記 `# jsc-assist:assistant {工作}`,只動自己那一筆,別人的排程一行都不碰。條目自己把環境帶齊:CLI 用 `command -v` 解成絕對路徑、安裝當下把 `GITEA_HOST`、`GITEA_TOKEN`、`JSC_HOME`、`JSC_ASSISTANT_HEARTBEAT_TTL` 與已設定的 `JSC_WIKI_REPO` 系列快照進條目、自帶 `JSC_GITEA_CONFIRM=yes`——cron 的 PATH 很短、不讀設定檔、也沒有 tty。印出條目時金鑰一律遮掉,條目本身含金鑰快照,crontab 檔案要保持只有本人讀得到,變數改過要重跑一次 install。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron;也會檢查 `$JSC_HOME/current` 那組連結在不在、印出這一輪要開的 allow 規則,連結不在只警告、不代建。`--dry-run` 只印組出來的條目與寫回後的內容,什麼都不動 |
|
||||||
| `tools/patrol.sh` | 一輪巡檢的收攏與收口。三個子命令:`collect` 取鎖、讀四項來源、組出監控頁要附加的那一節與目錄頁那一列;`finish` 在監控頁寫成之後才寫心跳、換上用量快照、放掉鎖;`abort` 只放掉鎖,不寫心跳。四項來源各自獨立,一項失敗其餘三項照跑,失敗那一項在頁上寫明是「這一項失敗」而不是沒資料。整輪拿一把目錄鎖,上一輪還在跑就回 4 讓開;鎖逾時(門檻取心跳門檻)會被下一輪搶回來,並在頁上記一筆。`version-guard.sh report` 回「查詢失敗」時照原字抄,不補查、不美化 |
|
| `tools/patrol.sh` | 一輪巡檢的收攏與收口。三個子命令:`collect` 取鎖、讀四項來源、組出監控頁的「最新一輪」與「近 24 輪摘要」兩塊、本輪的摘要列與目錄頁那一列;`finish` 在監控頁寫成之後才寫心跳、換上用量快照、放掉鎖;`abort` 只放掉鎖,不寫心跳。四項來源各自獨立,一項失敗其餘三項照跑,失敗那一項在頁上寫明是「這一項失敗」而不是沒資料。整輪拿一把目錄鎖,上一輪還在跑就回 4 讓開;鎖逾時(門檻取心跳門檻)會被下一輪搶回來,並在頁上記一筆。`version-guard.sh report` 回「查詢失敗」時照原字抄,不補查、不美化 |
|
||||||
| `references/behaviors.md` | 本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 `plugins/meta` 的 `references/guidelines.md`「技能行為清單」 |
|
| `references/behaviors.md` | 本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 `plugins/meta` 的 `references/guidelines.md`「技能行為清單」 |
|
||||||
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本。一列代表一台機器,雜湊來源是 `{主機名}/{登入帳號}`。寫入語意是**只更新自己那一列**:比對主機與帳號兩欄,別台機器的列原樣保留,禁止整頁覆蓋 |
|
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本。一列代表一台機器,雜湊來源是 `{主機名}/{登入帳號}`。寫入語意是**只更新自己那一列**:比對主機與帳號兩欄,別台機器的列原樣保留,禁止整頁覆蓋 |
|
||||||
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。寫入語意與目錄頁相反,是**一律附加一節、不覆寫**:一次巡檢一節,節標題帶時間戳,既有的節一個字都不動 |
|
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。頁面固定三塊:本頁基本資料建頁時寫一次就不動、最新一輪每輪整塊換掉、近 24 輪摘要一輪一列且最新的在最上面。軌跡留在摘要表,完整內容只留最新一輪,頁面才讀得完 |
|
||||||
|
|
||||||
## 助理的狀態檔
|
## 助理的狀態檔
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| `tasks/{id}` | 待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
|
| `tasks/{id}` | 待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
|
||||||
| `schedule.log` | 排程條目的輸出。刻意放在存取庫外面:寫進專案會多出未追蹤檔,污染別人的變更盤點 |
|
| `schedule.log` | 排程條目的輸出。刻意放在存取庫外面:寫進專案會多出未追蹤檔,污染別人的變更盤點 |
|
||||||
| `patrol.lock/` | 一輪巡檢的鎖,是目錄——`mkdir` 是原子操作,搶不到就是別人在跑。裡面的 `info` 記 `round`、`pid`、`started` |
|
| `patrol.lock/` | 一輪巡檢的鎖,是目錄——`mkdir` 是原子操作,搶不到就是別人在跑。裡面的 `info` 記 `round`、`pid`、`started` |
|
||||||
| `patrol/` | 本輪巡檢的暫存檔:`section.md` 是要附加的那一節,`newpage.md` 是頁不存在時要建的整頁,`contents.tsv` 是目錄頁那一列 |
|
| `patrol/` | 本輪巡檢的暫存檔:`latest.md` 是「最新一輪」那一塊,`summary.md` 是摘要那一塊、裡面已經放好本輪這一列,`summary-row.md` 只有那一列,`newpage.md` 是頁不存在時要建的整頁,`contents.tsv` 是目錄頁那一列 |
|
||||||
| `usage-prev.tsv` | 上一輪記下來的累計用量。有了它,下一輪的「本輪次數」才算得出來;沒有它的第一輪一律寫「-」,不拿累計冒充本輪 |
|
| `usage-prev.tsv` | 上一輪記下來的累計用量。有了它,下一輪的「本輪次數」才算得出來;沒有它的第一輪一律寫「-」,不拿累計冒充本輪 |
|
||||||
|
|
||||||
## 相關 domain
|
## 相關 domain
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-assist",
|
"name": "jsc-assist",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||||
"skills": "./skills/",
|
"skills": "./skills/",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 觸發時機 | 要啟動助理、要停止助理、要跑一輪巡檢,或要問助理現在還在不在跑、待辦簿剩下哪幾筆時用。四個操作 `start`、`status`、`patrol`、`stop` 都走這一支。排程每一輪叫起來的也是這一支的 `patrol`。執行環境健檢不走這支,走 `jsc-cli:doctor`。技能使用次數不走這支,走 `jsc-log:stats` |
|
| 觸發時機 | 要啟動助理、要停止助理、要跑一輪巡檢,或要問助理現在還在不在跑、待辦簿剩下哪幾筆時用。四個操作 `start`、`status`、`patrol`、`stop` 都走這一支。排程每一輪叫起來的也是這一支的 `patrol`。執行環境健檢不走這支,走 `jsc-cli:doctor`。技能使用次數不走這支,走 `jsc-log:stats` |
|
||||||
| 關鍵步驟 | 先認出使用者要的是哪一個操作,`patrol` 那一路全程不問人。`start`:先照 `patrol` 的每一步跑完一輪巡檢,第一次心跳由那一輪寫、不另外寫、跑不完就不算啟動、跑 `heartbeat.sh report` 確認 `state=fresh`、跑 `tools/schedule.sh install patrol` 裝巡檢那一筆排程、依結束碼選一段收尾訊息印出——排程接上、排程寫進去了但 cron 沒在跑、排程沒接上三種各一段。心跳那一筆不裝了,`install heartbeat` 一律回 6。`patrol`:跑 `tools/patrol.sh collect` 取鎖並讀四項來源、結束碼 4 就讓開不寫任何東西、結束碼 1 與 3 照樣把那一節寫上監控頁、`hash` 是空的就 `abort`、把 `section_file` 交給 `jsc-gitea:wiki` 附加到 `MONITOR_{HASH}`、頁不存在(唯有結束碼 4)才用 `newpage_file` 建頁、把 `contents_file` 的 `row` 更新到 `MONITOR_CONTENTS` 自己那一列、兩次寫入任一失敗就 `abort` 且不寫心跳、全部寫成才跑 `tools/patrol.sh finish` 寫心跳、最後印出四項結果與待人處理列。`status`:跑 `heartbeat.sh report` 取心跳現況、把 `state` 對映成新鮮、過期、心跳檔損壞、不存在、不自己解析心跳檔也不自己判定、從 `file=` 解出助理目錄後列出 `tasks/` 底下每一個檔案並解析 `state`、`title`、`next_run`、`fail_count`、跑 `tools/schedule.sh status` 取排程現況與週期、印成心跳、排程、待辦三塊、`fail_count` 大於 0 的列標上「已連續失敗 N 次」、心跳與排程兜起來會誤讀的四種組合各補一句話。`stop`:先跑 `heartbeat.sh report` 留下原本的狀態、再跑 `tools/schedule.sh remove all` 移除排程與舊版遺留的心跳條目、最後才跑 `heartbeat.sh clear` 清掉心跳、印出停止訊息並說明心跳清掉之後閘門會擋人、同時說明閘門還沒接線所以現在擋不到人 |
|
| 關鍵步驟 | 先認出使用者要的是哪一個操作,`patrol` 那一路全程不問人。`start`:先照 `patrol` 的每一步跑完一輪巡檢,第一次心跳由那一輪寫、不另外寫、跑不完就不算啟動、跑 `heartbeat.sh report` 確認 `state=fresh`、跑 `tools/schedule.sh install patrol` 裝巡檢那一筆排程、把它印的 `allow_rule=` 每一行、環境快照提醒與 `current` 連結缺漏的警告原樣轉給人、依結束碼選一段收尾訊息印出——排程接上、排程寫進去了但 cron 沒在跑、排程沒接上三種各一段。心跳那一筆不裝了,`install heartbeat` 一律回 6。`patrol`:跑 `tools/patrol.sh collect` 取鎖並讀四項來源、結束碼 4 就讓開不寫任何東西、結束碼 1 與 3 照樣把這一輪寫上監控頁、`hash` 是空的就 `abort`、經 `jsc-gitea:wiki` 讀回 `MONITOR_{HASH}` 舊頁、基本資料原樣留著、最新一輪那一塊整塊換成 `latest_file`、`summary_file` 的本輪那一列擺最上面(五欄:巡檢時間、本輪判定、四項成敗、待人處理、警示來源)、舊的資料列接在下面並截到 24 列、三塊重組成整頁寫回、頁不存在(唯有結束碼 4)才用 `newpage_file` 建頁、讀不回舊頁就不寫、把 `contents_file` 的 `row` 更新到 `MONITOR_CONTENTS` 自己那一列、兩次寫入任一失敗就 `abort` 且不寫心跳、全部寫成才跑 `tools/patrol.sh finish` 寫心跳、最後印出四項結果、判成警示時的警示來源與待人處理列。`status`:跑 `heartbeat.sh report` 取心跳現況、把 `state` 對映成新鮮、過期、心跳檔損壞、不存在、不自己解析心跳檔也不自己判定、從 `file=` 解出助理目錄後列出 `tasks/` 底下每一個檔案並解析 `state`、`title`、`next_run`、`fail_count`、跑 `tools/schedule.sh status` 取排程現況與週期、印成心跳、排程、待辦三塊、`fail_count` 大於 0 的列標上「已連續失敗 N 次」、心跳與排程兜起來會誤讀的四種組合各補一句話。`stop`:先跑 `heartbeat.sh report` 留下原本的狀態、再跑 `tools/schedule.sh remove all` 移除排程與舊版遺留的心跳條目、最後才跑 `heartbeat.sh clear` 清掉心跳、印出停止訊息並說明心跳清掉之後閘門會擋人、同時說明閘門還沒接線所以現在擋不到人 |
|
||||||
| 外部呼叫 | `jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。`write` 只由 `tools/patrol.sh finish` 呼叫,技能自己不呼叫。本 domain 的 `tools/schedule.sh` 的 `install`、`remove`、`status` 三個子命令,七個結束碼各有處置:0 往下走、1 是條目裝了但 cron 沒在跑要照實講不會執行、2 是缺 jsc-hooks 導致門檻讀不到、3 是這台機器沒有排程機制、4 是排程操作失敗要原樣引用 stderr、5 是回讀驗證失敗要叫人自己去看 `crontab -l`、6 是呼叫寫錯,含 `install heartbeat` 與週期塞不進門檻。本 domain 的 `tools/patrol.sh` 的 `collect`、`finish`、`abort` 三個子命令,七個結束碼各有處置:0 往下走、1 部分失敗照樣寫頁、2 是 finish 找不到 heartbeat.sh 要回報「記下來了但沒有心跳」、3 是四項全失敗照樣寫頁且判定異常、4 是讓開或鎖被搶走一律不寫心跳、5 是檔案系統失敗要吵出來、6 是呼叫寫錯。巡檢那四項讀 `jsc-log/tools/usage-stats.sh`、`jsc-hooks/hooks/version-guard.sh report`、`jsc-hooks/hooks/restart-gate.sh report`、`$JSC_HOME/sessions/*.stage`、`$JSC_HOME/wp/*.pr`、`heartbeat.sh report`,全部只讀,任一項失敗不影響其餘三項。wiki 讀寫一律經 `jsc-gitea:wiki`,技能自己不拼 API 呼叫。crontab 與 schtasks 一律經 `tools/schedule.sh`。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。呼叫端沒講清楚要哪一個操作時走 `jsc-ask:ask` 的決策樹問,但 `patrol` 那一路一律不問。不參與閘門判定 |
|
| 外部呼叫 | 工具一律走 `$JSC_HOME/current/{外掛名}` 那一組不帶版本的路徑:`current/jsc-assist/tools/patrol.sh`、`current/jsc-assist/tools/schedule.sh`、`current/jsc-hooks/hooks/heartbeat.sh`,wiki 那一支是 `current/jsc-gitea/tools/gitea.sh`,`$JSC_HOME` 沒設就退回 `~/.jsc`;不拿技能提示給的快取基底目錄組工具路徑——權限只放行 current 那一組,用錯路徑會被靜靜擋掉。`jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。`write` 只由 `tools/patrol.sh finish` 呼叫,技能自己不呼叫。本 domain 的 `tools/schedule.sh` 的 `install`、`remove`、`status` 三個子命令:`install` 會查 `$JSC_HOME/current/jsc-assist` 與 `$JSC_HOME/current/jsc-gitea` 兩個連結在不在、不在就警告且不代建,會把巡檢的 CLI 用 `command -v` 解成絕對路徑、把 `GITEA_HOST`、`GITEA_TOKEN`、`JSC_HOME`、`JSC_ASSISTANT_HEARTBEAT_TTL` 與所有已設定的 `JSC_WIKI_REPO` 系列快照進條目、條目自帶 `JSC_GITEA_CONFIRM=yes`、並印出這一輪要開的 `allow_rule=` 規則(四支腳本各三種呼叫形式,含 `gitea.sh`——`Skill(jsc-gitea:wiki)` 只放行叫用技能,技能內部的 Bash 呼叫仍各自受檢;路徑是 `current` 那一組確切路徑,不用萬用字元);七個結束碼各有處置:0 往下走、1 是條目裝了但 cron 沒在跑要照實講不會執行、2 是缺 jsc-hooks 導致門檻讀不到、3 是這台機器沒有排程機制、4 是排程操作失敗要原樣引用 stderr、5 是回讀驗證失敗要叫人自己去看 `crontab -l`、6 是呼叫寫錯,含 `install heartbeat`、週期塞不進門檻、判不出 CLI,以及那一支 CLI 的執行檔不在 `PATH` 上。本 domain 的 `tools/patrol.sh` 的 `collect`、`finish`、`abort` 三個子命令,七個結束碼各有處置:0 往下走、1 部分失敗照樣寫頁、2 是 finish 找不到 heartbeat.sh 要回報「記下來了但沒有心跳」、3 是四項全失敗照樣寫頁且判定異常、4 是讓開或鎖被搶走一律不寫心跳、5 是檔案系統失敗要吵出來、6 是呼叫寫錯。巡檢那四項讀 `jsc-log/tools/usage-stats.sh`、`jsc-hooks/hooks/version-guard.sh report`、`jsc-hooks/hooks/restart-gate.sh report`、`$JSC_HOME/sessions/*.stage`、`$JSC_HOME/wp/*.pr`、`heartbeat.sh report`,全部只讀,任一項失敗不影響其餘三項。wiki 讀寫一律經 `jsc-gitea:wiki`,技能自己不拼 API 呼叫。crontab 與 schtasks 一律經 `tools/schedule.sh`。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。呼叫端沒講清楚要哪一個操作時走 `jsc-ask:ask` 的決策樹問,但 `patrol` 那一路一律不問。不參與閘門判定 |
|
||||||
| 完成條件 | `start` 要那一輪巡檢的 `finish` 回 0 且 `report` 回 `state=fresh`,才算啟動成功;巡檢沒寫成心跳一律回報失敗並停下,不得宣稱啟動;`schedule.sh install patrol` 回 1 要講明條目不會被執行與 `sudo service cron start`,不得宣稱排程會定時執行。`patrol` 要四項各自有 `status`、監控頁附加成功、目錄頁那一列更新成功、`finish` 回 0,才算一輪跑完;`collect` 回 4 是讓開,不算失敗也不寫任何東西;監控頁或目錄頁任一沒寫成就 `abort`,回報「這一輪沒有結果」,心跳一定不寫。`status` 要印出現況表,或印出「助理未運行」並說明原因;心跳不存在、待辦簿目錄不存在、待辦簿零筆、排程沒裝,四種都算正常結束。`stop` 要 `schedule.sh remove all` 先回 0、`clear` 再回 0,並印出帶三段話的停止訊息;`remove` 非 0 就回報排程還在、助理停不掉,不清心跳也不印停止訊息;`clear` 回 5 就回報心跳檔還在、助理沒有確實停掉,不印停止訊息 |
|
| 完成條件 | `start` 要那一輪巡檢的 `finish` 回 0 且 `report` 回 `state=fresh`,才算啟動成功;巡檢沒寫成心跳一律回報失敗並停下,不得宣稱啟動;`schedule.sh install patrol` 回 1 要講明條目不會被執行與 `sudo service cron start`,不得宣稱排程會定時執行;回 0 或 1 都要把 `allow_rule=` 各行、「條目含金鑰快照、變數改了要重裝」這句提醒,以及 `current` 連結缺漏的警告轉出去。`patrol` 要四項各自有 `status`、監控頁三塊重組寫成、目錄頁那一列更新成功、`finish` 回 0,才算一輪跑完;`collect` 回 4 是讓開,不算失敗也不寫任何東西;舊頁讀不回來就不寫,回報「這一輪沒有結果」;監控頁或目錄頁任一沒寫成就 `abort`,心跳一定不寫。`status` 要印出現況表,或印出「助理未運行」並說明原因;心跳不存在、待辦簿目錄不存在、待辦簿零筆、排程沒裝,四種都算正常結束。`stop` 要 `schedule.sh remove all` 先回 0、`clear` 再回 0,並印出帶三段話的停止訊息;`remove` 非 0 就回報排程還在、助理停不掉,不清心跳也不印停止訊息;`clear` 回 5 就回報心跳檔還在、助理沒有確實停掉,不印停止訊息 |
|
||||||
| 可驗證跡象 | `start` 之後 `$JSC_HOME/assistant/heartbeat` 存在,`ts` 是剛才那一輪的時間,`crontab -l` 找得到一筆帶 `# jsc-assist:assistant patrol` 的條目,而且只有一筆,帶 `# jsc-assist:assistant heartbeat` 的舊條目一筆都不剩。`patrol` 跑完之後 wiki 的 `MONITOR_{HASH}` 多一節、節標題帶時間戳、舊的節一字不改,`MONITOR_CONTENTS` 只有自己那一列變動,`$JSC_HOME/assistant/patrol/` 底下有本輪的 `section.md`、`newpage.md`、`contents.tsv`,`$JSC_HOME/assistant/usage-prev.tsv` 換成本輪的累計數,`$JSC_HOME/assistant/patrol.lock` 已經放掉。讓開的那一輪沒有任何寫入跡象。`stop` 之後心跳路徑不存在,`crontab -l` 找不到任何 `# jsc-assist:assistant` 條目。以上都不動別人的排程條目,條目數量前後相同。`status` 無寫入跡象,只有回報內容。四個操作都不動 `tasks/` 底下的檔案,也不動 worktree 與程式碼存取庫。排程的 log 一律在 `$JSC_HOME/assistant/schedule.log`,不落在任何存取庫 |
|
| 可驗證跡象 | `start` 之後 `$JSC_HOME/assistant/heartbeat` 存在,`ts` 是剛才那一輪的時間,`crontab -l` 找得到一筆帶 `# jsc-assist:assistant patrol` 的條目,而且只有一筆,帶 `# jsc-assist:assistant heartbeat` 的舊條目一筆都不剩;那一筆條目裡的 CLI 是絕對路徑,前面帶著 `JSC_GITEA_CONFIRM=yes` 與環境變數快照;install 印出的 `allow_rule=` 都是 `$JSC_HOME/current` 那一組確切路徑,沒有萬用字元,也沒有 `Write(...)`。`patrol` 跑完之後 wiki 的 `MONITOR_{HASH}` 只有三塊:基本資料一字未改、最新一輪換成本輪、摘要表最上面一列是本輪且總列數不超過 24,`MONITOR_CONTENTS` 只有自己那一列變動,`$JSC_HOME/assistant/patrol/` 底下有本輪的 `latest.md`、`summary.md`、`summary-row.md`、`newpage.md`、`contents.tsv`,摘要列是五欄、警示來源那一欄有值或寫「無」;兩支腳本不是從 `$JSC_HOME/current` 跑起來時,stderr 會有一行 `[WARN]` 點出實際路徑與應該用的路徑,`$JSC_HOME/assistant/usage-prev.tsv` 換成本輪的累計數,`$JSC_HOME/assistant/patrol.lock` 已經放掉。讓開的那一輪沒有任何寫入跡象。`stop` 之後心跳路徑不存在,`crontab -l` 找不到任何 `# jsc-assist:assistant` 條目。以上都不動別人的排程條目,條目數量前後相同。`status` 無寫入跡象,只有回報內容。四個操作都不動 `tasks/` 底下的檔案,也不動 worktree 與程式碼存取庫。排程的 log 一律在 `$JSC_HOME/assistant/schedule.log`,不落在任何存取庫 |
|
||||||
|
|||||||
+59
-30
@@ -1,20 +1,39 @@
|
|||||||
---
|
---
|
||||||
name: assistant
|
name: assistant
|
||||||
description: Start, inspect, patrol, or stop the background assistant, with jsc-hooks/hooks/heartbeat.sh owning the single freshness verdict, tools/schedule.sh owning the system scheduler, and tools/patrol.sh owning one patrol round. The heartbeat is written by a completed patrol round and by nothing else, so the schedule carries the patrol entry only and its period is derived from the heartbeat TTL; start runs one round and then installs that entry, status turns heartbeat.sh report, schedule.sh status and the task book into one read-only table, stop removes the entry first and then clears the heartbeat. One round reads four independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, and the heartbeat's own report - and appends the result to wiki MONITOR_{HASH} through jsc-gitea:wiki before tools/patrol.sh finish writes the heartbeat. A round that cannot record its result writes no heartbeat, and a round that starts while the previous one still holds the lock stands down. Use when someone starts, patrols or stops the assistant, or asks whether it is running and what is queued; not for environment health checks (jsc-cli:doctor), not for skill usage counts (jsc-log:stats).
|
description: 'Start, inspect, patrol or stop the background assistant: jsc-hooks/hooks/heartbeat.sh owns the freshness verdict, tools/schedule.sh the system scheduler, tools/patrol.sh one round. The heartbeat is written by a completed round and by nothing else, so the schedule carries the patrol entry only, its period from the heartbeat TTL; start runs one round then installs that entry - absolute CLI path, environment snapshot, unattended write confirmation, which cron lacks - status prints heartbeat, schedule and task book read-only, stop removes the entry before clearing the heartbeat. One round reads four independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, and the heartbeat''s own report - then rewrites wiki MONITOR_{HASH} through jsc-gitea:wiki as three fixed blocks: basic data untouched, the latest round replaced whole, a 24-row summary table. A round that cannot record its result writes no heartbeat; one that starts while the previous holds the lock stands down. Use when someone starts, patrols or stops the assistant, or asks whether it runs and what is queued; not for environment health checks (jsc-cli:doctor), not for skill usage counts (jsc-log:stats).'
|
||||||
---
|
---
|
||||||
|
|
||||||
# assistant — start, status, patrol, stop
|
# assistant — start, status, patrol, stop
|
||||||
|
|
||||||
The background assistant runs where nobody is watching it. Its heartbeat is the only evidence that it is alive, so this skill is the single entry point for the four operations that touch that evidence: `patrol` writes it, `status` reads it, `stop` clears it, and `start` bootstraps the whole loop.
|
The background assistant runs where nobody is watching it. Its heartbeat is the only evidence that it is alive, so this skill is the single entry point for the four operations that touch that evidence: `patrol` writes it, `status` reads it, `stop` clears it, and `start` bootstraps the whole loop.
|
||||||
|
|
||||||
`jsc-hooks/hooks/heartbeat.sh` owns every heartbeat operation, including the freshness verdict. Never read, parse, write or delete `$JSC_HOME/assistant/heartbeat` directly — one verdict, one source.
|
`$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh` owns every heartbeat operation, including the freshness verdict. Never read, parse, write or delete `$JSC_HOME/assistant/heartbeat` directly — one verdict, one source.
|
||||||
|
|
||||||
`tools/schedule.sh` owns every system-scheduler operation: installing an entry, removing it, and reading which entries exist. Never call `crontab` or `schtasks` from this skill, and never edit a crontab by hand.
|
`$JSC_HOME/current/jsc-assist/tools/schedule.sh` owns every system-scheduler operation: installing an entry, removing it, and reading which entries exist. Never call `crontab` or `schtasks` from this skill, and never edit a crontab by hand.
|
||||||
|
|
||||||
`tools/patrol.sh` owns one patrol round: taking the round lock, reading the four sources, composing the monitor-page section, and — after that section is on the page — writing the heartbeat. Never re-read a source this skill already handed to that script, and never compose the section by hand; the script prints the file paths.
|
`$JSC_HOME/current/jsc-assist/tools/patrol.sh` owns one patrol round: taking the round lock, reading the four sources, composing the monitor page's blocks, and — after the page carries this round — writing the heartbeat. Never re-read a source this skill already handed to that script, and never compose a block by hand; the script prints the file paths.
|
||||||
|
|
||||||
All three flows have fixed inputs and outputs, so all three live in scripts. The task book is the only thing this skill reads for itself, and that is one directory listing.
|
All three flows have fixed inputs and outputs, so all three live in scripts. The task book is the only thing this skill reads for itself, and that is one directory listing.
|
||||||
|
|
||||||
|
## Tool paths
|
||||||
|
|
||||||
|
Every tool below is addressed through `$JSC_HOME/current/{plugin}`, and `$JSC_HOME` falls back to `~/.jsc` exactly as everywhere else in this skill:
|
||||||
|
|
||||||
|
| What it does | Path to run |
|
||||||
|
| --- | --- |
|
||||||
|
| one patrol round | `$JSC_HOME/current/jsc-assist/tools/patrol.sh` |
|
||||||
|
| the system scheduler | `$JSC_HOME/current/jsc-assist/tools/schedule.sh` |
|
||||||
|
| the heartbeat | `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh` |
|
||||||
|
| the wiki, through `jsc-gitea:wiki` | `$JSC_HOME/current/jsc-gitea/tools/gitea.sh` |
|
||||||
|
|
||||||
|
**A `Skill(...)` rule permits invoking that skill and nothing more.** Every Bash call inside it is still checked on its own, so `jsc-gitea:wiki` reaching the wiki depends on `gitea.sh` carrying its own rule — without it the round is refused locally, before any request leaves the machine, and the monitor page never gets written.
|
||||||
|
|
||||||
|
**Never build a tool path out of the base directory the CLI hands you in the skill prompt.** That directory points into the plugin cache and carries a version segment, and the permission gate allows exactly the four paths above and nothing else. A cache path is therefore refused silently: the round stops on a permission prompt nobody can answer, records nothing, writes no heartbeat, and the refusal looks exactly like a broken tool. Read the paths off this table every time — not off the prompt, not off a previous transcript, not off `crontab -l`.
|
||||||
|
|
||||||
|
Both scripts check this for themselves: run from anywhere outside `$JSC_HOME/current`, they print a `[WARN]` line on stderr naming the path they were started from and the path they should have been started from, and then carry on. That line means this round is on the wrong path — quote it, fix the path, and do not treat the round's success as proof that the path was fine.
|
||||||
|
|
||||||
|
`current` is a set of version-free links that `jsc-cli:deploy` maintains, so an upgrade moves the cache and leaves these paths alone. When one of them is missing, report the missing link and say `jsc-cli:deploy` has to run; never fall back to a cache path to get the round through, and never create the link here.
|
||||||
|
|
||||||
## Pick the operation
|
## Pick the operation
|
||||||
|
|
||||||
Run exactly one operation per invocation. Take it from the request: starting, launching or waking the assistant is `start`; asking whether it runs, what it is doing, or what is queued is `status`; running one round, patrolling, or a scheduled wake-up is `patrol`; stopping, halting or shutting it down is `stop`. When the request names none of the four, or names more than one, ask through the `jsc-ask:ask` decision tree with those four as the options, each stating its effect — `start` runs one round and installs the scheduled entry that keeps running rounds, `status` changes nothing, `patrol` runs one round and writes one heartbeat, `stop` removes that entry and deletes the heartbeat. **The one exception: a `patrol` invocation never asks anything at all** (see 界線 1 below). Never guess, and never run a second operation the caller did not ask for. Completion condition: exactly one of `start`, `status`, `patrol`, `stop` is chosen and named in the report.
|
Run exactly one operation per invocation. Take it from the request: starting, launching or waking the assistant is `start`; asking whether it runs, what it is doing, or what is queued is `status`; running one round, patrolling, or a scheduled wake-up is `patrol`; stopping, halting or shutting it down is `stop`. When the request names none of the four, or names more than one, ask through the `jsc-ask:ask` decision tree with those four as the options, each stating its effect — `start` runs one round and installs the scheduled entry that keeps running rounds, `status` changes nothing, `patrol` runs one round and writes one heartbeat, `stop` removes that entry and deletes the heartbeat. **The one exception: a `patrol` invocation never asks anything at all** (see 界線 1 below). Never guess, and never run a second operation the caller did not ask for. Completion condition: exactly one of `start`, `status`, `patrol`, `stop` is chosen and named in the report.
|
||||||
@@ -27,7 +46,7 @@ Run exactly one operation per invocation. Take it from the request: starting, la
|
|||||||
| `$JSC_HOME/assistant/schedule.log` | nobody here — the scheduled entry appends to it | free text; point the operator at it when a scheduled round misbehaves |
|
| `$JSC_HOME/assistant/schedule.log` | nobody here — the scheduled entry appends to it | free text; point the operator at it when a scheduled round misbehaves |
|
||||||
| `$JSC_HOME/assistant/tasks/{id}` | this skill, read-only | `key=value` lines, one task per file: `id`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`) |
|
| `$JSC_HOME/assistant/tasks/{id}` | this skill, read-only | `key=value` lines, one task per file: `id`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`) |
|
||||||
| `$JSC_HOME/assistant/patrol.lock/` | `patrol.sh` only | the round lock, a directory. `info` holds `round`, `pid`, `started` |
|
| `$JSC_HOME/assistant/patrol.lock/` | `patrol.sh` only | the round lock, a directory. `info` holds `round`, `pid`, `started` |
|
||||||
| `$JSC_HOME/assistant/patrol/` | `patrol.sh` only | one round's scratch files, including `section.md`, `newpage.md` and `contents.tsv` |
|
| `$JSC_HOME/assistant/patrol/` | `patrol.sh` only | one round's scratch files, including `latest.md`, `summary.md`, `summary-row.md`, `newpage.md` and `contents.tsv` |
|
||||||
| `$JSC_HOME/assistant/usage-prev.tsv` | `patrol.sh` only | last recorded round's cumulative usage counts, so the next round can print a real per-round delta |
|
| `$JSC_HOME/assistant/usage-prev.tsv` | `patrol.sh` only | last recorded round's cumulative usage counts, so the next round can print a real per-round delta |
|
||||||
|
|
||||||
`$JSC_HOME` defaults to `~/.jsc`. `heartbeat.sh report` prints the resolved heartbeat path in its `file=` field, so take the assistant directory from there rather than rebuilding it.
|
`$JSC_HOME` defaults to `~/.jsc`. `heartbeat.sh report` prints the resolved heartbeat path in its `file=` field, so take the assistant directory from there rather than rebuilding it.
|
||||||
@@ -50,7 +69,7 @@ Every call in every operation below is judged by this table. Report the code you
|
|||||||
|
|
||||||
## The scheduler
|
## The scheduler
|
||||||
|
|
||||||
Nothing in a background assistant runs on its own. The system scheduler is what makes it periodic, and `tools/schedule.sh` is the only thing here that touches it. One job exists, written as exactly one entry carrying the fixed marker `# jsc-assist:assistant patrol`:
|
Nothing in a background assistant runs on its own. The system scheduler is what makes it periodic, and `$JSC_HOME/current/jsc-assist/tools/schedule.sh` is the only thing here that touches it. One job exists, written as exactly one entry carrying the fixed marker `# jsc-assist:assistant patrol`:
|
||||||
|
|
||||||
| Job | Period | Runs | Installed by `start` |
|
| Job | Period | Runs | Installed by `start` |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@@ -69,10 +88,12 @@ Four properties of that script matter enough to state here, because a report tha
|
|||||||
- **A written entry is not a running entry.** WSL does not start cron by default, and this is the machine's most likely state. Exit 1 from `install` means the entry is on disk and will never fire. Report that as a failure of the start, name `sudo service cron start`, and say it has to be run again after every WSL restart. Never soften exit 1 into "scheduling is set up".
|
- **A written entry is not a running entry.** WSL does not start cron by default, and this is the machine's most likely state. Exit 1 from `install` means the entry is on disk and will never fire. Report that as a failure of the start, name `sudo service cron start`, and say it has to be run again after every WSL restart. Never soften exit 1 into "scheduling is set up".
|
||||||
- **The log lives at `$JSC_HOME/assistant/schedule.log`**, deliberately outside every repository. Do not offer to move it into a project.
|
- **The log lives at `$JSC_HOME/assistant/schedule.log`**, deliberately outside every repository. Do not offer to move it into a project.
|
||||||
- **The entry runs with no human present.** The command is installed with `</dev/null`, so nothing it runs can block on input. A patrol round that stops to ask for a tool permission hangs that round, and the lock it holds stands the next round down until the lock ages out — which is why `patrol` asks nothing, of anybody, ever.
|
- **The entry runs with no human present.** The command is installed with `</dev/null`, so nothing it runs can block on input. A patrol round that stops to ask for a tool permission hangs that round, and the lock it holds stands the next round down until the lock ages out — which is why `patrol` asks nothing, of anybody, ever.
|
||||||
|
- **The entry carries its own environment.** cron gives it a short `PATH`, no settings file and no tty, so `schedule.sh` writes three things into the entry: the CLI resolved to an absolute path with `command -v`, a snapshot of the wiki variables taken at install time (`GITEA_HOST`, `GITEA_TOKEN`, `JSC_HOME`, `JSC_ASSISTANT_HEARTBEAT_TTL` and every set `JSC_WIKI_REPO*`), and `JSC_GITEA_CONFIRM=yes`, because the write confirmation only recognises a tty and an unattended round has nobody to confirm. Two consequences belong in every report: the entry holds a copy of the token, so the crontab file has to stay readable by its owner alone, and a changed variable only reaches the entry after another `install`. `install` prints the snapshotted names in `env_snapshot=` and masks the token in every entry it prints — never print an entry read from `crontab -l` yourself.
|
||||||
|
- **`install` prints the permission rules that round needs.** One `allow_rule=` line each, with `*` in the path's version segment. Hand them to the operator verbatim: an unattended round that hits a permission prompt hangs until the lock ages out, and nobody is there to approve it. `Write(...)` rules do nothing for file writes — only `Edit(...)` is recognised — so never turn a printed `Edit` rule into a `Write` one.
|
||||||
|
|
||||||
### What a fresh heartbeat actually proves
|
### What a fresh heartbeat actually proves
|
||||||
|
|
||||||
The heartbeat is written in exactly one place: `tools/patrol.sh finish`, and `finish` is called only after that round's result is on the monitor page. So the verdict 新鮮 now proves one thing that is worth proving — **the last patrol round ran to the end and its result was recorded** — and it still does not prove three others:
|
The heartbeat is written in exactly one place: `$JSC_HOME/current/jsc-assist/tools/patrol.sh finish`, and `finish` is called only after that round's result is on the monitor page. So the verdict 新鮮 now proves one thing that is worth proving — **the last patrol round ran to the end and its result was recorded** — and it still does not prove three others:
|
||||||
|
|
||||||
- **Not that the round was clean.** Four sources are read independently and a round with three failures still records and still beats. The health of a round is `本輪判定` on the monitor page, never the heartbeat.
|
- **Not that the round was clean.** Four sources are read independently and a round with three failures still records and still beats. The health of a round is `本輪判定` on the monitor page, never the heartbeat.
|
||||||
- **Not that any task in the book moved.** The task rows — `last_run`, `next_run`, `fail_count` — are the only evidence about work.
|
- **Not that any task in the book moved.** The task rows — `last_run`, `next_run`, `fail_count` — are the only evidence about work.
|
||||||
@@ -86,21 +107,21 @@ The failure this design buys is the one worth having: a round that cannot read i
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 0 | `install` wrote the entry and read it back, the scheduler service is running; `remove` finished, or there was nothing to remove; `status` printed its lines | Carry on. For `status`, the state still has to be read out of the `installed=` fields |
|
| 0 | `install` wrote the entry and read it back, the scheduler service is running; `remove` finished, or there was nothing to remove; `status` printed its lines | Carry on. For `status`, the state still has to be read out of the `installed=` fields |
|
||||||
| 1 | `install` wrote the entry, but the cron service is not running — the entry will never fire | The start did not succeed. Report the entry as installed and inert, quote the fix (`sudo service cron start`, and again after each WSL restart), and never claim the assistant will keep itself alive |
|
| 1 | `install` wrote the entry, but the cron service is not running — the entry will never fire | The start did not succeed. Report the entry as installed and inert, quote the fix (`sudo service cron start`, and again after each WSL restart), and never claim the assistant will keep itself alive |
|
||||||
| 2 | `jsc-hooks/hooks/heartbeat.sh` was not found, so the TTL cannot be read and the period cannot be derived | Report that `jsc-hooks` is missing or too old (0.3.7 or newer is required) and stop the operation |
|
| 2 | `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh` was not found, so the TTL cannot be read and the period cannot be derived | Report that `jsc-hooks` is missing or too old (0.3.7 or newer is required) and stop the operation |
|
||||||
| 3 | No usable scheduler on this machine | Report the platform and that neither `crontab` nor `schtasks` was found, and stop. Never fall back to some other mechanism |
|
| 3 | No usable scheduler on this machine | Report the platform and that neither `crontab` nor `schtasks` was found, and stop. Never fall back to some other mechanism |
|
||||||
| 4 | The scheduler operation failed — the existing schedule could not be read for a reason other than "no crontab", or the write or delete returned non-zero | Report the stderr text verbatim. A read failure means nothing was written, so the user's other entries are untouched; say so |
|
| 4 | The scheduler operation failed — the existing schedule could not be read for a reason other than "no crontab", or the write or delete returned non-zero | Report the stderr text verbatim. A read failure means nothing was written, so the user's other entries are untouched; say so |
|
||||||
| 5 | Read-back verification failed — the entry is missing after a successful write, is present twice, is still there after a delete, or somebody else's line count changed | Serious. Report it loudly with the printed numbers, and tell the operator to inspect `crontab -l` by hand before anything else is run |
|
| 5 | Read-back verification failed — the entry is missing after a successful write, is present twice, is still there after a delete, or somebody else's line count changed | Serious. Report it loudly with the printed numbers, and tell the operator to inspect `crontab -l` by hand before anything else is run |
|
||||||
| 6 | Usage error — an unknown subcommand or job name, a missing option value, `install heartbeat`, a `--period` that does not fit the TTL, or the patrol CLI could not be determined | A defect in the call, not a state of the machine. Correct the command line and run it once more; report a second exit 6 as a defect in this skill and stop |
|
| 6 | Usage error — an unknown subcommand or job name, a missing option value, `install heartbeat`, a `--period` that does not fit the TTL, the patrol CLI could not be determined, or that CLI's executable is not on `PATH` so no absolute path can be written | A defect in the call or a CLI that is not installed, not a state of the machine. The stderr line names which one it is; quote it, correct the command line, and run it once more. Report a second exit 6 as a defect in this skill and stop |
|
||||||
|
|
||||||
## patrol.sh exit codes
|
## patrol.sh exit codes
|
||||||
|
|
||||||
One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3 are results, not aborts.** A round with failed items still has a section to write, and refusing to write it would hide the failure instead of recording it.
|
One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3 are results, not aborts.** A round with failed items still has a result to record, and refusing to record it would hide the failure instead of showing it.
|
||||||
|
|
||||||
| Code | Meaning | What to do |
|
| Code | Meaning | What to do |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 0 | `collect`: all four items read to the end, empty sources included. `finish`: heartbeat written, snapshot promoted, lock released. `abort`: lock released | Carry on with the operation's next step |
|
| 0 | `collect`: all four items read to the end, empty sources included. `finish`: heartbeat written, snapshot promoted, lock released. `abort`: lock released | Carry on with the operation's next step |
|
||||||
| 1 | `collect`: partial success — at least one item failed and at least one produced a result | **Write the page anyway.** The section already marks the failed items and the round verdict is 警示. Name the failed items and their `note=` text in the report |
|
| 1 | `collect`: partial success — at least one item failed and at least one produced a result | **Write the page anyway.** The latest-round block already marks the failed items and the round verdict is 警示. Name the failed items and their `note=` text in the report |
|
||||||
| 2 | `finish`: `jsc-hooks/hooks/heartbeat.sh` was not found | The round completed and is recorded, but no heartbeat exists to prove it. Report the round as recorded and the heartbeat as not written, say `jsc-hooks` 0.3.7 or newer has to be installed, and run `tools/patrol.sh abort --round {id}` to release the lock |
|
| 2 | `finish`: `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh` was not found | The round completed and is recorded, but no heartbeat exists to prove it. Report the round as recorded and the heartbeat as not written, say `jsc-hooks` 0.3.7 or newer has to be installed, and run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {id}` to release the lock |
|
||||||
| 3 | `collect`: all four items failed | **Write the page anyway**, with verdict 異常. A page listing four failures is the signal; a missing page is not. Then carry on to `finish` as usual — the round did complete |
|
| 3 | `collect`: all four items failed | **Write the page anyway**, with verdict 異常. A page listing four failures is the signal; a missing page is not. Then carry on to `finish` as usual — the round did complete |
|
||||||
| 4 | Another round holds the lock (`collect`), or the lock is no longer this round's (`finish`, `abort`) | Not a failure. On `collect`: report 本輪讓開 and name the holder and its age from the printed `lock=busy` line, then write nothing and stop. On `finish`: the previous round overran and was taken over, so this round's result does not count — report it, write no heartbeat, and stop |
|
| 4 | Another round holds the lock (`collect`), or the lock is no longer this round's (`finish`, `abort`) | Not a failure. On `collect`: report 本輪讓開 and name the holder and its age from the printed `lock=busy` line, then write nothing and stop. On `finish`: the previous round overran and was taken over, so this round's result does not count — report it, write no heartbeat, and stop |
|
||||||
| 5 | Filesystem failure — the lock could not be created or released, a scratch file could not be written, the snapshot could not be promoted, or `heartbeat.sh write` returned non-zero | Serious. Report it loudly with the stderr text and the path. On a `finish` failure the round is recorded but unproven: say so plainly and never claim the round beat |
|
| 5 | Filesystem failure — the lock could not be created or released, a scratch file could not be written, the snapshot could not be promoted, or `heartbeat.sh write` returned non-zero | Serious. Report it loudly with the stderr text and the path. On a `finish` failure the round is recorded but unproven: say so plainly and never claim the round beat |
|
||||||
@@ -112,7 +133,7 @@ The six limits in `AGENTS.md`「助理的界線」 hold for all four operations.
|
|||||||
|
|
||||||
- **This skill never judges a gate.** It maintains the heartbeat and prints what the heartbeat says. Whether a stale heartbeat blocks a skill call is decided by a hook, synchronously and offline; nothing in this skill blocks or waves through anything. 界線 2.
|
- **This skill never judges a gate.** It maintains the heartbeat and prints what the heartbeat says. Whether a stale heartbeat blocks a skill call is decided by a hook, synchronously and offline; nothing in this skill blocks or waves through anything. 界線 2.
|
||||||
- **A patrol round asks nothing.** It runs from cron with nobody present, so there is no one to answer and a question hangs the round. Every branch in the patrol steps below resolves without a question: a missing source is recorded as missing, an ambiguous result is recorded verbatim, and a round that cannot proceed aborts and reports. Never call `jsc-ask:ask` from `patrol`. 界線 1.
|
- **A patrol round asks nothing.** It runs from cron with nobody present, so there is no one to answer and a question hangs the round. Every branch in the patrol steps below resolves without a question: a missing source is recorded as missing, an ambiguous result is recorded verbatim, and a round that cannot proceed aborts and reports. Never call `jsc-ask:ask` from `patrol`. 界線 1.
|
||||||
- **A patrol round only ever appends to the monitor page.** Read the old page back first, append one section, put the whole page. The contents page gets its own row updated and nobody else's. A page that could not be read is a page that does not get written. 界線 4.
|
- **A patrol round rewrites the monitor page as three fixed blocks.** Read the old page back first; keep 本頁基本資料 as it stands, replace 最新一輪 whole, put this round's row on top of the summary table and cut it to 24; then put the whole page. The contents page gets its own row updated and nobody else's. A page that could not be read is a page that does not get written — the summary table only survives if the old one came back. 界線 4.
|
||||||
- **A patrol round reports; it never acts on what it found.** The 待人處理 rows name an entry point for a human. The patrol does not run that entry point, does not fix a hook, does not update a plugin and does not touch a repository. 界線 3 and 界線 6.
|
- **A patrol round reports; it never acts on what it found.** The 待人處理 rows name an entry point for a human. The patrol does not run that entry point, does not fix a hook, does not update a plugin and does not touch a repository. 界線 3 and 界線 6.
|
||||||
- **`stop` clearing the heartbeat and removing the schedule is not a breach of 界線 5「不刪除狀態檔」.** That limit protects state that records work — the task book, worktrees, wiki pages — from a background process nobody is watching. The heartbeat records one fact only, "the last patrol round finished", and the schedule entry is what keeps rounds running, so a `stop` that leaves either behind leaves a lie behind. Clearing both is the whole job of `stop`, and they are the only deletions any operation here performs, both of them entries this skill installed itself. `stop` touches nothing under `tasks/`, nobody else's cron entry, no worktree and no wiki page. Do not "restore" this limit later by taking either removal out of `stop`.
|
- **`stop` clearing the heartbeat and removing the schedule is not a breach of 界線 5「不刪除狀態檔」.** That limit protects state that records work — the task book, worktrees, wiki pages — from a background process nobody is watching. The heartbeat records one fact only, "the last patrol round finished", and the schedule entry is what keeps rounds running, so a `stop` that leaves either behind leaves a lie behind. Clearing both is the whole job of `stop`, and they are the only deletions any operation here performs, both of them entries this skill installed itself. `stop` touches nothing under `tasks/`, nobody else's cron entry, no worktree and no wiki page. Do not "restore" this limit later by taking either removal out of `stop`.
|
||||||
|
|
||||||
@@ -122,7 +143,7 @@ An assistant that is killed, crashes, or dies with the machine writes no farewel
|
|||||||
|
|
||||||
The round lock is the one thing a crash does leave behind, and it ages out the same way: `patrol.sh collect` breaks a lock older than the heartbeat TTL, takes it, and prints `lock_broken=1` so the takeover lands on the monitor page instead of happening quietly. The overrun round that lost its lock then gets exit 4 from `finish` and writes no heartbeat, which is correct — it never reached the end.
|
The round lock is the one thing a crash does leave behind, and it ages out the same way: `patrol.sh collect` breaks a lock older than the heartbeat TTL, takes it, and prints `lock_broken=1` so the takeover lands on the monitor page instead of happening quietly. The overrun round that lost its lock then gets exit 4 from `finish` and writes no heartbeat, which is correct — it never reached the end.
|
||||||
|
|
||||||
That property holds only while nothing fakes a heartbeat. **`write` is called by `tools/patrol.sh finish` and nowhere else.** `start` does not call it, `status` does not call it, `stop` does not call it, no scheduled entry calls it, and no other skill calls it. A heartbeat written by anything that is not a finished round says a round finished when none did, and the reader has no way to tell the difference. This is also why `stop` removes the scheduled entry before clearing the heartbeat, and never in the other order.
|
That property holds only while nothing fakes a heartbeat. **`write` is called by `$JSC_HOME/current/jsc-assist/tools/patrol.sh finish` and nowhere else.** `start` does not call it, `status` does not call it, `stop` does not call it, no scheduled entry calls it, and no other skill calls it. A heartbeat written by anything that is not a finished round says a round finished when none did, and the reader has no way to tell the difference. This is also why `stop` removes the scheduled entry before clearing the heartbeat, and never in the other order.
|
||||||
|
|
||||||
## start
|
## start
|
||||||
|
|
||||||
@@ -130,11 +151,11 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
|
|||||||
|
|
||||||
1. **Run one patrol round.** Follow every step of the `patrol` operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (`collect` exit 4, 5 or 6, an empty `hash=`, a failed wiki write, or `finish` exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 2, and do not claim a started assistant. A round that completed with failed items (`collect` exit 1 or 3) is still a completed round — carry on to step 2 and name the failures in the closing report. Completion condition: `patrol.sh finish` exited 0, or the failure report naming the step and the code has been printed and no start was claimed.
|
1. **Run one patrol round.** Follow every step of the `patrol` operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (`collect` exit 4, 5 or 6, an empty `hash=`, a failed wiki write, or `finish` exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 2, and do not claim a started assistant. A round that completed with failed items (`collect` exit 1 or 3) is still a completed round — carry on to step 2 and name the failures in the closing report. Completion condition: `patrol.sh finish` exited 0, or the failure report naming the step and the code has been printed and no start was claimed.
|
||||||
|
|
||||||
2. **Confirm the heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh report` and read its `state=`, `ts=`, `ttl=`, `pid=`, `cli=`, `session=` and `file=` fields. `state=fresh` is the expected result. Any other state right after a successful round means something rewrote or removed the file in between: report the state, the path and that the heartbeat did not survive its own write, and do not claim a started assistant. Completion condition: the report line was read and either `state=fresh` was recorded with its seven fields, or the mismatch was reported.
|
2. **Confirm the heartbeat.** Run `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh report` and read its `state=`, `ts=`, `ttl=`, `pid=`, `cli=`, `session=` and `file=` fields. `state=fresh` is the expected result. Any other state right after a successful round means something rewrote or removed the file in between: report the state, the path and that the heartbeat did not survive its own write, and do not claim a started assistant. Completion condition: the report line was read and either `state=fresh` was recorded with its seven fields, or the mismatch was reported.
|
||||||
|
|
||||||
3. **Install the patrol entry.** Run `tools/schedule.sh install patrol`. Judge the result by the schedule.sh exit-code table, and keep the printed `entry=`, `ttl=`, `period=`, `legacy_removed=`, `others_kept=` and `service=` fields for the report. Exit 1 is the case to get right: the entry is installed and inert, so step 4 reports a started assistant whose heartbeat will expire, not a scheduled one. On 2, 3, 4, 5 or 6 nothing is scheduled — report the code, say the round ran but no further round will, and do not claim the assistant will stay alive. Completion condition: the exit code is recorded, and on exit 0 the printed entry line, the TTL, the period, the legacy count and the surviving-entry count are recorded with it.
|
3. **Install the patrol entry.** Run `$JSC_HOME/current/jsc-assist/tools/schedule.sh install patrol`. Judge the result by the schedule.sh exit-code table, and keep the printed `entry=`, `ttl=`, `period=`, `legacy_removed=`, `others_kept=`, `env_snapshot=`, every `allow_rule=` line and `service=` for the report. Exit 1 is the case to get right: the entry is installed and inert, so step 4 reports a started assistant whose heartbeat will expire, not a scheduled one. Exit 6 with a CLI executable that is not on `PATH` is the second one: nothing was installed, and the fix is to install that CLI or to pass `--patrol-cmd`, not to write a bare command name into the entry. On 2, 3, 4, 5 or 6 nothing is scheduled — report the code, say the round ran but no further round will, and do not claim the assistant will stay alive. Completion condition: the exit code is recorded, and on exit 0 the entry line, the TTL, the period, the legacy count, the surviving-entry count, the snapshotted variable names and the allow rules are recorded with it.
|
||||||
|
|
||||||
4. **Report the start.** Print the round's verdict and its four item results, the monitor page that was written, the heartbeat path, the local time of `ts`, the TTL in seconds, `pid`, `cli` and `session` as hints, then the scheduler mechanism, the derived period, the installed entry line, how many legacy heartbeat entries were removed, and how many other entries were left untouched. Close with the notice that matches step 3's outcome, printed literally with `{ttl}` replaced by the TTL just read and `{period}` by the derived period:
|
4. **Report the start.** Print the round's verdict and its four item results, the monitor page that was written, the heartbeat path, the local time of `ts`, the TTL in seconds, `pid`, `cli` and `session` as hints, then the scheduler mechanism, the derived period, the installed entry line as the script printed it with the token already masked, how many legacy heartbeat entries were removed, and how many other entries were left untouched. Then hand over the two operator items the install printed: the `allow_rule=` lines verbatim, so the unattended round never meets a permission prompt, and the reminder that the entry holds a snapshot of the listed variables including the token — keep the crontab file readable by its owner alone, and run `install` again after any of those variables changes. Close with the notice that matches step 3's outcome, printed literally with `{ttl}` replaced by the TTL just read and `{period}` by the derived period:
|
||||||
|
|
||||||
| Step 3 | Notice |
|
| Step 3 | Notice |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -142,29 +163,37 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
|
|||||||
| exit 1 | 助理已啟動,第一輪巡檢跑完了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 `sudo service cron start`,重開 WSL 之後要再跑一次。 |
|
| exit 1 | 助理已啟動,第一輪巡檢跑完了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 `sudo service cron start`,重開 WSL 之後要再跑一次。 |
|
||||||
| 其他結束碼 | 助理已啟動,第一輪巡檢跑完了,但排程沒接上(結束碼 {code})。不會再有下一輪,心跳過了 {ttl} 秒就會過期,屆時請再跑一次 start。 |
|
| 其他結束碼 | 助理已啟動,第一輪巡檢跑完了,但排程沒接上(結束碼 {code})。不會再有下一輪,心跳過了 {ttl} 秒就會過期,屆時請再跑一次 start。 |
|
||||||
|
|
||||||
Completion condition: the report carries the round verdict, the monitor page name, the path, the local heartbeat time, the TTL, the period, the three hint fields and the scheduler outcome, and exactly one notice above appears with the real numbers.
|
Completion condition: the report carries the round verdict, the monitor page name, the path, the local heartbeat time, the TTL, the period, the three hint fields, the scheduler outcome, the allow rules and the snapshot reminder, and exactly one notice above appears with the real numbers.
|
||||||
|
|
||||||
## patrol
|
## patrol
|
||||||
|
|
||||||
One round: read four sources, record the result, then beat. Everything before the heartbeat is read-only except the round's own scratch files. Ask nobody anything.
|
One round: read four sources, record the result, then beat. Everything before the heartbeat is read-only except the round's own scratch files. Ask nobody anything.
|
||||||
|
|
||||||
1. **Collect.** Run `tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). Judge the exit code by the patrol.sh table. Exit 4 stands the round down — report the holder and its age from the printed `lock=busy` line, and stop; write no page and no heartbeat. Exit 5 and 6 stop the round the same way, with the code and the stderr text. Exit 0, 1 and 3 all carry on to step 2. Record `round=`, `lock_broken=`, `hash=`, `page=`, `verdict=`, `failed_sources=`, every `item=` line, and the three file paths `section_file=`, `newpage_file=` and `contents_file=`. Completion condition: the round id, the page name and the three file paths are recorded, or the stand-down or the failure was reported and the round stopped.
|
1. **Collect.** Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). Judge the exit code by the patrol.sh table. Exit 4 stands the round down — report the holder and its age from the printed `lock=busy` line, and stop; write no page and no heartbeat. Exit 5 and 6 stop the round the same way, with the code and the stderr text. Exit 0, 1 and 3 all carry on to step 2. Record `round=`, `lock_broken=`, `hash=`, `page=`, `verdict=`, `failed_sources=`, `warn_sources=`, `pending=`, every `item=` line, and the file paths `latest_file=`, `summary_file=`, `summary_row_file=`, `newpage_file=` and `contents_file=`. Completion condition: the round id, the page name and the five file paths are recorded, or the stand-down or the failure was reported and the round stopped.
|
||||||
|
|
||||||
2. **Check the page name.** An empty `hash=` means `jsc-gitea/tools/hash-id` could not be found or could not run, so there is no page to write to and nothing can be recorded. Run `tools/patrol.sh abort --round {round}`, report that the round found its results but has nowhere to put them, name `jsc-gitea` as missing, and stop. Never invent a page name — a hand-made name lands the content on a page nobody reads. Completion condition: `page=` holds a `MONITOR_{HASH}` name, or the abort ran and the round was reported as unrecorded.
|
2. **Check the page name.** An empty `hash=` means `jsc-gitea/tools/hash-id` could not be found or could not run, so there is no page to write to and nothing can be recorded. Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report that the round found its results but has nowhere to put them, name `jsc-gitea` as missing, and stop. Never invent a page name — a hand-made name lands the content on a page nobody reads. Completion condition: `page=` holds a `MONITOR_{HASH}` name, or the abort ran and the round was reported as unrecorded.
|
||||||
|
|
||||||
3. **Append the section to `MONITOR_{HASH}`.** Hand it to `jsc-gitea:wiki` with page type `MONITOR`: read the page back first, then append the whole content of `section_file` as a new last section and put the whole page. Only exit 4 from the read permits creating the page instead, and then the page body is the whole content of `newpage_file`, which already carries the basic-data section plus this round's section. Exit 7 and exit 8 mean the old content is unknown: create nothing, write nothing. On any write failure — including exit 3 with no wiki repo configured for `MONITOR`, which the patrol cannot ask about — run `tools/patrol.sh abort --round {round}`, report the code, and stop. **No record, no heartbeat.** Completion condition: the append or the create returned success, or the abort ran and the round was reported as unrecorded with its exit code.
|
3. **Rebuild `MONITOR_{HASH}` from its three blocks.** Hand it to `jsc-gitea:wiki` with page type `MONITOR`: read the page back first, then build the new body out of what came back plus this round's files, in this order and with nothing else on the page:
|
||||||
|
|
||||||
4. **Update this machine's row in `MONITOR_CONTENTS`.** Take the `row=` line from `contents_file` — it is already the finished table row. Hand it to `jsc-gitea:wiki`: read the whole page, match the row whose 主機 and 帳號 columns both equal this round's `host=` and `user=`, overwrite that row's remaining columns, and put the whole page back. No matching row means append one. Never overwrite the whole page, and never touch another machine's row — the write semantics here are the opposite of the content page's, and mixing them up deletes other machines' records. On failure, run `tools/patrol.sh abort --round {round}`, report the code, and stop. Completion condition: exactly one row carries this machine's 主機 and 帳號 values, every other row is byte-identical to what was read, and the put returned success.
|
| Block | Where it comes from |
|
||||||
|
| --- | --- |
|
||||||
|
| 本頁基本資料 | the old page, byte for byte from its heading to the line before 最新一輪. Never rewritten, never re-derived |
|
||||||
|
| 最新一輪 | the whole content of `latest_file`, replacing the old block entirely |
|
||||||
|
| 近 24 輪摘要 | `summary_file`, which already holds the heading, the five-column table header (`巡檢時間`、`本輪判定`、`四項成敗`、`待人處理`、`警示來源`) and this round's row; then the old table's data rows in their old order underneath, cut so the table holds at most 24 rows |
|
||||||
|
|
||||||
5. **Write the heartbeat.** Run `tools/patrol.sh finish --round {round}`. This is the last step for a reason: it is the only thing that turns a fresh heartbeat into a true statement. Judge the exit code by the patrol.sh table — 2, 4 and 5 all mean the round is recorded but unproven, and each has its own report line there. Completion condition: `finish` exited 0, or the failure was reported as "recorded but no heartbeat" with its code.
|
Put the whole page. An old-format page — per-round sections stacked up, no summary table — has no rows to carry over: keep its `本頁基本資料` block, drop the stacked sections, let the table start with this round's row, and say in the report that the page was converted. Only exit 4 from the read permits creating the page instead, and then the body is the whole content of `newpage_file`, which already carries all three blocks. Exit 7 and exit 8 mean the old content is unknown: create nothing, write nothing — rebuilding a page from an unknown original throws the summary table away. On any write failure — including exit 3 with no wiki repo configured for `MONITOR`, which the patrol cannot ask about — run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. **No record, no heartbeat.** Completion condition: the put or the create returned success and the page holds exactly three blocks with the summary table at 24 rows or fewer and this round's row on top, or the abort ran and the round was reported as unrecorded with its exit code.
|
||||||
|
|
||||||
6. **Report the round.** Print the round verdict, one line per item with its `status=` and, for a failure, its `note=`; the monitor page name and the contents row that was written; whether the heartbeat was written; and, when `lock_broken=1`, that the previous round's lock was taken over because it had aged past the TTL. Close with the 待人處理 rows from the section, verbatim, and nothing else — the patrol names an entry point and stops there. Completion condition: all four items appear in the report, the heartbeat outcome is stated as written or not written, and no suggestion in 待人處理 was acted on.
|
4. **Update this machine's row in `MONITOR_CONTENTS`.** Take the `row=` line from `contents_file` — it is already the finished table row. Hand it to `jsc-gitea:wiki`: read the whole page, match the row whose 主機 and 帳號 columns both equal this round's `host=` and `user=`, overwrite that row's remaining columns, and put the whole page back. No matching row means append one. Never rebuild this page the way the content page is rebuilt, and never touch another machine's row — every other row here belongs to a machine that is not this one, and one careless whole-page write deletes their records. On failure, run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. Completion condition: exactly one row carries this machine's 主機 and 帳號 values, every other row is byte-identical to what was read, and the put returned success.
|
||||||
|
|
||||||
|
5. **Write the heartbeat.** Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh finish --round {round}`. This is the last step for a reason: it is the only thing that turns a fresh heartbeat into a true statement. Judge the exit code by the patrol.sh table — 2, 4 and 5 all mean the round is recorded but unproven, and each has its own report line there. Completion condition: `finish` exited 0, or the failure was reported as "recorded but no heartbeat" with its code.
|
||||||
|
|
||||||
|
6. **Report the round.** Print the round verdict and, when it is `警示`, the `warn_sources=` text that says why — a round can read all four sources and still come out `警示`, and that column is the only place the reason appears; then one line per item with its `status=` and, for a failure, its `note=`; the monitor page name and the contents row that was written; whether the heartbeat was written; and, when `lock_broken=1`, that the previous round's lock was taken over because it had aged past the TTL. Close with the 待人處理 rows from the latest-round block, verbatim, and nothing else — the patrol names an entry point and stops there. Completion condition: all four items appear in the report, the heartbeat outcome is stated as written or not written, and no suggestion in 待人處理 was acted on.
|
||||||
|
|
||||||
## status
|
## status
|
||||||
|
|
||||||
Read-only throughout. This operation creates, modifies and deletes nothing under `$JSC_HOME`, and it never calls `write` or `clear`.
|
Read-only throughout. This operation creates, modifies and deletes nothing under `$JSC_HOME`, and it never calls `write` or `clear`.
|
||||||
|
|
||||||
1. **Read the heartbeat through the script.** Run `jsc-hooks/hooks/heartbeat.sh report` and split the line on spaces, taking `file=` last so a path containing spaces stays intact. Map `state=` to the verdict: `fresh` → `新鮮`, `stale` → `過期`, `invalid` → `心跳檔損壞`, `absent` → `不存在`. Print `助理未運行` for `stale`, `invalid` and `absent`. Never re-derive the verdict from `ts` yourself, and never treat `invalid` as fresh. On exit 2 or 6, follow that code's row, record the heartbeat state as unknown, and carry on to step 2 — the task book is still worth printing. Completion condition: the heartbeat state holds one of `新鮮`, `過期`, `心跳檔損壞`, `不存在` or unknown, and `ts`, `age`, `ttl`, `pid`, `cli`, `session` and `file` are recorded as read or as empty.
|
1. **Read the heartbeat through the script.** Run `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh report` and split the line on spaces, taking `file=` last so a path containing spaces stays intact. Map `state=` to the verdict: `fresh` → `新鮮`, `stale` → `過期`, `invalid` → `心跳檔損壞`, `absent` → `不存在`. Print `助理未運行` for `stale`, `invalid` and `absent`. Never re-derive the verdict from `ts` yourself, and never treat `invalid` as fresh. On exit 2 or 6, follow that code's row, record the heartbeat state as unknown, and carry on to step 2 — the task book is still worth printing. Completion condition: the heartbeat state holds one of `新鮮`, `過期`, `心跳檔損壞`, `不存在` or unknown, and `ts`, `age`, `ttl`, `pid`, `cli`, `session` and `file` are recorded as read or as empty.
|
||||||
|
|
||||||
2. **Read the task book.** Take the assistant directory from the `file=` path of step 1, list the regular files directly under its `tasks/` subdirectory, and parse each one as `key=value` lines. Branch on the outcome.
|
2. **Read the task book.** Take the assistant directory from the `file=` path of step 1, list the regular files directly under its `tasks/` subdirectory, and parse each one as `key=value` lines. Branch on the outcome.
|
||||||
|
|
||||||
@@ -177,7 +206,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
|||||||
|
|
||||||
Completion condition: every file under `tasks/` produced exactly one row, or zero entries was reported.
|
Completion condition: every file under `tasks/` produced exactly one row, or zero entries was reported.
|
||||||
|
|
||||||
3. **Read the schedule.** Run `tools/schedule.sh status`. It writes nothing. Record `mechanism=`, `service=`, `ttl=`, `period=` and the `installed=` value of both jobs. A `heartbeat` job reported as installed is a leftover from an older version: say so, and say `start` or `schedule.sh install patrol` removes it. On exit 2, 3 or 6 nothing was read: record the schedule state as unknown with its code and carry on — the heartbeat and the task book still print. Completion condition: both jobs have an installed state, or the schedule state is recorded as unknown with its code.
|
3. **Read the schedule.** Run `$JSC_HOME/current/jsc-assist/tools/schedule.sh status`. It writes nothing. Record `mechanism=`, `service=`, `ttl=`, `period=` and the `installed=` value of both jobs. A `heartbeat` job reported as installed is a leftover from an older version: say so, and say `start` or `schedule.sh install patrol` removes it. On exit 2, 3 or 6 nothing was read: record the schedule state as unknown with its code and carry on — the heartbeat and the task book still print. Completion condition: both jobs have an installed state, or the schedule state is recorded as unknown with its code.
|
||||||
|
|
||||||
4. **Print the status table.** Lead with the heartbeat block — verdict, last heartbeat time rendered from `ts` in local time, age in seconds, TTL, `cli`, `session`, `pid`, and the task count. Follow it with the schedule block — mechanism, service state, derived period, and one line per job saying installed or not. Then one row per task carrying `state`, `title`, `next_run` and `fail_count`, in the order the files were listed. Completion condition: the heartbeat block holds all eight values, the schedule block holds both jobs and the period, and the row count equals the task count from step 2.
|
4. **Print the status table.** Lead with the heartbeat block — verdict, last heartbeat time rendered from `ts` in local time, age in seconds, TTL, `cli`, `session`, `pid`, and the task count. Follow it with the schedule block — mechanism, service state, derived period, and one line per job saying installed or not. Then one row per task carrying `state`, `title`, `next_run` and `fail_count`, in the order the files were listed. Completion condition: the heartbeat block holds all eight values, the schedule block holds both jobs and the period, and the row count equals the task count from step 2.
|
||||||
|
|
||||||
@@ -187,7 +216,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 新鮮 | patrol installed, service running | 上一輪巡檢跑完了,結果也記上監控頁了,排程還在跑。那一輪四項有沒有全過,要看監控頁的本輪判定 |
|
| 新鮮 | patrol installed, service running | 上一輪巡檢跑完了,結果也記上監控頁了,排程還在跑。那一輪四項有沒有全過,要看監控頁的本輪判定 |
|
||||||
| 新鮮 | not installed, or service stopped | 上一輪巡檢跑完了,但沒有排程在叫下一輪,過了 TTL 心跳就會過期 |
|
| 新鮮 | not installed, or service stopped | 上一輪巡檢跑完了,但沒有排程在叫下一輪,過了 TTL 心跳就會過期 |
|
||||||
| 過期 or 不存在 | patrol installed, service running | 排程裝著卻沒有新的心跳,巡檢自己跑失敗了,去看 `$JSC_HOME/assistant/schedule.log` 與監控頁最新一節 |
|
| 過期 or 不存在 | patrol installed, service running | 排程裝著卻沒有新的心跳,巡檢自己跑失敗了,去看 `$JSC_HOME/assistant/schedule.log` 與監控頁的最新一輪 |
|
||||||
| any | `heartbeat` job installed | 舊版的心跳排程還留著,它會蓋掉「心跳等於巡檢跑完」這件事。請跑一次 `start`,或 `schedule.sh install patrol` 把它清掉 |
|
| any | `heartbeat` job installed | 舊版的心跳排程還留著,它會蓋掉「心跳等於巡檢跑完」這件事。請跑一次 `start`,或 `schedule.sh install patrol` 把它清掉 |
|
||||||
|
|
||||||
Completion condition: every matching sentence is printed, or none of the four combinations applied.
|
Completion condition: every matching sentence is printed, or none of the four combinations applied.
|
||||||
@@ -198,11 +227,11 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
|||||||
|
|
||||||
## stop
|
## stop
|
||||||
|
|
||||||
1. **Record what is being stopped.** Run `jsc-hooks/hooks/heartbeat.sh report` first and keep its `state=`, `ts=`, `pid=`, `cli=` and `file=` fields for the closing report — after the clear they are gone for good. `state=absent` means no round has finished; say so and still run steps 2 and 3, because a scheduled entry can outlive its heartbeat and `clear` on a missing file is a success, so running both leaves the outcome unambiguous. On exit 2 or 6, follow that code's row, record the previous state as unknown, and carry on to step 2. Completion condition: the previous state and its fields are recorded, or the previous state is recorded as unknown with its code.
|
1. **Record what is being stopped.** Run `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh report` first and keep its `state=`, `ts=`, `pid=`, `cli=` and `file=` fields for the closing report — after the clear they are gone for good. `state=absent` means no round has finished; say so and still run steps 2 and 3, because a scheduled entry can outlive its heartbeat and `clear` on a missing file is a success, so running both leaves the outcome unambiguous. On exit 2 or 6, follow that code's row, record the previous state as unknown, and carry on to step 2. Completion condition: the previous state and its fields are recorded, or the previous state is recorded as unknown with its code.
|
||||||
|
|
||||||
2. **Remove the schedule first.** Run `tools/schedule.sh remove all` — both job names, so the patrol entry and any leftover heartbeat entry from an older install both go. This comes before the clear and never after: clear first and the next scheduled round writes a fresh heartbeat over the stopped assistant, and every reader from then on is told a dead assistant is alive. Judge the result by the schedule.sh exit-code table, and keep `removed=` and `others_kept=` for the report. On any non-zero code the schedule is still installed: report the code, say plainly that rounds will keep running and the assistant therefore cannot be stopped, name the manual fix (`crontab -l` to look, then remove the line carrying `# jsc-assist:assistant` by hand), and skip steps 3 and 4 — clearing a heartbeat that the next round rewrites only hides the problem. Completion condition: `remove` exited 0 with its counts recorded, or the failure report has been printed and no stop was claimed.
|
2. **Remove the schedule first.** Run `$JSC_HOME/current/jsc-assist/tools/schedule.sh remove all` — both job names, so the patrol entry and any leftover heartbeat entry from an older install both go. This comes before the clear and never after: clear first and the next scheduled round writes a fresh heartbeat over the stopped assistant, and every reader from then on is told a dead assistant is alive. Judge the result by the schedule.sh exit-code table, and keep `removed=` and `others_kept=` for the report. On any non-zero code the schedule is still installed: report the code, say plainly that rounds will keep running and the assistant therefore cannot be stopped, name the manual fix (`crontab -l` to look, then remove the line carrying `# jsc-assist:assistant` by hand), and skip steps 3 and 4 — clearing a heartbeat that the next round rewrites only hides the problem. Completion condition: `remove` exited 0 with its counts recorded, or the failure report has been printed and no stop was claimed.
|
||||||
|
|
||||||
3. **Clear the heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh clear`. On exit 5 the file is still there: report the failure with the script's stderr line and the path, say plainly that every reader still sees a heartbeat claiming a round just finished and that the assistant is therefore not reliably stopped, name the manual fix (delete that path by hand, then run `status` to confirm `助理未運行`), and skip step 4 — the closing notice must not be printed after a failed clear. On exit 2 or 6, follow that code's row and stop the same way. Completion condition: `clear` exited 0, or the failure report naming the code, the path and the manual fix has been printed and no stop was claimed.
|
3. **Clear the heartbeat.** Run `$JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh clear`. On exit 5 the file is still there: report the failure with the script's stderr line and the path, say plainly that every reader still sees a heartbeat claiming a round just finished and that the assistant is therefore not reliably stopped, name the manual fix (delete that path by hand, then run `status` to confirm `助理未運行`), and skip step 4 — the closing notice must not be printed after a failed clear. On exit 2 or 6, follow that code's row and stop the same way. Completion condition: `clear` exited 0, or the failure report naming the code, the path and the manual fix has been printed and no stop was claimed.
|
||||||
|
|
||||||
4. **Report the stop and what it means for the gate.** Print the previous state and heartbeat time from step 1 and the entries removed in step 2, then this literally:
|
4. **Report the stop and what it means for the gate.** Print the previous state and heartbeat time from step 1 and the entries removed in step 2, then this literally:
|
||||||
|
|
||||||
|
|||||||
+36
-18
@@ -1,20 +1,23 @@
|
|||||||
# 助理巡檢 — {主機名}/{登入帳號}
|
# 助理巡檢 — {主機名}/{登入帳號}
|
||||||
|
|
||||||
> 由 `jsc-assist` 維護。這是監控頁 `MONITOR_{HASH}`。
|
> 由 `jsc-assist` 維護。這是監控頁 `MONITOR_{HASH}`。
|
||||||
> 這頁是這台機器的巡檢軌跡:一次巡檢附加一節,節標題帶時間戳,舊的節一個字都不動。
|
> 這頁固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。
|
||||||
> 附加是刻意的。助理的寫入是背景行為,覆寫錯了沒人在現場,軌跡被抹掉也看不出斷在哪一輪。
|
> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。
|
||||||
> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,寫入語意與這頁不同,不要混用。
|
> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。
|
||||||
|
> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,別台機器的列一個字都不動。
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A[巡檢一輪] --> B[收攏各類結果]
|
A[巡檢一輪] --> B[收攏四項結果]
|
||||||
B --> C[附加一節,節標題帶時間戳]
|
B --> C[讀回舊頁]
|
||||||
C --> D[既有的節原樣保留]
|
C --> D[換掉最新一輪那一塊]
|
||||||
D --> E[回頭更新 MONITOR_CONTENTS 自己那一列]
|
D --> E[本輪摘要列插到表格最上面,截到 24 列]
|
||||||
E --> F[最後才寫心跳]
|
E --> F[整頁寫回]
|
||||||
|
F --> G[更新 MONITOR_CONTENTS 自己那一列]
|
||||||
|
G --> H[最後才寫心跳]
|
||||||
```
|
```
|
||||||
|
|
||||||
心跳排在最後一步,不能提前。心跳新鮮的意思就是「上一輪跑到這一步了」:這一節沒寫上來,心跳就不寫,讓它自己過期。那是巡檢在空轉的唯一訊號。
|
心跳排在最後一步,不能提前。心跳新鮮的意思就是「上一輪跑到這一步了」:這一輪沒寫上來,心跳就不寫,讓它自己過期。那是巡檢在空轉的唯一訊號。
|
||||||
|
|
||||||
## 本頁基本資料
|
## 本頁基本資料
|
||||||
|
|
||||||
@@ -27,9 +30,11 @@ flowchart LR
|
|||||||
| 雜湊來源 | `{主機名}/{登入帳號}` |
|
| 雜湊來源 | `{主機名}/{登入帳號}` |
|
||||||
| 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) |
|
| 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) |
|
||||||
|
|
||||||
## 巡檢 {yyyy-MM-dd HH:mm}
|
## 最新一輪
|
||||||
|
|
||||||
一輪巡檢就是這樣一節,最新的一節放在最下面。六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
|
這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。
|
||||||
|
|
||||||
|
六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
|
||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -38,6 +43,7 @@ flowchart LR
|
|||||||
| 本輪判定 | {正常、警示、異常 三選一} |
|
| 本輪判定 | {正常、警示、異常 三選一} |
|
||||||
| 本輪項目 | {這一輪跑了哪幾項,成功幾項、失敗幾項} |
|
| 本輪項目 | {這一輪跑了哪幾項,成功幾項、失敗幾項} |
|
||||||
| 讀不到的來源 | {路徑清單,全部讀得到就寫「無」} |
|
| 讀不到的來源 | {路徑清單,全部讀得到就寫「無」} |
|
||||||
|
| 警示來源 | {警示原因,多個用頓號串;沒有就寫「無」} |
|
||||||
|
|
||||||
### 心跳與閘門狀態
|
### 心跳與閘門狀態
|
||||||
|
|
||||||
@@ -49,9 +55,9 @@ flowchart LR
|
|||||||
| session | {工作階段代號} |
|
| session | {工作階段代號} |
|
||||||
| pid | {數字}。只給要找行程的人參考,不參與判定 |
|
| pid | {數字}。只給要找行程的人參考,不參與判定 |
|
||||||
|
|
||||||
這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一節寫上來之後才寫。
|
這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一頁寫成之後才寫。
|
||||||
|
|
||||||
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看本節上面的「本輪判定」。
|
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看這一塊上面的「本輪判定」。
|
||||||
|
|
||||||
### 技能與呼叫鏈使用統計
|
### 技能與呼叫鏈使用統計
|
||||||
|
|
||||||
@@ -111,11 +117,23 @@ flowchart LR
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| {一句話講完要處理什麼} | {上面六個子節之一} | {技能名或指令} |
|
| {一句話講完要處理什麼} | {上面六個子節之一} | {技能名或指令} |
|
||||||
|
|
||||||
|
## 近 24 輪摘要
|
||||||
|
|
||||||
|
一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。
|
||||||
|
|
||||||
|
| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |
|
||||||
|
| --- | --- | --- | ---: | --- |
|
||||||
|
| {yyyy-MM-dd HH:mm} | {正常、警示、異常 三選一} | {成功項數}/{總項數} | {待人處理筆數} | {警示原因,多個用頓號串;沒有就寫「無」} |
|
||||||
|
|
||||||
|
「警示來源」那一欄不能省。四項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄是 4/4,沒有這一欄的話,看的人不知道警示哪來。理由要短,一眼讀完,像「心跳過期」「版本查詢失敗」「重啟閘門未清」「上一輪逾時被接手」。
|
||||||
|
|
||||||
## 寫入規則
|
## 寫入規則
|
||||||
|
|
||||||
- 一次巡檢附加一節,節標題帶時間戳,節名不重複。
|
- 讀不到舊頁就中止,不重組,也不寫入。舊頁讀不回來就沒有摘要表可以接下去。
|
||||||
- 既有的節原樣保留,一個字都不改。
|
- 本頁基本資料原樣保留,一個字都不改。
|
||||||
- 禁止整頁覆寫。覆寫等於把這台機器的巡檢軌跡刪掉。
|
- 最新一輪整塊換掉,只留這一輪的完整內容。
|
||||||
- 讀不到舊內容就中止,不附加,也不寫入。
|
- 本輪的摘要列插到摘要表最上面,舊的列往下移,超過 24 列就丟掉最舊的那一列。
|
||||||
- 附加成功之後,才回頭更新 `MONITOR_CONTENTS` 自己那一列。
|
- 三塊重組成一整頁再整頁寫回。除了這三塊,頁上沒有別的東西。
|
||||||
|
- 舊格式的頁(一輪一節疊起來的那種)第一次重組時,基本資料留著,那些節收掉,摘要表從本輪這一列開始,並在回報裡說明。
|
||||||
|
- 整頁寫成之後,才回頭更新 `MONITOR_CONTENTS` 自己那一列,別台機器的列一個字都不動。
|
||||||
- 兩頁都寫成之後,才寫這一輪的心跳。任一頁沒寫成就不寫心跳,讓它過期。
|
- 兩頁都寫成之後,才寫這一輪的心跳。任一頁沒寫成就不寫心跳,讓它過期。
|
||||||
|
|||||||
+103
-28
@@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
# collect 帶了 --out,finish 與 abort 就要帶同一個目錄,不然換不到本輪的用量快照。
|
# collect 帶了 --out,finish 與 abort 就要帶同一個目錄,不然換不到本輪的用量快照。
|
||||||
#
|
#
|
||||||
# collect 讀四項來源、組出監控頁要附加的那一節、把鎖拿在手上。
|
# collect 讀四項來源、組出監控頁那三塊、把鎖拿在手上。
|
||||||
# finish 在監控頁寫成功之後才呼叫:寫心跳、換上用量快照、放掉鎖。
|
# finish 在監控頁寫成功之後才呼叫:寫心跳、換上用量快照、放掉鎖。
|
||||||
# abort 在監控頁沒寫成時呼叫:只放掉鎖,不寫心跳。
|
# abort 在監控頁沒寫成時呼叫:只放掉鎖,不寫心跳。
|
||||||
#
|
#
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
# 0 collect:四項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
|
# 0 collect:四項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
|
||||||
# 鎖放掉;abort:鎖放掉,本來就沒鎖也算
|
# 鎖放掉;abort:鎖放掉,本來就沒鎖也算
|
||||||
# 1 collect:部分成功——至少一項失敗,也至少一項有結果。**結果照樣印得出來,呼叫端
|
# 1 collect:部分成功——至少一項失敗,也至少一項有結果。**結果照樣印得出來,呼叫端
|
||||||
# 照樣要把這一節寫上監控頁**,只是本輪判定要標成警示
|
# 照樣要把這一輪寫上監控頁**,只是本輪判定要標成警示
|
||||||
# 2 finish:找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可寫。collect 不會回這
|
# 2 finish:找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可寫。collect 不會回這
|
||||||
# 一碼——心跳讀不到只是 D-09 這一項失敗,另外三項照跑
|
# 一碼——心跳讀不到只是 D-09 這一項失敗,另外三項照跑
|
||||||
# 3 collect:四項全部失敗,一項資料都沒有。這一節還是要寫上監控頁,本輪判定標成異常
|
# 3 collect:四項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常
|
||||||
# 4 上一輪還在跑,本輪讓開(collect),或鎖已經不在自己手上(finish、abort)。這不是
|
# 4 上一輪還在跑,本輪讓開(collect),或鎖已經不在自己手上(finish、abort)。這不是
|
||||||
# 失敗,是刻意讓開:不寫心跳、不寫監控頁,下一輪再來
|
# 失敗,是刻意讓開:不寫心跳、不寫監控頁,下一輪再來
|
||||||
# 5 檔案系統失敗:鎖建不起來或放不掉、暫存檔寫不進去、快照換不上,或 heartbeat.sh write
|
# 5 檔案系統失敗:鎖建不起來或放不掉、暫存檔寫不進去、快照換不上,或 heartbeat.sh write
|
||||||
@@ -43,8 +43,7 @@
|
|||||||
#
|
#
|
||||||
# 一輪巡檢包含一次 CLI 呼叫與兩次 wiki 寫入,跑過一個排程週期是有可能的。所以整輪拿一把
|
# 一輪巡檢包含一次 CLI 呼叫與兩次 wiki 寫入,跑過一個排程週期是有可能的。所以整輪拿一把
|
||||||
# 鎖:$JSC_HOME/assistant/patrol.lock 是目錄,mkdir 是原子操作,搶不到就是別人在跑。
|
# 鎖:$JSC_HOME/assistant/patrol.lock 是目錄,mkdir 是原子操作,搶不到就是別人在跑。
|
||||||
# 搶不到的那一輪回 4 直接讓開,不排隊、不並行——並行的兩輪會在同一頁附加兩節,還會互相
|
# 搶不到的那一輪回 4 直接讓開,不排隊、不並行——並行的兩輪會互相蓋掉監控頁與用量快照。
|
||||||
# 蓋掉用量快照。
|
|
||||||
# 鎖會逾時自動搶回來,門檻取心跳門檻(heartbeat.sh report 的 ttl 欄):上一輪跑得比門檻
|
# 鎖會逾時自動搶回來,門檻取心跳門檻(heartbeat.sh report 的 ttl 欄):上一輪跑得比門檻
|
||||||
# 還久,它本來就已經維持不住心跳新鮮了,讓新的一輪接手才對。被搶回來的那一輪,finish 會
|
# 還久,它本來就已經維持不住心跳新鮮了,讓新的一輪接手才對。被搶回來的那一輪,finish 會
|
||||||
# 拿 --round 比對出鎖不是自己的,回 4 且不寫心跳。
|
# 拿 --round 比對出鎖不是自己的,回 4 且不寫心跳。
|
||||||
@@ -65,6 +64,16 @@
|
|||||||
# 巡檢照抄第四欄原字,不自己補查遠端版本、不把「查詢失敗」寫成「相符」或「最新」。
|
# 巡檢照抄第四欄原字,不自己補查遠端版本、不把「查詢失敗」寫成「相符」或「最新」。
|
||||||
# 查不到就是沒有證據,寫成別的字等於幫一個既有缺陷蓋章。
|
# 查不到就是沒有證據,寫成別的字等於幫一個既有缺陷蓋章。
|
||||||
#
|
#
|
||||||
|
# --- 監控頁固定三塊 ---
|
||||||
|
#
|
||||||
|
# 監控頁不再一輪附加一節。15 分鐘一輪,一天就是 96 節,那樣的頁沒有人讀得完,也就沒有人
|
||||||
|
# 會發現壞掉。改成固定三塊:
|
||||||
|
# 本頁基本資料 建頁時寫一次,之後一個字都不動
|
||||||
|
# 最新一輪 每輪整塊換掉,只留最新那一輪的完整內容
|
||||||
|
# 近 24 輪摘要 一輪一列,最新的在最上面,超過 24 列就丟掉最舊的
|
||||||
|
# 軌跡留在摘要表:一輪一列,看得出是從哪一輪開始壞的。完整內容只留最新一輪,因為頁面要能
|
||||||
|
# 讀完才有人讀。
|
||||||
|
#
|
||||||
# --- collect 的輸出 ---
|
# --- collect 的輸出 ---
|
||||||
#
|
#
|
||||||
# stdout 是 key=value,一行一個鍵,供呼叫端逐行取值。監控頁要用的 markdown 不印在
|
# stdout 是 key=value,一行一個鍵,供呼叫端逐行取值。監控頁要用的 markdown 不印在
|
||||||
@@ -78,9 +87,14 @@
|
|||||||
# item= 一項一行,欄位 status(ok、empty、fail)、rc、note
|
# item= 一項一行,欄位 status(ok、empty、fail)、rc、note
|
||||||
# verdict= 正常、警示、異常
|
# verdict= 正常、警示、異常
|
||||||
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
|
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
|
||||||
|
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。四項全過卻判成警示
|
||||||
|
# 時,原因只寫在這裡
|
||||||
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一列用
|
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一列用
|
||||||
# section_file= 要附加到 MONITOR_{HASH} 的那一節
|
# pending= 本輪待人處理的筆數
|
||||||
# newpage_file= MONITOR_{HASH} 不存在時要建的整頁內容
|
# latest_file= 「最新一輪」那一塊,整塊換掉舊頁同名那一塊
|
||||||
|
# summary_file= 「近 24 輪摘要」那一塊,表格裡先放本輪這一列,舊頁的資料列接在下面
|
||||||
|
# summary_row_file= 只有本輪那一列,方便直接插到既有表格最上面
|
||||||
|
# newpage_file= MONITOR_{HASH} 不存在時要建的整頁內容,三塊都已經排好
|
||||||
# contents_file= MONITOR_CONTENTS 那一列的欄位值
|
# contents_file= MONITOR_CONTENTS 那一列的欄位值
|
||||||
#
|
#
|
||||||
# 環境變數:
|
# 環境變數:
|
||||||
@@ -95,6 +109,7 @@ set -u
|
|||||||
|
|
||||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||||
STATE_DIR="$JSC_HOME/assistant"
|
STATE_DIR="$JSC_HOME/assistant"
|
||||||
|
CURRENT="$JSC_HOME/current"
|
||||||
LOCK="$STATE_DIR/patrol.lock"
|
LOCK="$STATE_DIR/patrol.lock"
|
||||||
PREV_SNAP="$STATE_DIR/usage-prev.tsv"
|
PREV_SNAP="$STATE_DIR/usage-prev.tsv"
|
||||||
RD="$STATE_DIR/patrol"
|
RD="$STATE_DIR/patrol"
|
||||||
@@ -107,10 +122,34 @@ ROUND=''
|
|||||||
DRYRUN=0
|
DRYRUN=0
|
||||||
LOCK_BROKEN=0
|
LOCK_BROKEN=0
|
||||||
FAILED_SOURCES=''
|
FAILED_SOURCES=''
|
||||||
|
WARN_SOURCES=''
|
||||||
OK_COUNT=0
|
OK_COUNT=0
|
||||||
FAIL_COUNT=0
|
FAIL_COUNT=0
|
||||||
WARN=0
|
WARN=0
|
||||||
|
|
||||||
|
# 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。
|
||||||
|
# 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉;
|
||||||
|
# 真正的失敗會發生在權限閘門那裡——閘門只放行 current 那一組確切路徑,用別的路徑那一輪會
|
||||||
|
# 被靜靜擋掉、失敗,而且不會寫心跳,外面只看得到心跳過期。這裡先把話講在前面。
|
||||||
|
warn_if_not_current() {
|
||||||
|
_self="$SCRIPT_DIR/$(basename -- "$0")"
|
||||||
|
_want="$CURRENT/jsc-assist/tools/$(basename -- "$0")"
|
||||||
|
case "$SCRIPT_DIR/" in
|
||||||
|
"$CURRENT"/*) return 0 ;;
|
||||||
|
esac
|
||||||
|
printf '[jsc][助理巡檢][WARN]:這支腳本是從 %s 跑起來的,不是 %s。權限閘門只放行 current 那一組確切路徑:排程那一輪用別的路徑會被靜靜擋掉,那一輪失敗、不寫心跳,外面只看得到心跳過期。開發時這樣跑沒關係,無人值守那一輪一律走 current。\n' \
|
||||||
|
"$_self" "$_want" >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
warn_if_not_current
|
||||||
|
|
||||||
|
# 記一個警示來源。每一處把 WARN 設成 1 的地方都經過這裡,摘要表那一欄才看得出警示哪來——
|
||||||
|
# 四項全過卻判成警示,光看成敗欄是查不出原因的。
|
||||||
|
add_warn() { # $1=一句話講完的理由
|
||||||
|
WARN=1
|
||||||
|
if [ -z "$WARN_SOURCES" ]; then WARN_SOURCES="$1"; else WARN_SOURCES="$WARN_SOURCES、$1"; fi
|
||||||
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
usage: patrol.sh collect [--out 目錄] [--trigger 排程|事件|手動]
|
usage: patrol.sh collect [--out 目錄] [--trigger 排程|事件|手動]
|
||||||
@@ -126,12 +165,17 @@ die() { # $1=結束碼 $2=訊息
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 找一支別的 domain 的腳本。搜尋順序比照 schedule.sh 的 heartbeat_sh():先環境變數覆寫,
|
# 找一支別的 domain 的腳本。搜尋順序比照 schedule.sh 的 heartbeat_sh():先環境變數覆寫,
|
||||||
# 再開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
# 再 $JSC_HOME/current 那一組連結,然後開發用的並排存取庫版面,最後已安裝的 plugin 快取
|
||||||
|
# 版面。current 排在快取前面是刻意的:技能與權限規則都以 current 為準,腳本內部再自己去挑
|
||||||
|
# 另一個版本,同一輪就會跑到混版的工具,而那種不一致查起來沒有任何線索。
|
||||||
find_tool() { # $1=domain 短名 $2=domain 內相對路徑 $3=環境變數覆寫值(可為空)
|
find_tool() { # $1=domain 短名 $2=domain 內相對路徑 $3=環境變數覆寫值(可為空)
|
||||||
if [ -n "$3" ]; then
|
if [ -n "$3" ]; then
|
||||||
[ -f "$3" ] && { printf '%s\n' "$3"; return 0; }
|
[ -f "$3" ] && { printf '%s\n' "$3"; return 0; }
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
for _c in "$CURRENT/jsc-$1/$2" "$CURRENT/$1/$2"; do
|
||||||
|
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||||
|
done
|
||||||
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||||||
for _c in "$_root/../$1/$2" "$_root/../jsc-$1/$2"; do
|
for _c in "$_root/../$1/$2" "$_root/../jsc-$1/$2"; do
|
||||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||||
@@ -206,7 +250,7 @@ lock_acquire() {
|
|||||||
printf 'round=%s\npid=%s\nstarted=%s\n' "$ROUND" "$$" "$(date +%s)" >"$LOCK/info" 2>/dev/null \
|
printf 'round=%s\npid=%s\nstarted=%s\n' "$ROUND" "$$" "$(date +%s)" >"$LOCK/info" 2>/dev/null \
|
||||||
|| die 5 "鎖搶回來了,卻寫不進 $LOCK/info。"
|
|| die 5 "鎖搶回來了,卻寫不進 $LOCK/info。"
|
||||||
LOCK_BROKEN=1
|
LOCK_BROKEN=1
|
||||||
WARN=1
|
add_warn '上一輪逾時被接手'
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +387,7 @@ d04() {
|
|||||||
[ "$_rows" -eq 0 ] && printf '| (無 domain) | - | - | 這台機器一個 jsc plugin 都沒查到 |\n' >>"$RD/d04.md"
|
[ "$_rows" -eq 0 ] && printf '| (無 domain) | - | - | 這台機器一個 jsc plugin 都沒查到 |\n' >>"$RD/d04.md"
|
||||||
printf '\n判定欄照 `version-guard.sh report` 第四欄原字抄。抄到「查詢失敗」就寫「查詢失敗」,不改寫成「相符」或「最新」,也不自己補查遠端版本——查不到是沒有證據,不是版本沒問題。\n\n' >>"$RD/d04.md"
|
printf '\n判定欄照 `version-guard.sh report` 第四欄原字抄。抄到「查詢失敗」就寫「查詢失敗」,不改寫成「相符」或「最新」,也不自己補查遠端版本——查不到是沒有證據,不是版本沒問題。\n\n' >>"$RD/d04.md"
|
||||||
if [ "$_unver" -gt 0 ]; then
|
if [ "$_unver" -gt 0 ]; then
|
||||||
WARN=1
|
add_warn '版本查詢失敗'
|
||||||
printf '**本輪有 %s 列查不到遠端版本。** 同一支腳本在擋人那條路徑查得到遠端版本,report 這條查不到,這是既有缺陷,不是這台機器的網路問題。\n\n' "$_unver" >>"$RD/d04.md"
|
printf '**本輪有 %s 列查不到遠端版本。** 同一支腳本在擋人那條路徑查得到遠端版本,report 這條查不到,這是既有缺陷,不是這台機器的網路問題。\n\n' "$_unver" >>"$RD/d04.md"
|
||||||
add_pending 'version-guard.sh report 查不到遠端版本,版本落差本輪無證據' '版本落差與重啟閘門' '/jsc-cli:doctor'
|
add_pending 'version-guard.sh report 查不到遠端版本,版本落差本輪無證據' '版本落差與重啟閘門' '/jsc-cli:doctor'
|
||||||
fi
|
fi
|
||||||
@@ -378,7 +422,7 @@ d04() {
|
|||||||
if [ "$_up" -eq 0 ]; then
|
if [ "$_up" -eq 0 ]; then
|
||||||
printf '| (無) | 未升起 | - |\n' >>"$RD/d04.md"
|
printf '| (無) | 未升起 | - |\n' >>"$RD/d04.md"
|
||||||
else
|
else
|
||||||
WARN=1
|
add_warn '重啟閘門未清'
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -499,10 +543,10 @@ d09() {
|
|||||||
HEARTBEAT_TTL="$_ttl"
|
HEARTBEAT_TTL="$_ttl"
|
||||||
case "$_st" in
|
case "$_st" in
|
||||||
fresh) HEARTBEAT_STATE='新鮮' ;;
|
fresh) HEARTBEAT_STATE='新鮮' ;;
|
||||||
stale) HEARTBEAT_STATE='過期'; WARN=1 ;;
|
stale) HEARTBEAT_STATE='過期'; add_warn '心跳過期' ;;
|
||||||
invalid) HEARTBEAT_STATE='心跳檔損壞'; WARN=1 ;;
|
invalid) HEARTBEAT_STATE='心跳檔損壞'; add_warn '心跳檔損壞' ;;
|
||||||
absent) HEARTBEAT_STATE='不存在'; WARN=1 ;;
|
absent) HEARTBEAT_STATE='不存在'; add_warn '心跳不存在' ;;
|
||||||
*) HEARTBEAT_STATE="判不出(state=${_st:-空值})"; WARN=1 ;;
|
*) HEARTBEAT_STATE="判不出(state=${_st:-空值})"; add_warn '心跳判不出' ;;
|
||||||
esac
|
esac
|
||||||
{
|
{
|
||||||
printf '| 項目 | 內容 |\n'
|
printf '| 項目 | 內容 |\n'
|
||||||
@@ -519,7 +563,7 @@ d09() {
|
|||||||
printf '| pid | %s。只給要找行程的人參考,不參與判定 |\n' "$(cell "${_pid:--}")"
|
printf '| pid | %s。只給要找行程的人參考,不參與判定 |\n' "$(cell "${_pid:--}")"
|
||||||
printf '| 心跳檔 | `%s` |\n' "$(cell "${_file:--}")"
|
printf '| 心跳檔 | `%s` |\n' "$(cell "${_file:--}")"
|
||||||
printf '\n心跳的判準只看 `ts` 距現在有沒有超過門檻,不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳,不參與判定。\n'
|
printf '\n心跳的判準只看 `ts` 距現在有沒有超過門檻,不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳,不參與判定。\n'
|
||||||
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看本節上面的「本輪判定」。\n'
|
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看這一塊上面的「本輪判定」。\n'
|
||||||
} >>"$RD/d09.md"
|
} >>"$RD/d09.md"
|
||||||
|
|
||||||
case "$_st" in
|
case "$_st" in
|
||||||
@@ -546,7 +590,7 @@ count_tasks() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 組出監控頁那一節 ---
|
# --- 組出監控頁那三塊 ---
|
||||||
|
|
||||||
tally() { # $1=項目狀態
|
tally() { # $1=項目狀態
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -562,8 +606,12 @@ compose() {
|
|||||||
[ "$OK_COUNT" -eq 0 ] && _v='異常'
|
[ "$OK_COUNT" -eq 0 ] && _v='異常'
|
||||||
VERDICT="$_v"
|
VERDICT="$_v"
|
||||||
|
|
||||||
|
ITEM_TOTAL=$(( OK_COUNT + FAIL_COUNT ))
|
||||||
|
PEND_COUNT=$(grep -c '^| ' "$RD/pend.md" 2>/dev/null); [ -n "$PEND_COUNT" ] || PEND_COUNT=0
|
||||||
|
|
||||||
{
|
{
|
||||||
printf '## 巡檢 %s\n\n' "$AT"
|
printf '## 最新一輪\n\n'
|
||||||
|
printf '這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。\n\n'
|
||||||
printf '| 項目 | 內容 |\n'
|
printf '| 項目 | 內容 |\n'
|
||||||
printf '| --- | --- |\n'
|
printf '| --- | --- |\n'
|
||||||
printf '| 巡檢時間 | %s |\n' "$AT"
|
printf '| 巡檢時間 | %s |\n' "$AT"
|
||||||
@@ -571,6 +619,9 @@ compose() {
|
|||||||
printf '| 本輪判定 | %s |\n' "$VERDICT"
|
printf '| 本輪判定 | %s |\n' "$VERDICT"
|
||||||
printf '| 本輪項目 | 四項:D-01 使用統計、D-04 版本與重啟閘門、D-07 階段鎖與工作包鎖、D-09 心跳自述。成功 %s 項、失敗 %s 項 |\n' "$OK_COUNT" "$FAIL_COUNT"
|
printf '| 本輪項目 | 四項:D-01 使用統計、D-04 版本與重啟閘門、D-07 階段鎖與工作包鎖、D-09 心跳自述。成功 %s 項、失敗 %s 項 |\n' "$OK_COUNT" "$FAIL_COUNT"
|
||||||
printf '| 讀不到的來源 | %s |\n' "$(cell "${FAILED_SOURCES:-無}")"
|
printf '| 讀不到的來源 | %s |\n' "$(cell "${FAILED_SOURCES:-無}")"
|
||||||
|
# 警示來源緊接在讀不到的來源後面:兩列語意相近,而且四項讀取全部成功、判定卻是警示
|
||||||
|
# 時,這一塊裡只有這一列講得出原因,跟摘要表那一欄是同一個理由。
|
||||||
|
printf '| 警示來源 | %s |\n' "$(cell "${WARN_SOURCES:-無}")"
|
||||||
if [ "$LOCK_BROKEN" -eq 1 ]; then
|
if [ "$LOCK_BROKEN" -eq 1 ]; then
|
||||||
printf '| 鎖 | 上一輪的鎖逾時,本輪搶回來了。上一輪沒跑完,那一輪不會寫心跳 |\n'
|
printf '| 鎖 | 上一輪的鎖逾時,本輪搶回來了。上一輪沒跑完,那一輪不會寫心跳 |\n'
|
||||||
fi
|
fi
|
||||||
@@ -589,21 +640,40 @@ compose() {
|
|||||||
printf '| 項目 | 來源子節 | 建議入口 |\n'
|
printf '| 項目 | 來源子節 | 建議入口 |\n'
|
||||||
printf '| --- | --- | --- |\n'
|
printf '| --- | --- | --- |\n'
|
||||||
if [ -s "$RD/pend.md" ]; then cat "$RD/pend.md"; else printf '| (無) | - | - |\n'; fi
|
if [ -s "$RD/pend.md" ]; then cat "$RD/pend.md"; else printf '| (無) | - | - |\n'; fi
|
||||||
} >"$RD/section.md"
|
} >"$RD/latest.md"
|
||||||
|
|
||||||
|
# 摘要表的那一列。欄位刻意只有五個,一列要能一眼看完,才看得出是從哪一輪開始壞的。
|
||||||
|
# 「警示來源」那一欄不能省:四項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄
|
||||||
|
# 是 4/4,沒有這一欄的話,看的人不知道警示哪來。
|
||||||
|
printf '| %s | %s | %s/%s | %s | %s |\n' \
|
||||||
|
"$AT" "$VERDICT" "$OK_COUNT" "$ITEM_TOTAL" "$PEND_COUNT" \
|
||||||
|
"$(cell "${WARN_SOURCES:-無}")" >"$RD/summary-row.md"
|
||||||
|
|
||||||
|
# 摘要那一塊:標題、表頭,加上本輪這一列。呼叫端把舊頁的資料列接在這一列下面,截到 24 列。
|
||||||
|
{
|
||||||
|
printf '## 近 24 輪摘要\n\n'
|
||||||
|
printf '一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。\n\n'
|
||||||
|
printf '| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |\n'
|
||||||
|
printf '| --- | --- | --- | ---: | --- |\n'
|
||||||
|
cat "$RD/summary-row.md"
|
||||||
|
} >"$RD/summary.md"
|
||||||
|
|
||||||
# 監控頁不存在時要建的整頁內容。基本資料建頁時寫一次,之後不再更動。
|
# 監控頁不存在時要建的整頁內容。基本資料建頁時寫一次,之後不再更動。
|
||||||
{
|
{
|
||||||
printf '# 助理巡檢 — %s/%s\n\n' "$HOST" "$USER_NAME"
|
printf '# 助理巡檢 — %s/%s\n\n' "$HOST" "$USER_NAME"
|
||||||
printf '> 由 `jsc-assist` 維護。這是監控頁 `%s`。\n' "$PAGE"
|
printf '> 由 `jsc-assist` 維護。這是監控頁 `%s`。\n' "$PAGE"
|
||||||
printf '> 這頁是這台機器的巡檢軌跡:一次巡檢附加一節,節標題帶時間戳,舊的節一個字都不動。\n'
|
printf '> 這頁固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。\n'
|
||||||
printf '> 附加是刻意的。助理的寫入是背景行為,覆寫錯了沒人在現場,軌跡被抹掉也看不出斷在哪一輪。\n'
|
printf '> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。\n'
|
||||||
printf '> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,寫入語意與這頁不同,不要混用。\n\n'
|
printf '> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。\n'
|
||||||
|
printf '> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,別台機器的列一個字都不動。\n\n'
|
||||||
printf '```mermaid\nflowchart LR\n'
|
printf '```mermaid\nflowchart LR\n'
|
||||||
printf ' A[巡檢一輪] --> B[收攏四項結果]\n'
|
printf ' A[巡檢一輪] --> B[收攏四項結果]\n'
|
||||||
printf ' B --> C[附加一節,節標題帶時間戳]\n'
|
printf ' B --> C[讀回舊頁]\n'
|
||||||
printf ' C --> D[既有的節原樣保留]\n'
|
printf ' C --> D[換掉最新一輪那一塊]\n'
|
||||||
printf ' D --> E[回頭更新 MONITOR_CONTENTS 自己那一列]\n'
|
printf ' D --> E[本輪摘要列插到表格最上面,截到 24 列]\n'
|
||||||
printf ' E --> F[最後才寫心跳]\n'
|
printf ' E --> F[整頁寫回]\n'
|
||||||
|
printf ' F --> G[更新 MONITOR_CONTENTS 自己那一列]\n'
|
||||||
|
printf ' G --> H[最後才寫心跳]\n'
|
||||||
printf '```\n\n'
|
printf '```\n\n'
|
||||||
printf '## 本頁基本資料\n\n'
|
printf '## 本頁基本資料\n\n'
|
||||||
printf '建頁時寫一次,之後不再更動。\n\n'
|
printf '建頁時寫一次,之後不再更動。\n\n'
|
||||||
@@ -613,7 +683,8 @@ compose() {
|
|||||||
printf '| 帳號 | %s |\n' "$(cell "$USER_NAME")"
|
printf '| 帳號 | %s |\n' "$(cell "$USER_NAME")"
|
||||||
printf '| 雜湊來源 | `%s/%s` |\n' "$(cell "$HOST")" "$(cell "$USER_NAME")"
|
printf '| 雜湊來源 | `%s/%s` |\n' "$(cell "$HOST")" "$(cell "$USER_NAME")"
|
||||||
printf '| 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) |\n\n'
|
printf '| 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) |\n\n'
|
||||||
cat "$RD/section.md"
|
cat "$RD/latest.md"; printf '\n'
|
||||||
|
cat "$RD/summary.md"
|
||||||
} >"$RD/newpage.md"
|
} >"$RD/newpage.md"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -693,9 +764,13 @@ case "$CMD" in
|
|||||||
printf 'item=D-09 status=%s rc=%s note=%s\n' "$D09_STATUS" "$D09_RC" "$D09_NOTE"
|
printf 'item=D-09 status=%s rc=%s note=%s\n' "$D09_STATUS" "$D09_RC" "$D09_NOTE"
|
||||||
printf 'verdict=%s\n' "$VERDICT"
|
printf 'verdict=%s\n' "$VERDICT"
|
||||||
printf 'failed_sources=%s\n' "${FAILED_SOURCES:-無}"
|
printf 'failed_sources=%s\n' "${FAILED_SOURCES:-無}"
|
||||||
|
printf 'warn_sources=%s\n' "${WARN_SOURCES:-無}"
|
||||||
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
|
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
|
||||||
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
|
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
|
||||||
printf 'section_file=%s\n' "$RD/section.md"
|
printf 'pending=%s\n' "$PEND_COUNT"
|
||||||
|
printf 'latest_file=%s\n' "$RD/latest.md"
|
||||||
|
printf 'summary_file=%s\n' "$RD/summary.md"
|
||||||
|
printf 'summary_row_file=%s\n' "$RD/summary-row.md"
|
||||||
printf 'newpage_file=%s\n' "$RD/newpage.md"
|
printf 'newpage_file=%s\n' "$RD/newpage.md"
|
||||||
printf 'contents_file=%s\n' "$RD/contents.tsv"
|
printf 'contents_file=%s\n' "$RD/contents.tsv"
|
||||||
|
|
||||||
|
|||||||
+187
-29
@@ -60,6 +60,36 @@
|
|||||||
# 條目一律接 `</dev/null`,跑的東西讀不到標準輸入,卡不住。heartbeat.sh 本來就不讀 stdin,
|
# 條目一律接 `</dev/null`,跑的東西讀不到標準輸入,卡不住。heartbeat.sh 本來就不讀 stdin,
|
||||||
# 這一條是給巡檢那一筆用的:助理跳出權限詢問就會整輪卡死,等於排程沒跑。
|
# 這一條是給巡檢那一筆用的:助理跳出權限詢問就會整輪卡死,等於排程沒跑。
|
||||||
#
|
#
|
||||||
|
# --- 排程那一輪的環境跟你現在這個殼不一樣 ---
|
||||||
|
#
|
||||||
|
# cron 給的 PATH 很短,多半只有 /usr/bin 與 /bin,裝在 ~/.local/bin 的 CLI 一律 not found。
|
||||||
|
# cron 也不讀 .bashrc,所以殼裡設好的 GITEA_HOST、GITEA_TOKEN、JSC_WIKI_REPO_* 那一輪全是
|
||||||
|
# 空的,wiki 連不上,監控頁寫不成,巡檢就不寫心跳,整條排程空轉。
|
||||||
|
# 所以條目自己把環境帶齊,兩件事:
|
||||||
|
# 一、CLI 用 `command -v` 解成絕對路徑再寫進條目,解不到就回 6 停下,不猜路徑——猜錯只是
|
||||||
|
# 把「每輪 not found」換成「每輪跑錯東西」,一樣沒有人在現場看得到。
|
||||||
|
# 二、要用到的環境變數在 install 當下就地快照,以 `名稱='值'` 寫成條目的指令前綴。
|
||||||
|
# 條目裡不 source 任何設定檔:很多機器的 .bashrc 開頭就是「非互動就 return」,那一招在 cron
|
||||||
|
# 底下會安靜失效,失效的樣子跟沒設一模一樣,查起來要花一整輪。快照的代價是條目寫死了安裝
|
||||||
|
# 當下的值,環境變數改過就要重跑一次 install,條目才會跟著換。
|
||||||
|
# 條目裡帶著金鑰快照,crontab 檔案要保持只有本人讀得到;本腳本印出條目時一律把金鑰遮掉。
|
||||||
|
#
|
||||||
|
# --- 無人值守那一輪要自己帶寫入確認 ---
|
||||||
|
#
|
||||||
|
# jsc-gitea 的寫入確認只認 tty,而條目帶 `</dev/null`,排程那一輪永遠沒有 tty,寫 wiki 一定
|
||||||
|
# 被擋。頁寫不成就不寫心跳,於是排程裝著卻永遠空轉。所以條目自帶 JSC_GITEA_CONFIRM=yes:
|
||||||
|
# 無人值守的那一輪本來就沒有人可以按同意,擋下來也沒有人會看到。
|
||||||
|
#
|
||||||
|
# --- 裝完要開哪些權限 ---
|
||||||
|
#
|
||||||
|
# 排程那一輪跑在沒有人的工作階段,跳出權限詢問就是卡住整輪,而且卡到鎖逾時才有下一輪。
|
||||||
|
# install 成功之後會把那一輪需要的 allow 規則印出來,一行一條。
|
||||||
|
# 路徑一律走 $JSC_HOME/current/{外掛名}:那是一組不帶版本的連結,指向該外掛在快取裡的最新
|
||||||
|
# 版,由 deploy 維護。規則就是那組確切路徑,比對得準,外掛升版也不用回頭改設定。
|
||||||
|
# 檔案寫入只認 Edit(...),Write(...) 規則沒有作用,所以不印 Write。
|
||||||
|
# 連結不在就先警告:那一輪會因為找不到工具而失敗。連結由 deploy 建,本腳本不代建——排程
|
||||||
|
# 腳本自己去補外掛的部署結構,等於兩個地方管同一件事,壞掉的時候查不出是誰建的。
|
||||||
|
#
|
||||||
# --- log 放哪裡 ---
|
# --- log 放哪裡 ---
|
||||||
#
|
#
|
||||||
# $JSC_HOME/assistant/schedule.log(JSC_HOME 未設定時為 ~/.jsc)。刻意放在專案外面:寫進
|
# $JSC_HOME/assistant/schedule.log(JSC_HOME 未設定時為 ~/.jsc)。刻意放在專案外面:寫進
|
||||||
@@ -71,12 +101,16 @@
|
|||||||
# 假的 crontab 驗濾除邏輯時才設
|
# 假的 crontab 驗濾除邏輯時才設
|
||||||
# JSC_ASSIST_PATROL_CMD 巡檢要跑的指令,優先於 --patrol-cmd 以外的所有推斷
|
# JSC_ASSIST_PATROL_CMD 巡檢要跑的指令,優先於 --patrol-cmd 以外的所有推斷
|
||||||
# JSC_CLI 目前是哪一支 CLI,決定巡檢預設指令
|
# JSC_CLI 目前是哪一支 CLI,決定巡檢預設指令
|
||||||
# JSC_ASSISTANT_HEARTBEAT_TTL 心跳過期門檻,單位秒。巡檢週期由它算出來
|
# JSC_ASSISTANT_HEARTBEAT_TTL 心跳過期門檻,單位秒。巡檢週期由它算出來,也會快照進條目
|
||||||
|
# GITEA_HOST GITEA_TOKEN wiki 連線用。install 當下快照進條目
|
||||||
|
# JSC_WIKI_REPO wiki 存取庫預設值。install 當下快照進條目
|
||||||
|
# JSC_WIKI_REPO_{TYPE} 各頁型的 wiki 存取庫。已設定的全部快照進條目
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
MARK_PREFIX='# jsc-assist:assistant'
|
MARK_PREFIX='# jsc-assist:assistant'
|
||||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||||
STATE_DIR="$JSC_HOME/assistant"
|
STATE_DIR="$JSC_HOME/assistant"
|
||||||
|
CURRENT="$JSC_HOME/current"
|
||||||
LOG="$STATE_DIR/schedule.log"
|
LOG="$STATE_DIR/schedule.log"
|
||||||
CRONTAB_CMD="${JSC_ASSIST_CRONTAB_CMD:-crontab}"
|
CRONTAB_CMD="${JSC_ASSIST_CRONTAB_CMD:-crontab}"
|
||||||
TASK_PREFIX='jsc-assist-assistant'
|
TASK_PREFIX='jsc-assist-assistant'
|
||||||
@@ -89,6 +123,22 @@ PERIOD=''
|
|||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||||
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
||||||
|
|
||||||
|
# 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。
|
||||||
|
# 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉;
|
||||||
|
# 真正的失敗會發生在權限閘門那裡——閘門只放行 current 那一組確切路徑,用別的路徑那一輪會
|
||||||
|
# 被靜靜擋掉、失敗,而且不會寫心跳,外面只看得到心跳過期。這裡先把話講在前面。
|
||||||
|
warn_if_not_current() {
|
||||||
|
_self="$SCRIPT_DIR/$(basename -- "$0")"
|
||||||
|
_want="$CURRENT/jsc-assist/tools/$(basename -- "$0")"
|
||||||
|
case "$SCRIPT_DIR/" in
|
||||||
|
"$CURRENT"/*) return 0 ;;
|
||||||
|
esac
|
||||||
|
printf '[jsc][助理排程][WARN]:這支腳本是從 %s 跑起來的,不是 %s。權限閘門只放行 current 那一組確切路徑:排程那一輪用別的路徑會被靜靜擋掉,那一輪失敗、不寫心跳,外面只看得到心跳過期。開發時這樣跑沒關係,無人值守那一輪一律走 current。\n' \
|
||||||
|
"$_self" "$_want" >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
warn_if_not_current
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
usage: schedule.sh install [patrol|all] [--dry-run] [--cli 代號] [--patrol-cmd 指令] [--period 分鐘]
|
usage: schedule.sh install [patrol|all] [--dry-run] [--cli 代號] [--patrol-cmd 指令] [--period 分鐘]
|
||||||
@@ -105,12 +155,17 @@ die() { # $1=結束碼 $2=訊息
|
|||||||
|
|
||||||
note() { printf '[jsc][助理排程]:%s\n' "$1" >&2; }
|
note() { printf '[jsc][助理排程]:%s\n' "$1" >&2; }
|
||||||
|
|
||||||
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序比照 jsc-hooks lib.sh 的
|
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序:先環境變數覆寫,再
|
||||||
# jsc_gitea_sh():先環境變數,再開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
# $JSC_HOME/current 那一組連結,然後開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
||||||
|
# current 排在快取前面是刻意的:技能與權限規則都以 current 為準,腳本內部再自己去挑另一個
|
||||||
|
# 版本,同一輪就會跑到混版的工具,而那種不一致查起來沒有任何線索。
|
||||||
heartbeat_sh() {
|
heartbeat_sh() {
|
||||||
if [ -n "${JSC_HOOKS_HOOKS:-}" ] && [ -f "$JSC_HOOKS_HOOKS/heartbeat.sh" ]; then
|
if [ -n "${JSC_HOOKS_HOOKS:-}" ] && [ -f "$JSC_HOOKS_HOOKS/heartbeat.sh" ]; then
|
||||||
printf '%s\n' "$JSC_HOOKS_HOOKS/heartbeat.sh"; return 0
|
printf '%s\n' "$JSC_HOOKS_HOOKS/heartbeat.sh"; return 0
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$CURRENT/jsc-hooks/hooks/heartbeat.sh" ]; then
|
||||||
|
printf '%s\n' "$CURRENT/jsc-hooks/hooks/heartbeat.sh"; return 0
|
||||||
|
fi
|
||||||
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||||||
for _c in "$_root/../hooks/hooks/heartbeat.sh" "$_root/../jsc-hooks/hooks/heartbeat.sh"; do
|
for _c in "$_root/../hooks/hooks/heartbeat.sh" "$_root/../jsc-hooks/hooks/heartbeat.sh"; do
|
||||||
[ -f "$_c" ] && { (CDPATH= cd -- "$(dirname -- "$_c")" && printf '%s/heartbeat.sh\n' "$(pwd)"); return 0; }
|
[ -f "$_c" ] && { (CDPATH= cd -- "$(dirname -- "$_c")" && printf '%s/heartbeat.sh\n' "$(pwd)"); return 0; }
|
||||||
@@ -206,28 +261,74 @@ spec_of() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 巡檢要跑的指令。優先序:--patrol-cmd 或 JSC_ASSIST_PATROL_CMD > 依 CLI 代號推斷。
|
# 巡檢要跑的指令。優先序:--patrol-cmd 或 JSC_ASSIST_PATROL_CMD > 依 CLI 代號推斷。
|
||||||
# 判不出 CLI 就回非 0,由主流程回 6,不猜——猜錯會每 15 分鐘跑一支不存在的執行檔。
|
# 判不出 CLI,或那一支的執行檔不在 PATH 上,都回非 0 由主流程回 6,不猜。
|
||||||
|
# 執行檔一律用 `command -v` 解成絕對路徑:cron 的 PATH 只有 /usr/bin 與 /bin,裸的指令名
|
||||||
|
# 每一輪都是 not found,而那一輪不會有人看到錯誤訊息。
|
||||||
patrol_command() {
|
patrol_command() {
|
||||||
[ -n "$PATROL_CMD" ] && { printf '%s' "$PATROL_CMD"; return 0; }
|
[ -n "$PATROL_CMD" ] && { printf '%s' "$PATROL_CMD"; return 0; }
|
||||||
_cli="$CLI"
|
_cli="$CLI"
|
||||||
[ -n "$_cli" ] || _cli="${JSC_CLI:-}"
|
[ -n "$_cli" ] || _cli="${JSC_CLI:-}"
|
||||||
[ -n "$_cli" ] || { [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && _cli=claude; }
|
[ -n "$_cli" ] || { [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && _cli=claude; }
|
||||||
case "$_cli" in
|
case "$_cli" in
|
||||||
claude) printf 'claude -p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
claude) _bin='claude'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||||
codex) printf "codex exec '\$assistant 跑一輪巡檢'" ;;
|
codex) _bin='codex'; _args="exec '\$assistant 跑一輪巡檢'" ;;
|
||||||
copilot) printf 'copilot -p "跑一輪助理巡檢"' ;;
|
copilot) _bin='copilot'; _args='-p "跑一輪助理巡檢"' ;;
|
||||||
antigravity) printf 'agy -p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
antigravity) _bin='agy'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||||
kiro) printf 'kiro-cli -p "跑一輪助理巡檢"' ;;
|
kiro) _bin='kiro-cli'; _args='-p "跑一輪助理巡檢"' ;;
|
||||||
*) return 1 ;;
|
*) printf '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。\n' >&2
|
||||||
|
return 1 ;;
|
||||||
esac
|
esac
|
||||||
|
_abs=$(command -v "$_bin" 2>/dev/null) || _abs=''
|
||||||
|
# 內建指令與別名也會被 command -v 認出來,但那些寫進 cron 沒有意義,只收絕對路徑。
|
||||||
|
case "$_abs" in
|
||||||
|
/*) ;;
|
||||||
|
*) printf '這台機器的 PATH 上找不到 %s 的執行檔,排程條目沒有絕對路徑可以寫。請先確認 %s 裝好了,或改用 --patrol-cmd 指定完整指令。\n' "$_cli" "$_bin" >&2
|
||||||
|
return 1 ;;
|
||||||
|
esac
|
||||||
|
printf "'%s' %s" "$_abs" "$_args"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 要快照進條目的環境變數名稱。前四支是巡檢那一輪一定要用到的;JSC_WIKI_REPO 系列逐台機器
|
||||||
|
# 不同,直接從現在的環境撈出所有已設定的,不寫死清單。
|
||||||
|
snapshot_names() {
|
||||||
|
printf '%s\n' GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL
|
||||||
|
env 2>/dev/null \
|
||||||
|
| sed -n 's/^\(JSC_WIKI_REPO\)=.*/\1/p; s/^\(JSC_WIKI_REPO_[A-Za-z0-9_]*\)=.*/\1/p' \
|
||||||
|
| sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# 把值包成單引號字串。值裡的單引號照 POSIX 的 '\'' 寫法拆開再接回來,不然一個引號就把
|
||||||
|
# 整條 cron 指令切斷。
|
||||||
|
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
||||||
|
|
||||||
|
# 條目的指令前綴:固定三個旗標,加上這一刻的環境快照。沒設定的變數直接跳過,不寫空值——
|
||||||
|
# 寫 NAME='' 進去,讀的人分不出是「刻意設成空」還是「安裝時忘了設」。
|
||||||
|
# JSC_GITEA_CONFIRM=yes 是必要的:寫入確認只認 tty,排程那一輪沒有 tty,不帶這個旗標監控頁
|
||||||
|
# 一定寫不成,而頁寫不成就不寫心跳,排程等於永遠空轉。
|
||||||
|
env_prefix() { # 順便把快照到的變數名稱寫進 $1,供輸出列出名稱(只有名稱,不含值)
|
||||||
|
_p='JSC_CLI=cron JSC_SESSION_ID=schedule JSC_GITEA_CONFIRM=yes'
|
||||||
|
_names=''
|
||||||
|
for _n in $(snapshot_names); do
|
||||||
|
eval "_v=\${$_n:-}"
|
||||||
|
[ -n "$_v" ] || continue
|
||||||
|
_p="$_p $_n=$(shq "$_v")"
|
||||||
|
if [ -z "$_names" ]; then _names="$_n"; else _names="$_names,$_n"; fi
|
||||||
|
done
|
||||||
|
[ -n "${1:-}" ] && printf '%s' "$_names" >"$1"
|
||||||
|
printf '%s' "$_p"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 條目裡有金鑰快照,原樣印出來就是把金鑰留在對話紀錄與 log 裡,所以印之前先遮掉值。
|
||||||
|
# 遮到下一個空白為止,不遮到下一個單引號:值裡的單引號會跳脫成 '\'',遇到引號就收手會把
|
||||||
|
# 金鑰的後半段漏出來。Gitea 的金鑰不含空白,所以以空白為界是安全的。
|
||||||
|
mask_secret() { sed "s/GITEA_TOKEN=[^ ]*/GITEA_TOKEN='***'/g"; }
|
||||||
|
|
||||||
# 組出一筆 crontab 條目。`%` 在 crontab 是換行符號,一律跳脫。
|
# 組出一筆 crontab 條目。`%` 在 crontab 是換行符號,一律跳脫。
|
||||||
cron_entry() { # $1=工作代號
|
cron_entry() { # $1=工作代號
|
||||||
_spec=$(spec_of "$1")
|
_spec=$(spec_of "$1")
|
||||||
case "$1" in
|
case "$1" in
|
||||||
heartbeat) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule '$HEARTBEAT' write" ;;
|
heartbeat) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule '$HEARTBEAT' write" ;;
|
||||||
patrol) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule $PATROL_RESOLVED" ;;
|
patrol) _cmd="$ENV_PREFIX $PATROL_RESOLVED" ;;
|
||||||
esac
|
esac
|
||||||
printf '%s %s </dev/null >>%s 2>&1 %s' \
|
printf '%s %s </dev/null >>%s 2>&1 %s' \
|
||||||
"$_spec" "$_cmd" "'$LOG'" "$(marker_of "$1")" | sed 's/%/\\%/g'
|
"$_spec" "$_cmd" "'$LOG'" "$(marker_of "$1")" | sed 's/%/\\%/g'
|
||||||
@@ -301,18 +402,69 @@ else
|
|||||||
PERIOD=$(period_for_ttl "$TTL")
|
PERIOD=$(period_for_ttl "$TTL")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 巡檢指令在這裡就解出來。放進 cron_entry 再解的話,那支是在命令替換的子行程裡跑,
|
|
||||||
# 判不出 CLI 時 die 只結束子行程,主流程會帶著空指令繼續往下裝。
|
|
||||||
PATROL_RESOLVED=''
|
|
||||||
case "$CMD:$JOBS" in
|
|
||||||
install:*patrol*)
|
|
||||||
PATROL_RESOLVED=$(patrol_command) \
|
|
||||||
|| die 6 '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。' ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
TMPD=$(mktemp -d) || die 4 '建不出暫存目錄。'
|
TMPD=$(mktemp -d) || die 4 '建不出暫存目錄。'
|
||||||
trap 'rm -rf "$TMPD"' EXIT
|
trap 'rm -rf "$TMPD"' EXIT
|
||||||
|
|
||||||
|
# 巡檢指令與環境快照在這裡就解出來。放進 cron_entry 再解的話,那支是在命令替換的子行程裡
|
||||||
|
# 跑,判不出 CLI 時 die 只結束子行程,主流程會帶著空指令繼續往下裝。
|
||||||
|
PATROL_RESOLVED=''
|
||||||
|
ENV_PREFIX='JSC_CLI=cron JSC_SESSION_ID=schedule JSC_GITEA_CONFIRM=yes'
|
||||||
|
SNAPSHOT_NAMES=''
|
||||||
|
case "$CMD:$JOBS" in
|
||||||
|
install:*patrol*)
|
||||||
|
if ! PATROL_RESOLVED=$(patrol_command 2>"$TMPD/cmd.err"); then
|
||||||
|
_why=$(tr '\n' ' ' <"$TMPD/cmd.err" 2>/dev/null)
|
||||||
|
[ -n "$_why" ] || _why='判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。'
|
||||||
|
die 6 "$_why"
|
||||||
|
fi
|
||||||
|
ENV_PREFIX=$(env_prefix "$TMPD/snapnames")
|
||||||
|
SNAPSHOT_NAMES=$(cat "$TMPD/snapnames" 2>/dev/null) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- 裝完要開的權限 ---
|
||||||
|
|
||||||
|
# 排程那一輪跑在沒有人的工作階段:跳出一次權限詢問就是整輪卡住,卡到鎖逾時才有下一輪。
|
||||||
|
# 這一輪會用到的三支腳本,各印裸路徑、`sh 路徑`、`bash 路徑` 三種呼叫形式——同一支腳本換
|
||||||
|
# 一種叫法就是另一條規則,少印一種就會在那一種叫法上卡住。
|
||||||
|
# 路徑一律是 $JSC_HOME/current/{外掛名} 那一組,不是這支腳本現在被放在哪裡:規則放行的是
|
||||||
|
# 那一組路徑,巡檢那一輪也只能用那一組路徑去叫工具,兩邊對得起來才不會被靜靜擋掉。
|
||||||
|
print_allow_rules() {
|
||||||
|
# gitea.sh 一定要有自己這一條。`Skill(jsc-gitea:wiki)` 只放行「叫用那支技能」,技能裡的
|
||||||
|
# 每一個 Bash 呼叫仍然各自受檢,少了這一條,那一輪會在寫監控頁時靜靜被擋——頁寫不成就
|
||||||
|
# 不寫心跳,外面只看得到心跳過期,看不出是權限擋的。
|
||||||
|
for _s in "$CURRENT/jsc-assist/tools/schedule.sh" \
|
||||||
|
"$CURRENT/jsc-assist/tools/patrol.sh" \
|
||||||
|
"$CURRENT/jsc-hooks/hooks/heartbeat.sh" \
|
||||||
|
"$CURRENT/jsc-gitea/tools/gitea.sh"; do
|
||||||
|
printf 'allow_rule=Bash(%s:*)\n' "$_s"
|
||||||
|
printf 'allow_rule=Bash(sh %s:*)\n' "$_s"
|
||||||
|
printf 'allow_rule=Bash(bash %s:*)\n' "$_s"
|
||||||
|
done
|
||||||
|
# 檔案規則寫的是解出來的絕對路徑,不是 $JSC_HOME/assistant:設定檔不展開變數,寫變數名
|
||||||
|
# 等於這條規則永遠比對不到。
|
||||||
|
printf 'allow_rule=Read(%s/**)\n' "$STATE_DIR"
|
||||||
|
printf 'allow_rule=Edit(%s/**)\n' "$STATE_DIR"
|
||||||
|
printf 'allow_rule=Skill(jsc-gitea:wiki)\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 巡檢那一輪只用 $JSC_HOME/current 那一組連結叫工具,連結不在就是那一輪一定失敗。這裡只查
|
||||||
|
# 與警告,不代建:連結是 deploy 的職責,兩個地方都在建同一組連結,壞掉時查不出是誰建的。
|
||||||
|
check_current_links() {
|
||||||
|
_miss=''; _paths=''
|
||||||
|
for _p in jsc-assist jsc-gitea; do
|
||||||
|
[ -e "$CURRENT/$_p" ] && continue
|
||||||
|
if [ -z "$_miss" ]; then _miss="$_p"; _paths="$CURRENT/$_p"
|
||||||
|
else _miss="$_miss,$_p"; _paths="$_paths、$CURRENT/$_p"; fi
|
||||||
|
done
|
||||||
|
if [ -n "$_miss" ]; then
|
||||||
|
printf 'current_links=missing:%s\n' "$_miss"
|
||||||
|
note "找不到這幾個連結:$_paths。巡檢那一輪會照這一組路徑叫工具,連結不在就叫不到,那一輪一定失敗,也不會寫心跳。請先跑 deploy 把連結建起來——本腳本不代建。"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'current_links=ok\n'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- schtasks(Windows)---
|
# --- schtasks(Windows)---
|
||||||
|
|
||||||
schtasks_install() {
|
schtasks_install() {
|
||||||
@@ -397,12 +549,12 @@ crontab_install() {
|
|||||||
|
|
||||||
if [ "$DRYRUN" -eq 1 ]; then
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
for _job in $JOBS; do
|
for _job in $JOBS; do
|
||||||
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job")"
|
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job" | mask_secret)"
|
||||||
done
|
done
|
||||||
printf 'dryrun=crontab action=write ttl=%s period=%s legacy_removed=%s others_kept=%s total_lines=%s\n' \
|
printf 'dryrun=crontab action=write ttl=%s period=%s legacy_removed=%s others_kept=%s total_lines=%s env_snapshot=%s\n' \
|
||||||
"$TTL" "$PERIOD" "$_legacy" "$_others" "$(count_lines "$_new")"
|
"$TTL" "$PERIOD" "$_legacy" "$_others" "$(count_lines "$_new")" "${SNAPSHOT_NAMES:-無}"
|
||||||
printf -- '--- 寫回後的 crontab ---\n'
|
printf -- '--- 寫回後的 crontab ---\n'
|
||||||
cat "$_new"
|
mask_secret <"$_new"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -422,10 +574,10 @@ crontab_install() {
|
|||||||
|| die 5 "別人的排程條目從 $_others 筆變成 $_kept 筆,寫回不完整。"
|
|| die 5 "別人的排程條目從 $_others 筆變成 $_kept 筆,寫回不完整。"
|
||||||
|
|
||||||
for _job in $JOBS; do
|
for _job in $JOBS; do
|
||||||
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job")"
|
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job" | mask_secret)"
|
||||||
done
|
done
|
||||||
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s log=%s\n' \
|
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s env_snapshot=%s log=%s\n' \
|
||||||
"$TTL" "$PERIOD" "$_legacy" "$_kept" "$LOG"
|
"$TTL" "$PERIOD" "$_legacy" "$_kept" "${SNAPSHOT_NAMES:-無}" "$LOG"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +606,7 @@ crontab_remove() {
|
|||||||
if [ "$DRYRUN" -eq 1 ]; then
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
printf 'dryrun=crontab action=remove jobs=%s removed=%s\n' "$JOBS" "$_hit"
|
printf 'dryrun=crontab action=remove jobs=%s removed=%s\n' "$JOBS" "$_hit"
|
||||||
printf -- '--- 寫回後的 crontab ---\n'
|
printf -- '--- 寫回後的 crontab ---\n'
|
||||||
cat "$_new"
|
mask_secret <"$_new"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -480,7 +632,7 @@ crontab_status() {
|
|||||||
printf 'mechanism=crontab service=%s ttl=%s period=%s log=%s\n' \
|
printf 'mechanism=crontab service=%s ttl=%s period=%s log=%s\n' \
|
||||||
"$(service_state)" "$TTL" "$PERIOD" "$LOG"
|
"$(service_state)" "$TTL" "$PERIOD" "$LOG"
|
||||||
for _job in heartbeat patrol; do
|
for _job in heartbeat patrol; do
|
||||||
_line=$(cron_lines_for "$_cur" "$_job")
|
_line=$(cron_lines_for "$_cur" "$_job" | mask_secret)
|
||||||
if [ -n "$_line" ]; then
|
if [ -n "$_line" ]; then
|
||||||
printf 'job=%s installed=yes entry=%s\n' "$_job" "$_line"
|
printf 'job=%s installed=yes entry=%s\n' "$_job" "$_line"
|
||||||
else
|
else
|
||||||
@@ -498,6 +650,12 @@ case "$CMD" in
|
|||||||
crontab) crontab_install ;;
|
crontab) crontab_install ;;
|
||||||
schtasks) schtasks_install ;;
|
schtasks) schtasks_install ;;
|
||||||
esac
|
esac
|
||||||
|
# 權限規則與快照提醒排在服務檢查前面:結束碼 1 那一條也是條目已經寫進去了,那台機器
|
||||||
|
# 一樣要開權限,只是還要先把 cron 服務叫起來。
|
||||||
|
print_allow_rules
|
||||||
|
check_current_links
|
||||||
|
note '上面這幾條 allow 規則要先開,排程那一輪才不會停在權限詢問——那一輪沒有人可以按同意。規則放行的是 $JSC_HOME/current 那一組路徑,巡檢也只能用那一組路徑叫工具。檔案寫入只認 Edit,Write 規則沒有作用。'
|
||||||
|
note "條目帶著安裝當下的環境變數快照(${SNAPSHOT_NAMES:-無}),其中含 Gitea 金鑰:crontab 檔案請保持只有本人讀得到。這幾個變數改過就要重跑一次 install,條目才會跟著換。"
|
||||||
[ "$DRYRUN" -eq 1 ] && exit 0
|
[ "$DRYRUN" -eq 1 ] && exit 0
|
||||||
_svc=$(service_state)
|
_svc=$(service_state)
|
||||||
if [ "$_svc" = stopped ]; then
|
if [ "$_svc" = stopped ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user