Merge pull request '釋出助理巡檢的路徑修正與先前累積的變更至 master,版本 0.1.2 升到 0.1.6' (#15) from develop into master
Reviewed-on: #15 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #15.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-assist",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-assist",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||
"skills": "./skills",
|
||||
"jsc": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
1. 不做任何要問使用者的決策。背景巡檢時靜默套預設值,等於把逐項共識整條做掉。
|
||||
2. 不參與閘門判定。閘門必須留在 hook:同步、不連網、毫秒級。助理只負責維持心跳。
|
||||
3. 不寫程式碼存取庫、不 commit、不 push、不開 PR、不合併。
|
||||
4. 監控頁只照固定三塊寫:最新一輪整塊換掉、摘要表保留近 24 輪、基本資料建頁之後不動;目錄頁只動自己那一列,別台機器的列一個字都不碰。軌跡留在摘要表,一輪一列,看得出是從哪一輪開始壞的;完整內容只留最新一輪,因為頁面要能讀完才有人讀。
|
||||
4. 監控頁只照固定三塊寫:最新一輪整塊換掉、摘要表保留近 24 輪、基本資料建頁之後不動;目錄頁一台機器一個 H2 區塊,只動自己那一個區塊,別台機器的區塊一個字都不碰。軌跡留在摘要表,一輪一列,看得出是從哪一輪開始壞的;完整內容只留最新一輪,因為頁面要能讀完才有人讀。
|
||||
5. 不刪除狀態檔、worktree 與 wiki 頁。破壞性操作留給人發動。
|
||||
6. 不自動執行自己提出的建議。建議與執行是兩件事,自動接下去等於整條流程沒人按過同意就跑完。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
|
||||
### `assistant`
|
||||
|
||||
助理主體,四個操作:`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 輪,一輪一列。目錄頁 `MONITOR_CONTENTS` 在另一個存取庫(`JSC_WIKI_REPO_CONTENTS`),只更新自己那一列,交給 `jsc-gitea/tools/wiki-contents.sh upsert` 寫,連結用絕對網址;那個存取庫沒設定時只少一列索引,這一輪照樣算跑完、照樣寫心跳。`status` 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
|
||||
助理主體,四個操作:`start` 啟動、`status` 查現況、`patrol` 跑一輪巡檢、`stop` 停止。心跳的寫入、判定與清除一律交給 `jsc-hooks` 的 `hooks/heartbeat.sh`,判定只有那一份;系統排程一律交給 `tools/schedule.sh`;一輪巡檢的流程交給 `tools/patrol.sh`。工具一律用 current 那一組不帶版本的字面絕對路徑叫,不用技能提示給的快取基底目錄。根目錄由叫用文字的 `工具根目錄=` 帶進來,那一輪自己不解——權限比對指令的字面字串,帶未展開變數或波浪號的路徑一律要核准,解路徑的指令本身在無人值守時同樣被擋。規則與指令都必須是完整字面,路徑中段寫萬用字元不匹配,所以快取那組帶版本號的路徑放不進允許清單。**心跳由巡檢寫,而且只由巡檢寫**:一輪跑完、結果寫上監控頁了,才寫那一次心跳,所以心跳新鮮等於「上一輪巡檢真的做完了」。`start` 先跑一輪巡檢,再裝上巡檢那一筆排程;巡檢週期由心跳的過期門檻算出來,兩個數字綁在一起。`patrol` 讀五項來源(使用統計、版本與重啟閘門、SDLC 階段鎖與工作包鎖、心跳自述、執行狀態事件),各項各自獨立,一項掛掉其餘各項照跑、照記,結果寫上 `MONITOR_{HASH}`:那頁固定三塊,基本資料不動、最新一輪整塊換掉、摘要表保留近 24 輪,一輪一列。目錄頁 `MONITOR_CONTENTS` 在另一個存取庫(`JSC_WIKI_REPO_CONTENTS`),一台機器一個 H2 區塊,只更新自己那一個區塊,交給 `jsc-gitea/tools/wiki-contents.sh upsert` 寫,連結用絕對網址;那個存取庫沒設定時只少一筆索引,這一輪照樣算跑完、照樣寫心跳。`status` 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
|
||||
|
||||
<!-- JSC-SKILLS:END -->
|
||||
|
||||
@@ -35,7 +35,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
| Plugin | 最低版本 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `jsc-cli` | `>=0.2.7` | CLI 偵測與委派 |
|
||||
| `jsc-gitea` | `>=0.2.0` | 監控頁的所有 wiki 讀寫,一律經 `tools/gitea.sh`;目錄頁那一列走 `tools/wiki-contents.sh upsert`,頁名雜湊走 `tools/hash-id` |
|
||||
| `jsc-gitea` | `>=0.2.0` | 監控頁的所有 wiki 讀寫,一律經 `tools/gitea.sh`;目錄頁那一個區塊走 `tools/wiki-contents.sh upsert`,頁名雜湊走 `tools/hash-id`,兩頁要放進去的連結一律先過 `tools/link-check.sh` |
|
||||
| `jsc-hooks` | `>=0.3.7` | 心跳、閘門與事件來源(`$JSC_HOME` 底下的狀態檔)。心跳的寫入、判定與清除一律走 `hooks/heartbeat.sh`,那支腳本是 `0.3.7` 才有的 |
|
||||
| `jsc-log` | `>=0.1.4` | 使用統計與工作日誌的資料來源 |
|
||||
|
||||
@@ -43,10 +43,10 @@ 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 {工作}`,只動自己那一筆,別人的排程一行都不碰。條目自己把環境帶齊:CLI 用 `command -v` 解成絕對路徑、安裝當下把 `GITEA_HOST`、`GITEA_TOKEN`、`JSC_HOME`、`JSC_ASSISTANT_HEARTBEAT_TTL` 與已設定的 `JSC_WIKI_REPO` 系列快照進條目、自帶 `JSC_GITEA_CONFIRM=yes`。`JSC_WIKI_REPO` 系列含內容頁的 `JSC_WIKI_REPO_MONITOR` 與目錄頁的 `JSC_WIKI_REPO_CONTENTS`:監控頁 `MONITOR_{HASH}` 與目錄頁 `MONITOR_CONTENTS` 分屬不同存取庫,兩支變數都要帶。名單是安裝當下從環境撈出所有已設定的,不寫死,所以新增的頁型變數自動涵蓋,這支不必跟著改——cron 的 PATH 很短、不讀設定檔、也沒有 tty。印出條目時金鑰一律遮掉,條目本身含金鑰快照,crontab 檔案要保持只有本人讀得到,變數改過要重跑一次 install。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron;也會檢查 `$JSC_HOME/current` 那組連結在不在、印出這一輪要開的 allow 規則,連結不在只警告、不代建。`--dry-run` 只印組出來的條目與寫回後的內容,什麼都不動 |
|
||||
| `tools/patrol.sh` | 一輪巡檢的收攏與收口。三個子命令:`collect` 取鎖、讀四項來源、組出監控頁的「最新一輪」與「近 24 輪摘要」兩塊、本輪的摘要列與目錄頁那一列(那一列的第一欄是連結,網址留佔位,等監控頁寫成之後由呼叫端用 `gitea.sh wiki-url` 的絕對網址換掉;第 2 欄是裸 HASH,upsert 拿那一欄當鍵);`finish` 在監控頁寫成之後才寫心跳、換上用量快照、放掉鎖;`abort` 只放掉鎖,不寫心跳。四項來源各自獨立,一項失敗其餘三項照跑,失敗那一項在頁上寫明是「這一項失敗」而不是沒資料。整輪拿一把目錄鎖,上一輪還在跑就回 4 讓開;鎖逾時(門檻取心跳門檻)會被下一輪搶回來,並在頁上記一筆。`version-guard.sh report` 回「查詢失敗」時照原字抄,不補查、不美化 |
|
||||
| `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`。`JSC_WIKI_REPO` 系列含內容頁的 `JSC_WIKI_REPO_MONITOR` 與目錄頁的 `JSC_WIKI_REPO_CONTENTS`:監控頁 `MONITOR_{HASH}` 與目錄頁 `MONITOR_CONTENTS` 分屬不同存取庫,兩支變數都要帶。名單是安裝當下從環境撈出所有已設定的,不寫死,所以新增的頁型變數自動涵蓋,這支不必跟著改——cron 的 PATH 很短、不讀設定檔、也沒有 tty。印出條目時金鑰一律遮掉,條目本身含金鑰快照,crontab 檔案要保持只有本人讀得到,變數改過要重跑一次 install。安裝當下把解好的字面根目錄寫進條目的提示文字(`工具根目錄={絕對路徑}`)並印成 `patrol_root=`:那一輪自己解不出根目錄,只能從提示文字拿,拿不到就停下回報;自訂巡檢指令沒帶這一段只警告、不中止。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron;也會檢查 `$JSC_HOME/current` 那組連結在不在、印出這一輪要開的 allow 規則,連結不在只警告、不代建。`--dry-run` 只印組出來的條目與寫回後的內容,什麼都不動 |
|
||||
| `tools/patrol.sh` | 一輪巡檢的收攏與收口。三個子命令:`collect` 取鎖、讀五項來源、組出監控頁的「最新一輪」與「近 24 輪摘要」兩塊、本輪的摘要列與目錄頁那一個區塊(區塊的 H2 標題是內容頁頁名 `MONITOR_{HASH}`,upsert 拿標題當鍵;「監控頁」那一條是連結,網址留佔位,等監控頁寫成之後由呼叫端用 `gitea.sh wiki-url` 的絕對網址換掉);`finish` 在監控頁寫成之後才寫心跳、換上用量快照、放掉鎖;`abort` 只放掉鎖,不寫心跳。各項來源各自獨立,一項失敗其餘各項照跑,失敗那一項在頁上寫明是「這一項失敗」而不是沒資料。執行狀態事件那一項由 `collect` 自己叫 `jsc-hooks/tools/report-status.sh` 排空再輪替,把非 ok 的事件與「有 start 沒有配對 end」的技能彙整成頁上那一節;`drain` 是消耗性讀取,所以只由這支跑,且它失敗一律不中止那一輪。整輪拿一把目錄鎖,上一輪還在跑就回 4 讓開;鎖逾時(門檻取心跳門檻)會被下一輪搶回來,並在頁上記一筆。`version-guard.sh report` 回「查詢失敗」時照原字抄,不補查、不美化 |
|
||||
| `references/behaviors.md` | 本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 `plugins/meta` 的 `references/guidelines.md`「技能行為清單」 |
|
||||
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本,這一頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和監控頁不同庫。一列代表一台機器,雜湊來源是 `{主機名}/{登入帳號}`,主機名取短的那一段。寫入一律走 `jsc-gitea/tools/wiki-contents.sh upsert`,比對鍵是第 2 欄的裸 HASH:**只更新自己那一列**,別台機器的列原樣保留,禁止整頁覆蓋。第一欄的連結用 `gitea.sh wiki-url` 的絕對網址(跨存取庫 `[[...]]` 連不過去),但那一格含主機位址與網址編碼,會變,所以不當鍵 |
|
||||
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本,這一頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和監控頁不同庫。版面是 H1、`>` 引言,然後一台機器一個 H2 區塊,欄位在標題底下一行一條 `- {欄位名}:{值}`,頁上不放 markdown 表格。H2 標題就是內容頁頁名 `MONITOR_{HASH}`,雜湊來源是 `{主機名}/{登入帳號}`,主機名取短的那一段。寫入一律走 `jsc-gitea/tools/wiki-contents.sh upsert`,比對鍵是 H2 標題:**只更新自己那一個區塊**,別台機器的區塊原樣保留,禁止整頁覆蓋。「監控頁」那一條的連結一律寫成 `[{頁名}]({絕對網址})`,網址取 `gitea.sh wiki-url` 印的那一個,寫入前先過 `jsc-gitea/tools/link-check.sh`、結束碼 0 才寫;但那一條含主機位址與網址編碼,會變,所以不當鍵 |
|
||||
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。頁面固定三塊:本頁基本資料建頁時寫一次就不動、最新一輪每輪整塊換掉、近 24 輪摘要一輪一列且最新的在最上面。軌跡留在摘要表,完整內容只留最新一輪,頁面才讀得完 |
|
||||
|
||||
## 助理的狀態檔
|
||||
@@ -59,7 +59,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
| `tasks/{id}` | 待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
|
||||
| `schedule.log` | 排程條目的輸出。刻意放在存取庫外面:寫進專案會多出未追蹤檔,污染別人的變更盤點 |
|
||||
| `patrol.lock/` | 一輪巡檢的鎖,是目錄——`mkdir` 是原子操作,搶不到就是別人在跑。裡面的 `info` 記 `round`、`pid`、`started` |
|
||||
| `patrol/` | 本輪巡檢的暫存檔:`latest.md` 是「最新一輪」那一塊,`summary.md` 是摘要那一塊、裡面已經放好本輪這一列,`summary-row.md` 只有那一列,`newpage.md` 是頁不存在時要建的整頁,`contents.tsv` 是目錄頁那一列 |
|
||||
| `patrol/` | 本輪巡檢的暫存檔:`latest.md` 是「最新一輪」那一塊,`summary.md` 是摘要那一塊、裡面已經放好本輪這一列,`summary-row.md` 只有那一列,`newpage.md` 是頁不存在時要建的整頁,`contents-entry.md` 是目錄頁那一個 H2 區塊 |
|
||||
| `usage-prev.tsv` | 上一輪記下來的累計用量。有了它,下一輪的「本輪次數」才算得出來;沒有它的第一輪一律寫「-」,不拿累計冒充本輪 |
|
||||
|
||||
## 相關 domain
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-assist",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
|
||||
"skills": "./skills/",
|
||||
"jsc": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+163
-53
@@ -1,44 +1,126 @@
|
||||
---
|
||||
name: assistant
|
||||
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 - and upserts its MONITOR_CONTENTS row through jsc-gitea/tools/wiki-contents.sh, which reads the separate CONTENTS wiki repo and links the monitor page by its absolute wiki-url. 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).'
|
||||
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 five independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, the heartbeat''s own report, and the status event stream that jsc-hooks/tools/report-status.sh drains and rotates, whose starts with no matching end are the only evidence an earlier skill run aborted - 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 - and upserts its MONITOR_CONTENTS entry through jsc-gitea/tools/wiki-contents.sh, which reads the separate CONTENTS wiki repo and keeps one H2 block per machine - the heading is the monitor page''s own name, the fields are bullets under it, and one of them links that page by its absolute wiki-url. Every link on either page is written as [text](URL) and is verified by jsc-gitea/tools/link-check.sh before that page is written, so a dead link stops the write instead of landing on the page. 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
|
||||
|
||||
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_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.
|
||||
`{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.
|
||||
|
||||
`$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.
|
||||
`{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.
|
||||
|
||||
`$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.
|
||||
`{CURRENT}/jsc-assist/tools/patrol.sh` owns one patrol round: taking the round lock, reading the five 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.
|
||||
|
||||
## Step 0 — take the tool root from the invocation
|
||||
|
||||
Every operation starts here, before its own step 1. **This document calls the tool root `{CURRENT}`**, and every `{CURRENT}` below is replaced by it character for character: `{CURRENT}/jsc-assist/tools/patrol.sh` is run as `/root/.jsc/current/jsc-assist/tools/patrol.sh`.
|
||||
|
||||
The root comes from outside this skill. `schedule.sh` resolves it while a person is installing the schedule, and writes it into the entry's prompt as `工具根目錄={literal absolute path}`, so the round that entry wakes reads the root out of the text that woke it and runs no command at all.
|
||||
|
||||
| Who invoked this round | Where `{CURRENT}` comes from |
|
||||
| --- | --- |
|
||||
| the schedule — an unattended round, the `patrol` whose trigger is 排程 | the path after `工具根目錄=` in the invocation text, taken verbatim. No command is run |
|
||||
| a person, in front of the terminal | the same token when the invocation carries one; otherwise `readlink -f "$JSC_HOME/current"`, run once |
|
||||
|
||||
**An unattended round that finds no root in its invocation stops there.** Report that the scheduled entry carries no `工具根目錄=` — an entry written by an older `schedule.sh` — say the fix is to run `start` again, or `jsc-assist/tools/schedule.sh install patrol` under `$JSC_HOME/current`, so the entry is rewritten with the root in it. Then take the operation's `aborted` status, write the `skill-end`, and stop.
|
||||
|
||||
Never work the root out instead. `readlink -f "$JSC_HOME/current"`, `ls -d "$JSC_HOME/current"` and every other resolve are refused in an unattended session — measured, see the table below — so running one does not produce a root, it produces a round that stops one step earlier having recorded nothing. Never fall back to `$JSC_HOME/current` as a written-out path either, never take a path from the plugin prompt or a previous transcript, and never guess.
|
||||
|
||||
**Only an attended invocation may resolve the root itself.** `readlink -f "$JSC_HOME/current"` covers the documented `~/.jsc` fallback in the same call and prints one literal absolute path. It raises one permission prompt, and a person is there to answer it once. That is the whole reason the branch exists: portability survives where somebody can approve it, and nowhere else.
|
||||
|
||||
Take the root once per invocation and reuse that one answer. Never resolve it again per call, never print it as a report line of its own, and never add a tool that prints it. Never test the root with a command either — an unattended round cannot, and the first script call is the test that matters anyway.
|
||||
|
||||
An empty token, an empty `readlink` result, a path that is not absolute, or a resolved path that is not an existing directory means there is no root to work with. **That fourth item is the one the other three wave through.** With `JSC_HOME` unset, `readlink -f "$JSC_HOME/current"` prints `/current` and exits 0 — non-empty, absolute, and past every other item — and each literal path built from it then names a place that is not there. So the attended resolve is only accepted once `[ -d "{the path just printed}" ]` says that directory exists, run in the same approved step as the resolve itself. The unattended round tests nothing, exactly as above: its root was written into the entry by whoever installed the schedule, and its first script call is what fails if that root is wrong. Report it, say `jsc-cli:deploy` has to run, take the operation's `aborted` status, and stop. Never fall back to a cache path, and never create the root here. Completion condition: one literal absolute path is in hand and every later command line carries it, or the missing root was reported and the operation stopped.
|
||||
|
||||
## Every script call carries a literal absolute path
|
||||
|
||||
**No command line in this skill carries a variable or a tilde, and the one resolve above is the single exception, allowed only when a person is watching.** Never type `$JSC_HOME`, `${JSC_HOME}` or `~` into any other command line.
|
||||
|
||||
The reason is the permission layer: it matches its rules against the command text as written, before the shell expands anything. Two properties follow from what was measured on this machine, and every rule in this skill rests on them:
|
||||
|
||||
1. **Unattended, only a full literal command that is on the allow list runs.** There is no such thing as a read-only command that is safe by default: a bare `ls -d` is refused exactly like everything else, and a refusal in a session with nobody in it is silent.
|
||||
2. **A wildcard in the middle of a path does not match.** The rule and the command both have to be complete literals. A rule holding `*` where a version number goes matches nothing, so a cache path is refused however the rule is written.
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `/root/.jsc/current/jsc-assist/tools/patrol.sh` — a literal rule for it is on the allow list | ran |
|
||||
| `$JSC_HOME/current/jsc-assist/tools/patrol.sh` | refused |
|
||||
| `~/.jsc/current/jsc-assist/tools/patrol.sh` | refused |
|
||||
| `readlink -f "$JSC_HOME/current"` | refused |
|
||||
| `ls -d "$JSC_HOME/current"` | refused |
|
||||
| `ls -d /root/.jsc/current` — literal, read-only, no rule for it | refused |
|
||||
| `/root/.claude/plugins/cache/jsc/jsc-assist/0.1.0/tools/patrol.sh` — rule written with `*` for the version segment | refused |
|
||||
|
||||
A literal allow rule that itself starts with `$JSC_HOME` was added to the settings file and the same call was still refused, so no permission rule makes the variable form work either. The literal path is the whole fix, on both sides.
|
||||
|
||||
**These rules outrank portability, and the next maintainer is the one who has to know why.** A variable in the path reads as the portable choice and costs nothing while a person is watching: the prompt appears, somebody approves it, the round carries on. The scheduled round has nobody to approve it. It stops at its first script call, records nothing, writes no heartbeat, and the machine then reads as a stopped assistant with no trace of the refusal anywhere. And the resolve is no way out of that, because row 4 of the table is the resolve itself: a round that cannot run a script cannot run the command that would have told it which script to run. That is why the root is handed in by whoever installed the schedule, and why anything written into a command line here is already literal.
|
||||
|
||||
## 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:
|
||||
Every tool below is addressed through `{CURRENT}/{plugin}`, with `{CURRENT}` standing for the literal path step 0 took:
|
||||
|
||||
| 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` |
|
||||
| the `MONITOR_CONTENTS` row | `$JSC_HOME/current/jsc-gitea/tools/wiki-contents.sh` |
|
||||
| one patrol round | `{CURRENT}/jsc-assist/tools/patrol.sh` |
|
||||
| the system scheduler | `{CURRENT}/jsc-assist/tools/schedule.sh` |
|
||||
| the heartbeat | `{CURRENT}/jsc-hooks/hooks/heartbeat.sh` |
|
||||
| the status event stream | `{CURRENT}/jsc-hooks/tools/report-status.sh` |
|
||||
| the wiki, through `jsc-gitea:wiki` | `{CURRENT}/jsc-gitea/tools/gitea.sh` |
|
||||
| the `MONITOR_CONTENTS` entry | `{CURRENT}/jsc-gitea/tools/wiki-contents.sh` |
|
||||
| the link check every write depends on | `{CURRENT}/jsc-gitea/tools/link-check.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, and the directory row depends on `wiki-contents.sh` carrying one too — without them the round is refused locally, before any request leaves the machine, and the page never gets written.
|
||||
**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, the directory entry depends on `wiki-contents.sh` carrying one too, and both writes depend on `link-check.sh` carrying one — without them the round is refused locally, before any request leaves the machine, and the 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 five 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`.
|
||||
**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 seven 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.
|
||||
Both scripts check this for themselves: run from anywhere outside `{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.
|
||||
|
||||
## Two rules bind every link this skill writes
|
||||
|
||||
Both pages this round writes carry links, and both rules below hold for every one of them — the monitor page and the directory entry alike.
|
||||
|
||||
**Rule A — a link is always written as `[{text}]({URL})`.** The wiki's own `[[page]]` and `[[text|page]]` forms are not used here at all, and neither is the split between "same repo" and "cross repo" writing. The URL comes from `{CURRENT}/jsc-gitea/tools/gitea.sh wiki-url {repo} {page}`; never assemble a path by hand. `[[...]]` resolves only inside the wiki it sits in: the monitor page and the directory page live in two different repos, so a `[[MONITOR_{HASH}]]` written into the directory entry renders as an ordinary-looking link that goes nowhere, and nothing reports it.
|
||||
|
||||
**Rule B — a link is verified before it is written, never after.** Collect every link that is about to go into the page, hand the whole set to `{CURRENT}/jsc-gitea/tools/link-check.sh`, and write only on exit 0. The script prints one `{OK|DEAD|SKIP}<TAB>{URL}<TAB>{note}` line per URL and checks Gitea URLs through the API, never through the web status code — a private repo answers 404 to a logged-out web request, so a status-code check condemns live pages.
|
||||
|
||||
| Exit | Meaning | Do |
|
||||
| --- | --- | --- |
|
||||
| 0 | every link resolves | write the page |
|
||||
| 1 | at least one link is dead | write nothing, report the `DEAD` lines verbatim, and take the step's own abort row |
|
||||
| 2 | usage error — no URL was given | a defect in the call: pass the URLs and run it once more |
|
||||
| 3 | a Gitea URL is in the list but `GITEA_HOST` is unset | report the variable and stop; never skip the check to get the write through |
|
||||
| 7 | Gitea authentication failed (401, 403) | stop and report it as a token problem, never as dead links — an expired token makes live private pages look missing |
|
||||
|
||||
A round with no link to write skips the call and says so; a round that cannot verify writes nothing.
|
||||
|
||||
## 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.
|
||||
|
||||
## Every operation ends with one status event
|
||||
|
||||
The last thing any of the four operations does, after its report is printed, is write its own end event:
|
||||
|
||||
`{CURRENT}/jsc-hooks/tools/report-status.sh skill-end jsc-assist:assistant {status} {exit} "{one line}"`
|
||||
|
||||
The `start` half is already on record — a hook writes it when this skill loads — so this call is what tells the difference between an operation that finished and one that stopped half way. **Skipping it makes this skill's own run look aborted**, and the next patrol round reports it as such, on the page this skill writes. Pick the status from what actually happened:
|
||||
|
||||
| status | When |
|
||||
| --- | --- |
|
||||
| `ok` | the operation reached its own completion condition |
|
||||
| `blocked` | the round stood down because another round holds the lock, or `install heartbeat` was refused — nothing was done and nothing is wrong |
|
||||
| `failed` | a step returned a code that stopped the operation: `collect` exit 5 or 6, a monitor-page write that could not be made, `remove` non-zero in `stop` |
|
||||
| `degraded` | the operation finished with a known gap: the directory entry was left unwritten, or the schedule entry was installed but the cron service is stopped |
|
||||
| `aborted` | the operation stopped because a precondition did not hold, such as an empty `hash=` or a missing `current` link |
|
||||
|
||||
The call never changes the outcome: it returns 0 even when it cannot write, and a non-zero from it is reported as a defect in the reporting chain, never as a failure of the operation that just succeeded. Completion condition for all four operations: exactly one `skill-end` was written, and its status matches the outcome that was reported.
|
||||
|
||||
## Data sources
|
||||
|
||||
| Path | Read by | Format |
|
||||
@@ -47,8 +129,10 @@ 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/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/` | `patrol.sh` only | one round's scratch files, including `latest.md`, `summary.md`, `summary-row.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-entry.md` |
|
||||
| `$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/usage/events.jsonl` | `report-status.sh` only, never this skill and never `patrol.sh` by hand | one JSON object per line: `ts`, `cli`, `session`, `kind`, `name`, `phase`, `status`, `exit`, optional `ms` and `detail` |
|
||||
| `$JSC_HOME/assistant/events-open.tsv` | `patrol.sh` only | the starts still waiting for a matching end, carried from round to round: `session`, `name`, `kind`, first-seen epoch, the event's own `ts` |
|
||||
|
||||
`$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.
|
||||
|
||||
@@ -70,7 +154,7 @@ Every call in every operation below is judged by this table. Report the code you
|
||||
|
||||
## The scheduler
|
||||
|
||||
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`:
|
||||
Nothing in a background assistant runs on its own. The system scheduler is what makes it periodic, and `{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` |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -90,11 +174,12 @@ Four properties of that script matter enough to state here, because a report tha
|
||||
- **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 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*` — `JSC_WIKI_REPO`, `JSC_WIKI_REPO_MONITOR` for the monitor page and `JSC_WIKI_REPO_CONTENTS` for the directory page, which the script picks up from the environment rather than from a hardcoded list), 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.
|
||||
- **`install` prints the permission rules that round needs.** One `allow_rule=` line each, every path a full literal under `current` — no variable, no tilde, and no wildcard inside the path, because a rule holding one matches nothing. 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.
|
||||
- **`install` writes the tool root into the entry.** The round it schedules cannot resolve the root for itself, so `schedule.sh` puts the literal path into the entry's prompt as `工具根目錄={path}` and prints the same value as `patrol_root=`. Report that value, and treat any hand-edit of the entry that drops it as breaking every future round: from then on each one stops at step 0 with nothing recorded.
|
||||
|
||||
### What a fresh heartbeat actually proves
|
||||
|
||||
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:
|
||||
The heartbeat is written in exactly one place: `{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 any task in the book moved.** The task rows — `last_run`, `next_run`, `fail_count` — are the only evidence about work.
|
||||
@@ -108,11 +193,24 @@ 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 |
|
||||
| 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_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 |
|
||||
| 2 | `{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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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, that CLI's executable is not on `PATH` so no absolute path can be written, or `JSC_HOME` does not resolve to an absolute path so no literal root can go into the entry | 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 |
|
||||
|
||||
## The status event stream — the fifth source
|
||||
|
||||
`$JSC_HOME/usage/events.jsonl` is where every skill and every hook records how its run ended. A hook writes a skill's `start` for free; the matching `end` can only be written by the skill itself, in its own closing step. **So a `start` with no matching `end` is an aborted run, and it is the only evidence of one that exists anywhere.** That is what this source is for; the counts around it are secondary.
|
||||
|
||||
`{CURRENT}/jsc-assist/tools/patrol.sh collect` owns the whole of it — it calls `{CURRENT}/jsc-hooks/tools/report-status.sh drain`, then `rotate`, then does the pairing, and writes the 執行狀態事件 subsection into `latest_file`. **Never run `drain` from this skill.** Four properties make that the only safe arrangement, and each one is a way to lose events:
|
||||
|
||||
- **`drain` is a consuming read.** It prints everything written since the last drain and then moves the offset in `$JSC_HOME/usage/scan-state/events.offset`. The same events never come back. Read into a transcript instead of a file, they are one dropped line away from gone; a second `drain` in the same round returns exit 3 and the first drain's events are already spent.
|
||||
- **Exit 3 means there were no new events, and that is a normal round, not a failure.** Most rounds have nothing new. The script also uses 3 when the stream file does not exist yet.
|
||||
- **`rotate` has to follow `drain` immediately.** It renames the stream and resets the offset once the file passes its size limit, so anything appended between the two calls is moved aside without ever being drained. One script, one run, smallest possible gap.
|
||||
- **Pairing is by `session` plus `name`, and it carries across rounds.** Five CLIs run the same skill in different sessions at once, so `name` alone lets one session's `end` cancel another session's `start`. And a round lasts about two minutes while a skill run can last much longer, so an unmatched `start` is held in `$JSC_HOME/assistant/events-open.tsv` and matched against later rounds. It is reported as an abort only once it has stayed open longer than the heartbeat TTL; below that it counts as still running.
|
||||
|
||||
Neither `drain` nor `rotate` may stop a round. A missing `report-status.sh`, a `drain` that returns anything other than 0 or 3, and a failed `rotate` each mark this one item failed or add one 警示來源 — exactly like the other four sources — and the round carries on to record itself. The reporting chain breaking must never break the round that is doing the reporting.
|
||||
|
||||
## patrol.sh exit codes
|
||||
|
||||
@@ -120,10 +218,10 @@ One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3
|
||||
|
||||
| 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 five 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 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_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 |
|
||||
| 2 | `finish`: `{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 `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {id}` to release the lock |
|
||||
| 3 | `collect`: all five items failed | **Write the page anyway**, with verdict 異常. A page listing five 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 |
|
||||
| 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 |
|
||||
| 6 | Usage error — an unknown subcommand, a missing `--round`, or an option with no value | A defect in the call. Correct it and run it once more; report a second exit 6 as a defect in this skill and stop |
|
||||
@@ -133,8 +231,8 @@ One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3
|
||||
The six limits in `AGENTS.md`「助理的界線」 hold for all four operations. Four of them need saying out loud here:
|
||||
|
||||
- **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 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 directory page is a separate write in a separate wiki repo, and `wiki-contents.sh` does it: this machine's row is 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 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`. A command that is not on the allow list is a question too — the permission prompt is one, and it is the one nobody sees — which is why step 0 hands that round its root instead of letting it resolve one. 界線 1.
|
||||
- **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 directory page is a separate write in a separate wiki repo, and `wiki-contents.sh` does it: that page keeps one H2 block per machine, and this machine's block is the only one that is updated. 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.
|
||||
- **`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`.
|
||||
|
||||
@@ -144,35 +242,37 @@ 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.
|
||||
|
||||
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.
|
||||
That property holds only while nothing fakes a heartbeat. **`write` is called by `{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` proves the loop works before it schedules it: one patrol round first, then the scheduled entry. It installs no daemon and writes no bare heartbeat.
|
||||
|
||||
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 write of the monitor page, a directory-row failure other than exit 3, 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 write of the monitor page, a directory-entry failure other than exit 3, 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_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.
|
||||
2. **Confirm the heartbeat.** Run `{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 `$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.
|
||||
3. **Install the patrol entry.** Run `{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=`, `patrol_root=`, 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, the tool root the entry carries 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 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:
|
||||
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, the `patrol_root=` the entry carries — that is what every later round reads its tool root from — 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 |
|
||||
| --- | --- |
|
||||
| exit 0 | 助理已啟動,第一輪巡檢跑完了,結果寫上監控頁了,心跳也寫了。排程接上了,之後每 {period} 分鐘跑一輪,每一輪跑完才寫一次心跳。心跳新鮮代表上一輪巡檢真的做完了;那一輪四項有沒有全過,看監控頁的本輪判定。 |
|
||||
| exit 0 | 助理已啟動,第一輪巡檢跑完了,結果寫上監控頁了,心跳也寫了。排程接上了,之後每 {period} 分鐘跑一輪,每一輪跑完才寫一次心跳。心跳新鮮代表上一輪巡檢真的做完了;那一輪各項有沒有全過,看監控頁的本輪判定。 |
|
||||
| exit 1 | 助理已啟動,第一輪巡檢跑完了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 `sudo service cron start`,重開 WSL 之後要再跑一次。 |
|
||||
| 其他結束碼 | 助理已啟動,第一輪巡檢跑完了,但排程沒接上(結束碼 {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, the scheduler outcome, the allow rules and the snapshot reminder, 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 tool root the entry carries, the allow rules and the snapshot reminder, and exactly one notice above appears with the real numbers.
|
||||
|
||||
## 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 five 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 `$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.
|
||||
1. **Collect.** Run `{CURRENT}/jsc-assist/tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). That is the same split step 0 branched on: 排程 is the unattended round that read its root out of the invocation text, 手動 the round somebody asked for. 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 `$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.
|
||||
**The status event lines come out of the same call.** `collect` drained the stream and rotated it (see 「The status event stream」 above), so record `events_total=`, `events_bad=`, `events_unpaired=`, `events_running=`, `events_rotated=` and `events_file=` alongside the rest, and read `item=D-11` for whether that source was readable at all. The 執行狀態事件 subsection of `latest_file` already carries the two detail tables — the non-`ok` events and the starts with no matching end — so never rebuild either by hand and never call `report-status.sh` yourself: a second `drain` this round would either return exit 3 or eat events that then reach no page at all.
|
||||
|
||||
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 `{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, and never work the hash out by hand** — a hand-made name lands the content on a page nobody reads. `hash-id` hashes `{host}/{user}` and prints the full 40-character uppercase hexadecimal SHA-1: no truncation to 8, no `H` prefix, and an empty input exits 2 rather than hashing the empty string. So `page=` is either `MONITOR_` plus that 40-character string, exactly as the script printed it, or nothing at all. Completion condition: `page=` holds a `MONITOR_{HASH}` name taken verbatim from `collect`, or the abort ran and the round was reported as unrecorded.
|
||||
|
||||
@@ -182,41 +282,51 @@ One round: read four sources, record the result, then beat. Everything before th
|
||||
| --- | --- |
|
||||
| 本頁基本資料 | 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 |
|
||||
| 近 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 |
|
||||
|
||||
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**, and that verdict belongs to this step alone: the round's result lives on this page, so a repo this step cannot resolve leaves the round with nowhere to be recorded. Step 4 is judged on its own terms. 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.
|
||||
**Verify the page's links before the write.** List every link the rebuilt body carries — the ones the latest-round block brought in, and any that survived in the block carried over from the old page — and run `{CURRENT}/jsc-gitea/tools/link-check.sh` over the whole list. Exit 0 is the only result that permits the write. On exit 1 report the `DEAD` lines verbatim, then run `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}` and stop: a round that writes a dead link records a false trail nobody can follow back. Exits 2, 3 and 7 take the same abort, each reported by the rule B table above. A body carrying no link at all needs no call — say so in the report rather than claiming a check that never ran.
|
||||
|
||||
4. **Update this machine's row in `MONITOR_CONTENTS`, through `jsc-gitea/tools/wiki-contents.sh`.** That page is a directory every machine writes to, and it lives in the repo `gitea.sh wiki-repo CONTENTS` resolves — `JSC_WIKI_REPO_CONTENTS`, then `JSC_WIKI_REPO`, then exit 3, and never a fallback to `JSC_WIKI_REPO_MONITOR`. The script owns the read-match-write of one row, so never read this page and rebuild it by hand, never write it through `jsc-gitea:wiki`, and never rebuild it the way step 3 rebuilds the content page — every other row here belongs to a machine that is not this one, and one careless whole-page write deletes their records.
|
||||
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 `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. **No record, no heartbeat**, and that verdict belongs to this step alone: the round's result lives on this page, so a repo this step cannot resolve leaves the round with nowhere to be recorded. Step 4 is judged on its own terms. Completion condition: `link-check.sh` exited 0 over the body's links or the body carried none, 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.
|
||||
|
||||
**Finish the row first.** The `row=` line in `contents_file` carries the placeholder `{監控頁絕對網址}` in its first cell, because the directory page and the monitor page now sit in two different wikis: `[[MONITOR_{HASH}]]` resolves only inside one wiki and would dead-link from here while still looking like a link, and the absolute URL cannot be known until step 3 has actually put the page. Run `$JSC_HOME/current/jsc-gitea/tools/gitea.sh wiki-url {the MONITOR repo step 3 resolved} MONITOR_{HASH}`, replace the placeholder with what it prints, and write the finished row to a file. Exit 4 there means step 3's write has not landed — go back to step 3 rather than writing a row. Exit 5 means the page carries no `html_url`: report it and never assemble a URL by hand. Exit 7 or 8: report the code and take the abort row below. **Any other non-zero exit takes the same abort row**, a missing argument included — a URL that never arrived would otherwise leave the link cell holding the raw placeholder, and the row would still be written.
|
||||
4. **Update this machine's block in `MONITOR_CONTENTS`, through `jsc-gitea/tools/wiki-contents.sh`.** That page is a directory every machine writes to, and it lives in the repo `gitea.sh wiki-repo CONTENTS` resolves — `JSC_WIKI_REPO_CONTENTS`, then `JSC_WIKI_REPO`, then exit 3, and never a fallback to `JSC_WIKI_REPO_MONITOR`. The page carries no table: it is an H1, a `>` preamble, and then one H2 block per machine — the heading is that machine's monitor page name, and the fields are one `- {name}:{value}` bullet each underneath. The script owns the read-match-write of one block, so never read this page and rebuild it by hand, never write it through `jsc-gitea:wiki`, and never rebuild it the way step 3 rebuilds the content page — every other block here belongs to a machine that is not this one, and one careless whole-page write deletes their records.
|
||||
|
||||
**Finish the block first.** `contents_file` holds this machine's whole block — `## MONITOR_{HASH}`, a blank line, then the bullets — and its 監控頁 bullet already carries the rule A shape `[{page name}]({URL})` with the placeholder `{監控頁絕對網址}` standing in for the URL, because the absolute URL cannot be known until step 3 has actually put the page. Run `{CURRENT}/jsc-gitea/tools/gitea.sh wiki-url {the MONITOR repo step 3 resolved} MONITOR_{HASH}`, replace the placeholder with what it prints, and write the finished block to a file. Exit 4 there means step 3's write has not landed — go back to step 3 rather than writing a block. Exit 5 means the page carries no `html_url`: report it and never assemble a URL by hand. Exit 7 or 8: report the code and take the abort row below. **Any other non-zero exit takes the same abort row**, a missing argument included — a URL that never arrived would otherwise leave that bullet holding the raw placeholder, and the block would still be written.
|
||||
|
||||
**Then verify that URL before the block goes anywhere.** Run `{CURRENT}/jsc-gitea/tools/link-check.sh {the URL just substituted}` and read the exit code by the rule B table above. Exit 0 is the only result that permits the upsert. On exit 1 the directory would gain a block pointing at a page that is not there: report the `DEAD` line verbatim, write no block, and treat the directory entry as not updated — the round's own result is already on `MONITOR_{HASH}`, so carry on to step 5 and write the heartbeat, exactly as exit 3 from the upsert does, and put the dead link into the 待人處理 rows. Exits 2, 3 and 7 are reported the same way and the block is left unwritten. Never write the block first and check afterwards: the directory is what other people read to find this machine, and a dead link there sends every one of them to a page that does not exist.
|
||||
|
||||
Then run, with the template as the fifth argument every time:
|
||||
|
||||
`$JSC_HOME/current/jsc-gitea/tools/wiki-contents.sh upsert MONITOR 2 "{HASH}" {row file} $JSC_HOME/current/jsc-assist/templates/monitor-contents.md`
|
||||
`{CURRENT}/jsc-gitea/tools/wiki-contents.sh upsert MONITOR 1 "MONITOR_{HASH}" {block file} {CURRENT}/jsc-assist/templates/monitor-contents.md`
|
||||
|
||||
**The key is column 2, the bare `HASH` cell** — the 40-character string `collect` printed as `hash=`, copied verbatim, with no link, no brackets and no URL around it. The script compares the whole cell text, so column 1 cannot be the key: that cell holds `GITEA_HOST` and the wiki's encoding of the page name, so a changed host, a `JSC_WIKI_REPO_MONITOR` pointed at another repo, or a different URL encoding changes the text and stops it matching. This page is written once every round, so from that moment on every round appends one more row for this same machine and the old row is never updated again. The bare `HASH` depends on `{host}/{user}` alone, which none of those three touch. Column 1's link stays in the row for people to click, and never for matching. A key typed by hand matches nothing either, and appends the same duplicate row.
|
||||
**The key is the H2 heading — the page name `MONITOR_{HASH}`**, taken from `collect`'s `page=` line verbatim, with no link, no brackets and no URL around it. The script compares the heading text, so the 監控頁 bullet cannot be the key: it holds `GITEA_HOST` and the wiki's encoding of the page name, so a changed host, a `JSC_WIKI_REPO_MONITOR` pointed at another repo, or a different URL encoding changes that text and stops it matching. This page is written once every round, so from the moment matching breaks every round appends one more block for this same machine and the old block is never updated again. The page name depends on `{host}/{user}` alone, which none of those three touch. That bullet's link stays in the block for people to click, and never for matching. A key typed by hand matches nothing either, and appends the same duplicate block.
|
||||
|
||||
**The `1` is the third argument, `key-col`, and it only matters while an old page is still a table.** A directory page written in the previous format holds a markdown table, and the script converts the whole page to blocks before it upserts; `key-col` tells it which column of that table held the identity, counting from 1. Column 1 of this page's old table was the monitor-page link, whose cell is `[MONITOR_{HASH}]({URL})`, and the conversion takes the text out of it as the H2 heading. Once the page is in the block format the argument is ignored — pass `1` regardless, and never a column number worked out from the current page.
|
||||
|
||||
| Exit | Do |
|
||||
| --- | --- |
|
||||
| 0 | The row is in place. The script prints `updated` or `added` plus the page it wrote — carry that word into the report, and carry on to step 5 |
|
||||
| 1 | The write failed, or the directory page holds no markdown table. Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop |
|
||||
| 0 | The block is in place. The script prints `updated` or `added` plus the page it wrote — carry that word into the report, and carry on to step 5 |
|
||||
| 1 | The page content could not be built, or the write failed. Run `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. A page with no matching block is not this code: an unmatched key is an append |
|
||||
| 2 | An argument was rejected and nothing was written. A template path that does not exist lands here too, and means the plugin installation is incomplete. Correct the call and run it once more; report a second exit 2 as a defect in this skill, then abort and stop |
|
||||
| 3 | No `CONTENTS` wiki repo is configured. **This one does not stop the round.** Carry on to step 5 and write the heartbeat: the round's result is already on `MONITOR_{HASH}`, and that is exactly what a heartbeat stands for. Report the directory row as not updated, name `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set, and add that to the 待人處理 rows. Never abort a recorded round over the directory page — a missing directory row loses one index line, an aborted round loses the whole round, and the patrol cannot ask anybody for the missing setting |
|
||||
| 3 | No `CONTENTS` wiki repo is configured. **This one does not stop the round.** Carry on to step 5 and write the heartbeat: the round's result is already on `MONITOR_{HASH}`, and that is exactly what a heartbeat stands for. Report the directory entry as not updated, name `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set, and add that to the 待人處理 rows. Never abort a recorded round over the directory page — a missing directory block loses one index entry, an aborted round loses the whole round, and the patrol cannot ask anybody for the missing setting |
|
||||
| 4 | The page is absent and no template reached the script. The call above always passes the template as its fifth argument, so this code cannot come out of it — getting it means that argument was dropped, so restore it and run the call once more. A template path that does not exist is rejected as exit 2, never as 4 |
|
||||
| 7 | The token is invalid or lacks permission, so the other machines' rows are unknown. The script wrote nothing, which is what keeps those rows alive. Abort, report the key problem, and stop |
|
||||
| 7 | The token is invalid or lacks permission, so the other machines' blocks are unknown. The script wrote nothing, which is what keeps those blocks alive. Abort, report the key problem, and stop |
|
||||
| 8 | Some other API failure. Abort, report the status, and stop |
|
||||
|
||||
Completion condition: the script exited 0 and exactly one row carries this machine's bare `HASH` in column 2 with this round's values, or exit 3 was reported as an unwritten directory row and the round carried on, or one of the other non-zero codes — `wiki-url`'s included — was reported after the abort ran.
|
||||
Completion condition: `link-check.sh` exited 0 over the block's URL and the script exited 0 with exactly one `## MONITOR_{HASH}` block on the page carrying this round's values, or exit 3 from the upsert or a non-zero `link-check.sh` was reported as an unwritten directory entry and the round carried on, or one of the other non-zero codes — `wiki-url`'s included — was reported after the abort ran.
|
||||
|
||||
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.
|
||||
5. **Write the heartbeat.** Run `{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 directory row as `updated`, `added`, or not written with the exit code and the reason; 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.
|
||||
6. **Report the round.** Print the round verdict and, when it is `警示`, the `warn_sources=` text that says why — a round can read all five 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, the link-check verdict for each of the two writes — passed, skipped for a body with no link, or refused with its exit code and its `DEAD` lines — and the directory entry as `updated`, `added`, or not written with the exit code and the reason; 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.
|
||||
|
||||
**The event numbers get their own line, and the unpaired starts get their own list.** Print `events_total=` and `events_bad=` as this round's event count and its non-`ok` count, then every non-`ok` event with its `kind`, `name`, `status`, `exit` and `detail`, then — separately, never folded into the same list — every start with no matching end, by `name` and `session`. A non-zero `events_unpaired=` is the round's most important finding: each row is a skill run that started and never reached its closing step. Say `events_rotated=` too when it is `rotated` or `failed`. When `item=D-11` failed, say the source could not be read rather than reporting zero events — zero read events and zero existing events look identical in a report and mean opposite things.
|
||||
|
||||
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 five items appear in the report, the event count, the non-`ok` count and the unpaired starts are stated, the heartbeat outcome is stated as written or not written, and no suggestion in 待人處理 was acted on.
|
||||
|
||||
## status
|
||||
|
||||
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_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.
|
||||
1. **Read the heartbeat through the script.** Run `{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.
|
||||
|
||||
@@ -229,7 +339,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.
|
||||
|
||||
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.
|
||||
3. **Read the schedule.** Run `{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.
|
||||
|
||||
@@ -237,7 +347,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
||||
|
||||
| Heartbeat | Schedule | Say |
|
||||
| --- | --- | --- |
|
||||
| 新鮮 | patrol installed, service running | 上一輪巡檢跑完了,結果也記上監控頁了,排程還在跑。那一輪四項有沒有全過,要看監控頁的本輪判定 |
|
||||
| 新鮮 | patrol installed, service running | 上一輪巡檢跑完了,結果也記上監控頁了,排程還在跑。那一輪各項有沒有全過,要看監控頁的本輪判定 |
|
||||
| 新鮮 | not installed, or service stopped | 上一輪巡檢跑完了,但沒有排程在叫下一輪,過了 TTL 心跳就會過期 |
|
||||
| 過期 or 不存在 | patrol installed, service running | 排程裝著卻沒有新的心跳,巡檢自己跑失敗了,去看 `$JSC_HOME/assistant/schedule.log` 與監控頁的最新一輪 |
|
||||
| any | `heartbeat` job installed | 舊版的心跳排程還留著,它會蓋掉「心跳等於巡檢跑完」這件事。請跑一次 `start`,或 `schedule.sh install patrol` 把它清掉 |
|
||||
@@ -250,11 +360,11 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
||||
|
||||
## stop
|
||||
|
||||
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.
|
||||
1. **Record what is being stopped.** Run `{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 `$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.
|
||||
2. **Remove the schedule first.** Run `{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_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.
|
||||
3. **Clear the heartbeat.** Run `{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:
|
||||
|
||||
|
||||
@@ -1,54 +1,76 @@
|
||||
# 助理巡檢目錄
|
||||
|
||||
> 由 `jsc-assist` 維護。這是目錄頁 `MONITOR_CONTENTS`,落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和監控頁不同庫。
|
||||
> 一列代表一台機器。雜湊來源是 `{主機名}/{登入帳號}`,主機名取短的那一段,所以一台機器一列、一頁,換一支 CLI 不另開列。
|
||||
> 一個區塊代表一台機器。雜湊來源是 `{主機名}/{登入帳號}`,主機名取短的那一段,所以一台機器一個區塊、一頁,換一支 CLI 不另開區塊。
|
||||
> `MONITOR_{HASH}` 的 `{HASH}` 執行 `jsc-gitea/tools/hash-id {主機名}/{登入帳號}` 取得,原樣採用它印出的完整 40 碼大寫十六進位,不截短、不加前綴(共用 wiki hash 規則,演算法見 `jsc-meta` 的 `references/guidelines.md`)。
|
||||
>
|
||||
> 連結寫法:監控頁那一欄放 `jsc-gitea/tools/gitea.sh wiki-url` 印出的絕對網址,不用 `[[...]]`。兩頁分屬不同存取庫,`[[...]]` 連不過去,畫面上還看不出壞掉。
|
||||
> 版面固定三段:H1 頁名、這一段引言,然後每一台機器一個 H2 區塊。H2 標題就是那一台機器的內容頁頁名 `MONITOR_{HASH}`,標題不放連結、不放網址、不加前後綴、不加日期。欄位一行一條,格式 `- {欄位名}:{值}`,順序照這段引言的「欄位說明」從上到下。H2 與第一條之間空一行,區塊之間空一行。這一頁不放 markdown 表格。
|
||||
>
|
||||
> 比對鍵:第 2 欄的裸 HASH,純文字,不帶連結、不帶網址。連結那一欄是給人看的,不當鍵。
|
||||
> 連結寫法:一律寫成 `[{文字}]({絕對網址})`,網址取 `jsc-gitea/tools/gitea.sh wiki-url` 印出的那一個,不自己組路徑。wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常文字或死連結。
|
||||
>
|
||||
> 寫入前驗證:這一個區塊要放進去的連結,先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才寫。有 DEAD 就不寫這一個區塊,把連不到的那幾筆回報出去。驗證走 API,不看網頁狀態碼——私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把還在的頁判成死連結。
|
||||
>
|
||||
> 比對鍵:H2 標題,也就是內容頁頁名 `MONITOR_{HASH}`,純文字,不帶連結、不帶網址。「監控頁」那一條的連結是給人點的,不當鍵。
|
||||
>
|
||||
> 欄位說明:條列的順序就是下面這幾條從上到下的順序,一條都不能少。鍵在 H2 標題出現過,「監控頁」與「HASH」照樣各留一條,資料才不會少。
|
||||
>
|
||||
> - **監控頁**:指向 `MONITOR_{HASH}` 的連結,寫成 `[{頁名}]({絕對網址})`,給人點的,不當比對鍵。少了它就要人自己算雜湊才翻得到內容頁;絕對網址在哪一個存取庫都連得過去,寫入前也驗得起來。
|
||||
> - **HASH**:`hash-id` 印出的完整 40 碼大寫十六進位,純文字,不加連結、不加網址。這一條只跟 `{主機名}/{登入帳號}` 有關,換主機位址、換存取庫、換一種網址編碼都不會變。H2 標題就是 `MONITOR_` 接上這一串,所以這一條也是標題的來源。
|
||||
> - **主機**:這台機器的短主機名,與雜湊第一段相同。一眼看出這一個區塊是哪一台機器。
|
||||
> - **帳號**:助理執行時的登入帳號,與雜湊第二段相同。同一台機器換帳號就是另一個巡檢對象,雜湊也會不同。
|
||||
> - **心跳**:巡檢當下(本輪寫入前)的心跳判定,判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。一眼看出這台機器上一輪巡檢有沒有跑完,不必逐頁翻。
|
||||
> - **最後巡檢**:該頁最新一輪的時間戳。心跳由巡檢寫,兩條理當一致;差很多就代表有一輪寫了心跳卻沒寫頁,那是缺陷。
|
||||
> - **待辦筆數**:待辦簿現有筆數。心跳新鮮而筆數為 0,代表助理空轉,沒有東西可跑。
|
||||
> - **連續失敗項**:待辦簿裡 `fail_count` 大於 0 的筆數。待辦簿的項目失敗不會自動暫停,每輪都重試,這一條讓壞掉的項目在目錄頁就現形。
|
||||
>
|
||||
> 為什麼沒有「本輪非 ok 事件數」這一條:執行狀態事件的筆數只放在監控頁的「執行狀態事件」那一節,這一頁不加條。兩個理由:
|
||||
>
|
||||
> - **同一頁上會出現兩種欄位組合。** 每一個區塊由那一台機器自己那一輪寫,別台機器的區塊要到它下一輪才會重寫。新加一條,只有跑到新版的機器寫得出來,其餘機器的區塊還是舊的那幾條,而讀的人分不出「這台機器本輪沒有非 ok 事件」與「這台機器的版本還沒寫這一條」。目錄頁也沒有整頁改寫的路可以走:整頁覆蓋等於刪掉別台機器的紀錄。
|
||||
> - **這個數字離開監控頁就會被讀錯。** 它算的是「上一次排空之後到這一輪之間」的事件,視窗長度隨巡檢週期與上一輪的成敗變動。放在監控頁上,同一節裡就有事件總數、未配對的 `start` 與明細表可以對照;抽一個數字放到目錄頁,0 會被讀成「這台機器很健康」,但它同樣可能只是那一段時間沒有任何技能跑過。
|
||||
>
|
||||
> 要判斷一台機器有沒有問題,這一頁上的「心跳」與「最後巡檢」就夠帶人往下翻;細節一律回監控頁看。
|
||||
>
|
||||
> 寫入規則:這一頁是共用目錄,別台機器的區塊一律原樣保留。寫入一律用 `jsc-gitea/tools/wiki-contents.sh upsert`,不手工改頁。
|
||||
>
|
||||
> ```mermaid
|
||||
> flowchart TD
|
||||
> A[監控頁已經寫成] --> B[gitea.sh wiki-url 取監控頁絕對網址]
|
||||
> B --> C[組出本機那一個區塊,網址換掉佔位]
|
||||
> C --> V{link-check.sh 驗這一個區塊的連結}
|
||||
> V -- 結束碼 0 --> D[wiki-contents.sh upsert MONITOR,H2 標題 MONITOR_HASH 當鍵]
|
||||
> V -- 有 DEAD 或其他非 0 --> W[不寫這一個區塊,回報連不到的那幾筆]
|
||||
> D --> E{舊頁讀得回來}
|
||||
> E -- 是 --> F{找得到同名的 H2 標題}
|
||||
> F -- 是 --> G[整塊換掉那一個區塊]
|
||||
> F -- 否 --> H[附加一個區塊到頁尾]
|
||||
> E -- 頁不存在 --> I[用範本建頁,再附加一個區塊]
|
||||
> E -- 金鑰失效或 API 失敗 --> J[中止:不建頁、不寫入]
|
||||
> G --> K[整頁寫回,別台機器的區塊原樣送回]
|
||||
> H --> K
|
||||
> I --> K
|
||||
> ```
|
||||
>
|
||||
> - 連結一律 `[{文字}]({絕對網址})`,網址取 `gitea.sh wiki-url`。這一個區塊要放進去的每一個連結,寫入前先過 `link-check.sh`,結束碼 0 才寫;結束碼 1 就不寫這一個區塊,把 DEAD 那幾筆回報出去。結束碼 3 是 `GITEA_HOST` 沒設定,補設定再驗,不准跳過驗證;結束碼 7 是金鑰失效,停下來回報金鑰問題,不要當成死連結——金鑰過期時私有存取庫的回應和「頁不存在」分不出來,混為一談會把還在的頁整批判死。
|
||||
> - 存取庫走 `gitea.sh wiki-repo CONTENTS`:先 `JSC_WIKI_REPO_CONTENTS`,再 `JSC_WIKI_REPO`,都沒設就結束碼 3,不退回監控頁那一支變數。
|
||||
> - `upsert` 的位置參數是 `MONITOR`、`{key-col}`、`{key}`、`{區塊檔}`、`{範本}`:
|
||||
> - `{key-col}` 只在舊頁還是 markdown 表格時才用得到,指舊表格中持有身分的那一欄序號(1 起算)。本頁的舊表格是第 1 欄「監控頁」,那一格是 `[MONITOR_{HASH}](網址)`,轉檔時只取文字當 H2 標題。頁面已經是條列格式時這個參數完全用不到。
|
||||
> - `{key}` 是這一個區塊的 H2 標題文字,也就是內容頁頁名 `MONITOR_{HASH}`。用來找既有的區塊。
|
||||
> - `{區塊檔}` 是整個 H2 區塊的 markdown:`## MONITOR_{HASH}` 那一行、空行,然後各條 `- {欄位名}:{值}`。
|
||||
> - 比對鍵是 H2 標題,原樣比對標題文字(去頭尾空白後完全相等)。鍵取 `collect` 印的 `page=`,自己重打會對不上,結果是同一台機器多出第二個區塊。
|
||||
> - 「監控頁」那一條的連結不當鍵:那一條含 `GITEA_HOST` 與頁名的網址編碼,主機位址改掉、`JSC_WIKI_REPO_MONITOR` 換了存取庫、或 Gitea 的網址編碼有差,整條文字就變了,鍵跟著對不上。這一頁每 15 分鐘寫一次,對不上的那一刻起每輪多附一個區塊,舊區塊再也不會更新。頁名只由 `{主機名}/{登入帳號}` 決定,那三件事都動不到它。
|
||||
> - 找得到相同的 H2 標題就換掉那一個區塊,找不到才附加一個區塊。
|
||||
> - 只動自己那一個區塊,別台機器的區塊一個字都不改。禁止整頁覆蓋——整頁覆蓋等於刪掉別台機器的紀錄。
|
||||
> - 只有「頁不存在」才准用範本建頁。金鑰失效或 API 失敗一律中止:那兩種情況舊內容是未知的,拿範本蓋上去就是把活著的紀錄整份刪掉。
|
||||
> - 舊頁還是 markdown 表格時,`upsert` 自己先把整頁轉成 H2 區塊再做這一次寫入,資料列的順序原樣保留。這一頁不再留任何 markdown 表格。
|
||||
> - 內容頁 `MONITOR_{HASH}` 的寫入語意不同:那頁固定三塊,最新一輪整塊換掉,摘要表一輪一列、最新的在最上面、超過 24 列丟最舊的。內容頁維持表格,兩者不要混用。
|
||||
|
||||
| 監控頁 | HASH | 主機 | 帳號 | 心跳 | 最後巡檢 | 待辦筆數 | 連續失敗項 |
|
||||
| --- | --- | --- | --- | --- | --- | ---: | ---: |
|
||||
| [MONITOR_{HASH}]({wiki-url 印出的絕對網址}) | {HASH} | {主機名} | {登入帳號} | {新鮮、過期、不存在 三選一} | {yyyy-MM-dd HH:mm} | {n} | {n} |
|
||||
## MONITOR_{HASH}
|
||||
|
||||
## 欄位說明
|
||||
|
||||
| 欄位 | 內容 | 為什麼留這一欄 |
|
||||
| --- | --- | --- |
|
||||
| 監控頁 | 指向 `MONITOR_{HASH}` 的絕對網址連結,給人點的,不當比對鍵 | 少了連結就要人自己算雜湊才翻得到內容頁;跨存取庫只有絕對網址連得過去 |
|
||||
| HASH | `hash-id` 印出的完整 40 碼大寫十六進位,純文字,不加連結、不加網址,也是 upsert 的比對鍵 | 這一格只跟 `{主機名}/{登入帳號}` 有關,換主機位址、換存取庫、換一種網址編碼都不會變。拿含網址的連結當鍵才會對不上,然後同一台機器每輪多附一列 |
|
||||
| 主機 | 這台機器的短主機名,與雜湊第一段相同 | 一眼看出這一列是哪一台機器 |
|
||||
| 帳號 | 助理執行時的登入帳號,與雜湊第二段相同 | 同一台機器換帳號就是另一個巡檢對象,雜湊也會不同 |
|
||||
| 心跳 | 巡檢當下(本輪寫入前)的心跳判定,判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒 | 一眼看出這台機器上一輪巡檢有沒有跑完,不必逐頁翻 |
|
||||
| 最後巡檢 | 該頁最新一輪的時間戳 | 心跳由巡檢寫,兩欄理當一致;差很多就代表有一輪寫了心跳卻沒寫頁,那是缺陷 |
|
||||
| 待辦筆數 | 待辦簿現有筆數 | 心跳新鮮而筆數為 0,代表助理空轉,沒有東西可跑 |
|
||||
| 連續失敗項 | 待辦簿裡 `fail_count` 大於 0 的筆數 | 待辦簿的項目失敗不會自動暫停,每輪都重試。這一欄讓壞掉的項目在目錄頁就現形 |
|
||||
|
||||
## 寫入規則
|
||||
|
||||
這一頁是共用目錄,別台機器的列一律原樣保留。寫入一律用 `jsc-gitea/tools/wiki-contents.sh upsert`,不手工改頁。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[監控頁已經寫成] --> B[gitea.sh wiki-url 取監控頁絕對網址]
|
||||
B --> C[組出本機那一列,網址換掉佔位]
|
||||
C --> D[wiki-contents.sh upsert MONITOR 第 2 欄的裸 HASH 當鍵]
|
||||
D --> E{舊頁讀得回來}
|
||||
E -- 是 --> F{HASH 欄對得上}
|
||||
F -- 是 --> G[取代那一列]
|
||||
F -- 否 --> H[附加一列]
|
||||
E -- 頁不存在 --> I[用範本建頁,再附加一列]
|
||||
E -- 金鑰失效或 API 失敗 --> J[中止:不建頁、不寫入]
|
||||
G --> K[整頁寫回,別台機器的列原樣送回]
|
||||
H --> K
|
||||
I --> K
|
||||
```
|
||||
|
||||
- 存取庫走 `gitea.sh wiki-repo CONTENTS`:先 `JSC_WIKI_REPO_CONTENTS`,再 `JSC_WIKI_REPO`,都沒設就結束碼 3,不退回監控頁那一支變數。
|
||||
- 比對鍵是第 2 欄的裸 HASH,原樣比對整格文字。鍵取 `collect` 印的 `hash=`,自己重打會對不上,結果是同一台機器多出第二列。
|
||||
- 第一欄的連結不當鍵:那一格含 `GITEA_HOST` 與頁名的網址編碼,主機位址改掉、`JSC_WIKI_REPO_MONITOR` 換了存取庫、或 Gitea 的網址編碼有差,整格文字就變了,鍵跟著對不上。這一頁每 15 分鐘寫一次,對不上的那一刻起每輪多附一列,舊列再也不會更新。
|
||||
- 找得到相同的 HASH 就更新那一列,找不到才附加一列。
|
||||
- 只動自己那一列,別台機器的列一個字都不改。禁止整頁覆蓋——整頁覆蓋等於刪掉別台機器的紀錄。
|
||||
- 只有「頁不存在」才准用範本建頁。金鑰失效或 API 失敗一律中止:那兩種情況舊內容是未知的,拿範本蓋上去就是把活著的紀錄整份刪掉。
|
||||
- 內容頁 `MONITOR_{HASH}` 的寫入語意不同:那頁固定三塊,最新一輪整塊換掉,摘要表一輪一列、最新的在最上面、超過 24 列丟最舊的。兩者不要混用。
|
||||
- 監控頁:[MONITOR_{HASH}]({wiki-url 印出的絕對網址})
|
||||
- HASH:{HASH}
|
||||
- 主機:{主機名}
|
||||
- 帳號:{登入帳號}
|
||||
- 心跳:{新鮮、過期、不存在 三選一}
|
||||
- 最後巡檢:{yyyy-MM-dd HH:mm}
|
||||
- 待辦筆數:{n}
|
||||
- 連續失敗項:{n}
|
||||
|
||||
+52
-12
@@ -4,16 +4,17 @@
|
||||
> 這頁固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。
|
||||
> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。
|
||||
> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。
|
||||
> 目錄頁 `MONITOR_CONTENTS` 在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和這頁不同庫;那一頁只更新自己那一列,別台機器的列一個字都不動。
|
||||
> 目錄頁 `MONITOR_CONTENTS` 在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和這頁不同庫;那一頁一台機器一個 H2 區塊,只更新自己那一個區塊,別台機器的區塊一個字都不動。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[巡檢一輪] --> B[收攏四項結果]
|
||||
A[巡檢一輪] --> B[收攏各項結果]
|
||||
B --> C[讀回舊頁]
|
||||
C --> D[換掉最新一輪那一塊]
|
||||
D --> E[本輪摘要列插到表格最上面,截到 24 列]
|
||||
E --> F[整頁寫回]
|
||||
F --> G[wiki-contents.sh upsert 更新目錄頁自己那一列]
|
||||
E --> V[link-check.sh 驗這一頁要放的連結]
|
||||
V --> F[結束碼 0 才整頁寫回]
|
||||
F --> G[wiki-contents.sh upsert 更新目錄頁自己那一個區塊]
|
||||
G --> H[最後才寫心跳]
|
||||
```
|
||||
|
||||
@@ -34,7 +35,7 @@ flowchart LR
|
||||
|
||||
這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。
|
||||
|
||||
六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
|
||||
七個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
@@ -57,7 +58,7 @@ flowchart LR
|
||||
|
||||
這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一頁寫成之後才寫。
|
||||
|
||||
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看這一塊上面的「本輪判定」。
|
||||
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪各項都成功——那要看這一塊上面的「本輪判定」。
|
||||
|
||||
### 技能與呼叫鏈使用統計
|
||||
|
||||
@@ -67,13 +68,47 @@ flowchart LR
|
||||
| --- | --- | ---: | ---: |
|
||||
| {技能名或呼叫鏈} | {技能、呼叫鏈 二選一} | {n} | {n} |
|
||||
|
||||
### 執行狀態事件
|
||||
|
||||
資料出自 `$JSC_HOME/usage/events.jsonl`,由 `jsc-hooks` 的 `tools/report-status.sh drain` 排空,位移記在 `$JSC_HOME/usage/scan-state/events.offset`。排空之後緊接著跑一次 `rotate`。
|
||||
|
||||
技能的 `start` 由 hook 記,`end` 只能由技能自己在收尾時寫。**所以有 `start` 沒有配對的 `end` 就是那一輪中止了**,那也是這一整套機制唯一分得出中止的訊號。配對以 `session` 加 `name` 為鍵:五支 CLI 併發時同一支技能會有好幾個工作階段同時在跑,只比對 `name` 會讓 A 工作階段的 `end` 去配掉 B 工作階段的 `start`。
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 本輪事件數 | {n} |
|
||||
| 非 ok 事件數 | {n} |
|
||||
| 有 start 沒有 end(開超過 {門檻} 秒,疑似中止) | {n} |
|
||||
| 有 start 沒有 end(未達門檻,還在跑) | {n} |
|
||||
| 事件流輪替 | {rotated、not-needed、failed、skipped 四選一} |
|
||||
|
||||
#### 非 ok 事件明細
|
||||
|
||||
`status` 五種:`ok` 全部達成、`blocked` 被閘門或前置條件擋下、`failed` 做到一半失敗、`degraded` 做完了但有部分沒達成、`aborted` 使用者中止或前提不成立而主動停止。這一表只列不是 `ok` 的那幾筆。
|
||||
|
||||
| 時間 | 類別 | 名稱 | status | 結束碼 | detail |
|
||||
| --- | --- | --- | --- | ---: | --- |
|
||||
| {yyyy-MM-ddTHH:mm:ssZ} | {skill、hook 二選一} | {技能寫 domain:skill,hook 寫腳本檔名加子命令} | {blocked、failed、degraded、aborted 四選一} | {n} | {一行,最多 200 字;沒有就寫「-」} |
|
||||
|
||||
一筆都沒有就寫「本輪沒有 status 不是 ok 的事件」,不要留空表格。列太多時只列前面幾筆,並寫明總筆數與原始事件檔的路徑。
|
||||
|
||||
#### 有 start 沒有配對的 end
|
||||
|
||||
| 名稱 | 類別 | session | start 時間 | 已開著(秒) |
|
||||
| --- | --- | --- | --- | ---: |
|
||||
| {domain:skill} | {skill、hook 二選一} | {工作階段代號} | {yyyy-MM-ddTHH:mm:ssZ} | {n} |
|
||||
|
||||
只列開著超過心跳門檻的那幾筆。未達門檻的多半只是還在跑,另外算一個數字就好。沒配對到的 `start` 留在 `$JSC_HOME/assistant/events-open.tsv` 跨輪繼續配對;不跨輪的話,跑超過一個巡檢週期的技能每一輪都會被報成中止,而巡檢週期預設只有兩分鐘。
|
||||
|
||||
排空或輪替失敗時,這一節寫明是哪一步失敗、結束碼多少,那一項標成失敗。**這一節失敗一律不中止那一輪**:回報鏈自己壞掉,不可以把被回報的那一輪也拖下去。`drain` 回 3 是「沒有新事件」,那是正常狀態,多數輪次本來就沒有新事件。
|
||||
|
||||
### hook 執行期錯誤
|
||||
|
||||
資料出自 `jsc-hooks` 的 `tools/scan-hook-errors.sh` 與 `tools/scan-logs.sh`。助理只記錄與發動 `jsc-hooks:repair`,不自己改 hook。
|
||||
|
||||
| 發生時間 | hook | CLI | 結束碼 | 錯誤摘要 | 已寫 ERROR 頁 |
|
||||
| --- | --- | --- | ---: | --- | --- |
|
||||
| {yyyy-MM-dd HH:mm} | {腳本檔名} | {CLI 代號} | {n} | {一句摘要} | {ERROR_{HASH} 連結或「否」} |
|
||||
| {yyyy-MM-dd HH:mm} | {腳本檔名} | {CLI 代號} | {n} | {一句摘要} | {寫成 `[ERROR_{HASH}]({絕對網址})`,沒寫就填「否」} |
|
||||
|
||||
### 版本落差與重啟閘門
|
||||
|
||||
@@ -115,17 +150,19 @@ flowchart LR
|
||||
|
||||
| 項目 | 來源子節 | 建議入口 |
|
||||
| --- | --- | --- |
|
||||
| {一句話講完要處理什麼} | {上面六個子節之一} | {技能名或指令} |
|
||||
| {一句話講完要處理什麼} | {上面七個子節之一} | {技能名或指令} |
|
||||
|
||||
## 近 24 輪摘要
|
||||
|
||||
一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。
|
||||
|
||||
| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |
|
||||
| 巡檢時間 | 本輪判定 | 各項成敗 | 待人處理 | 警示來源 |
|
||||
| --- | --- | --- | ---: | --- |
|
||||
| {yyyy-MM-dd HH:mm} | {正常、警示、異常 三選一} | {成功項數}/{總項數} | {待人處理筆數} | {警示原因,多個用頓號串;沒有就寫「無」} |
|
||||
|
||||
「警示來源」那一欄不能省。四項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄是 4/4,沒有這一欄的話,看的人不知道警示哪來。理由要短,一眼讀完,像「心跳過期」「版本查詢失敗」「重啟閘門未清」「上一輪逾時被接手」。
|
||||
「警示來源」那一欄不能省。各項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄是滿分,沒有這一欄的話,看的人不知道警示哪來。理由要短,一眼讀完,像「心跳過期」「版本查詢失敗」「重啟閘門未清」「上一輪逾時被接手」「有技能只有 start 沒有 end」。
|
||||
|
||||
第三欄的欄名寫「各項成敗」,不寫項數。巡檢項目會增加,欄名寫死數字就要跟著改,而舊頁那些列的欄名不會跟著改,同一張表就會有兩種欄名。
|
||||
|
||||
## 寫入規則
|
||||
|
||||
@@ -135,6 +172,9 @@ flowchart LR
|
||||
- 本輪的摘要列插到摘要表最上面,舊的列往下移,超過 24 列就丟掉最舊的那一列。
|
||||
- 三塊重組成一整頁再整頁寫回。除了這三塊,頁上沒有別的東西。
|
||||
- 舊格式的頁(一輪一節疊起來的那種)第一次重組時,基本資料留著,那些節收掉,摘要表從本輪這一列開始,並在回報裡說明。
|
||||
- 整頁寫成之後,才回頭更新目錄頁自己那一列,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`,別台機器的列一個字都不動。目錄頁那一欄的連結用絕對網址,兩頁不同庫,`[[...]]` 連不過去。
|
||||
- 連結一律寫成 `[{文字}]({絕對網址})`,網址取 `jsc-gitea/tools/gitea.sh wiki-url` 印出的那一個,不自己組路徑。wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看不出壞掉。
|
||||
- 這一頁要放進去的每一個連結,寫入前先交給 `jsc-gitea/tools/link-check.sh`,結束碼 0 才整頁寫回。有 DEAD 就不寫,把連不到的那幾筆回報給呼叫端;結束碼 3 是 `GITEA_HOST` 沒設定,補設定再驗,不准跳過;結束碼 7 是金鑰失效,停下來回報金鑰問題,不要當成死連結。驗證一律走 API,不看網頁狀態碼——私有存取庫的網頁網址對未登入請求一律回 404。
|
||||
- 整頁寫成之後,才回頭更新目錄頁自己那一個區塊,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`,別台機器的區塊一個字都不動。目錄頁那一個區塊的連結同樣先驗過才寫。
|
||||
- 這一頁沒寫成就不寫心跳,讓它過期。心跳代表的是「這一輪的結果記在這一頁上了」。
|
||||
- 目錄頁只是索引。目錄頁的存取庫沒設定(結束碼 3)時照樣寫心跳,並把那一筆列進待人處理;其餘寫入失敗才不寫心跳。
|
||||
- 目錄頁只是索引。目錄頁的存取庫沒設定(結束碼 3)時照樣寫心跳,並把那一筆列進待人處理;其餘寫入失敗才不寫心跳。目錄頁少一個區塊只少一筆索引,這一輪的結果已經在這一頁上。
|
||||
- 執行狀態事件那一節的內容由 `tools/patrol.sh collect` 排空、彙整好,寫頁的人原樣採用,不自己再跑一次 `drain`。`drain` 是消耗性讀取:它一讀完就把位移往前推,同一批事件不會再出現第二次,第二次跑只會拿到 3,或者把下一輪的事件提前吃掉。
|
||||
|
||||
+327
-51
@@ -8,18 +8,18 @@
|
||||
#
|
||||
# collect 帶了 --out,finish 與 abort 就要帶同一個目錄,不然換不到本輪的用量快照。
|
||||
#
|
||||
# collect 讀四項來源、組出監控頁那三塊、把鎖拿在手上。
|
||||
# collect 讀各項來源、組出監控頁那三塊、把鎖拿在手上。
|
||||
# finish 在監控頁寫成功之後才呼叫:寫心跳、換上用量快照、放掉鎖。
|
||||
# abort 在監控頁沒寫成時呼叫:只放掉鎖,不寫心跳。
|
||||
#
|
||||
# 結束碼(三個子命令共用一張表,同一碼在不同子命令的成因寫在同一列):
|
||||
# 0 collect:四項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
|
||||
# 0 collect:各項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
|
||||
# 鎖放掉;abort:鎖放掉,本來就沒鎖也算
|
||||
# 1 collect:部分成功——至少一項失敗,也至少一項有結果。**結果照樣印得出來,呼叫端
|
||||
# 照樣要把這一輪寫上監控頁**,只是本輪判定要標成警示
|
||||
# 2 finish:找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可寫。collect 不會回這
|
||||
# 一碼——心跳讀不到只是 D-09 這一項失敗,另外三項照跑
|
||||
# 3 collect:四項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常
|
||||
# 一碼——心跳讀不到只是 D-09 這一項失敗,其餘各項照跑
|
||||
# 3 collect:各項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常
|
||||
# 4 上一輪還在跑,本輪讓開(collect),或鎖已經不在自己手上(finish、abort)。這不是
|
||||
# 失敗,是刻意讓開:不寫心跳、不寫監控頁,下一輪再來
|
||||
# 5 檔案系統失敗:鎖建不起來或放不掉、暫存檔寫不進去、快照換不上,或 heartbeat.sh write
|
||||
@@ -34,7 +34,7 @@
|
||||
#
|
||||
# --- 心跳寫不寫,只看結果有沒有記下來 ---
|
||||
#
|
||||
# 四項的成敗不決定心跳。四項全失敗但監控頁寫成了,那一輪還是跑完了,證據也留下來了,
|
||||
# 各項的成敗不決定心跳。各項全失敗但監控頁寫成了,那一輪還是跑完了,證據也留下來了,
|
||||
# 心跳照寫,頁上判定是異常,看頁的人自己判斷。反過來,監控頁沒寫成就是這一輪沒有結果,
|
||||
# 心跳一定不寫:讓它自己過期,就是「巡檢在空轉」的唯一訊號。
|
||||
# 所以寫心跳一定是獨立的 finish,時序上排在監控頁寫成之後,不與 collect 綁在一起。
|
||||
@@ -49,14 +49,17 @@
|
||||
# 拿 --round 比對出鎖不是自己的,回 4 且不寫心跳。
|
||||
# 搶回來這件事會記在監控頁上(lock_broken=1),不會安靜發生。
|
||||
#
|
||||
# --- 這四項都是純讀取 ---
|
||||
# --- 這幾項都是純讀取 ---
|
||||
#
|
||||
# D-01 技能與呼叫鏈使用統計 jsc-log 的 tools/usage-stats.sh
|
||||
# D-04 版本落差與重啟閘門 jsc-hooks 的 version-guard.sh report、restart-gate.sh report
|
||||
# D-07 SDLC 階段鎖與工作包鎖 $JSC_HOME/sessions/*.stage、$JSC_HOME/wp/*.pr
|
||||
# D-09 心跳與閘門狀態自述 jsc-hooks 的 heartbeat.sh report
|
||||
# 四項各自獨立:一項的來源不見了、或回非 0,只讓那一項標成失敗,其餘三項照跑、照記。
|
||||
# 四項都不呼叫別的技能、不寫程式碼存取庫、不做決策。
|
||||
# D-11 執行狀態事件 jsc-hooks 的 tools/report-status.sh drain
|
||||
# 各項各自獨立:一項的來源不見了、或回非 0,只讓那一項標成失敗,其餘各項照跑、照記。
|
||||
# 各項都不呼叫別的技能、不寫程式碼存取庫、不做決策。
|
||||
# D-11 是唯一會動到別人狀態的一項:drain 會把事件流的位移往前推。理由與配套見下面
|
||||
# 「執行狀態事件為什麼由這支排空」。
|
||||
#
|
||||
# --- version-guard.sh report 的既有缺陷照實記 ---
|
||||
#
|
||||
@@ -77,18 +80,53 @@
|
||||
# --- 目錄頁與內容頁分屬兩個存取庫 ---
|
||||
#
|
||||
# 內容頁 MONITOR_{HASH} 住 MONITOR 那個存取庫,目錄頁 MONITOR_CONTENTS 住 CONTENTS 那個
|
||||
# 專用存取庫,兩條解析鏈不互相退讓。所以目錄頁那一列指向內容頁的連結不能用 [[...]]:那種
|
||||
# 連結只在同一個 wiki 裡解得開,跨庫是死連結,畫面上還看不出壞掉,要用絕對網址。
|
||||
# 專用存取庫,兩條解析鏈不互相退讓。連結一律寫成 [{文字}]({絕對網址}),網址取
|
||||
# gitea.sh wiki-url 印的那一個,不自己組路徑;wiki 自己那種雙中括號寫法只在同一個 wiki 裡
|
||||
# 解得開,寫錯不會報錯,畫面上看起來像正常文字或死連結,巡不到也修不了。
|
||||
# 絕對網址要等內容頁真的寫進去才查得到(gitea.sh wiki-url 讀的是 API 回的 html_url),
|
||||
# 而 collect 跑在寫入之前,這裡查不到。所以這支只把列組好、網址留佔位,換字交給呼叫端。
|
||||
# 而 collect 跑在寫入之前,這裡查不到。所以這支只把區塊組好、網址留佔位,換字交給呼叫端。
|
||||
#
|
||||
# --- 目錄頁的比對鍵是裸 HASH,不是那個連結 ---
|
||||
# --- 連結先驗證連得到,才可以寫進頁面 ---
|
||||
#
|
||||
# 目錄頁那一列另外留一欄裸 HASH(純文字、不帶連結),upsert 就拿那一欄當鍵。wiki-contents.sh
|
||||
# 比對的是整格文字,拿含網址的連結當鍵太脆:GITEA_HOST 換掉、JSC_WIKI_REPO_MONITOR 換過存取
|
||||
# 庫、Gitea 對頁名的網址編碼有差,整格文字就變了,鍵對不上就走附加那一支,同一台機器多出第二
|
||||
# 列,舊列從此不再更新。這一頁每 15 分鐘寫一次,重複列累積得很快。裸 HASH 只由
|
||||
# {主機名}/{登入帳號} 決定,上面三件事都動不到它。
|
||||
# 這支腳本一頁都不寫:它只組出檔案,兩次 wiki 寫入都在呼叫端。所以驗證的時機也在呼叫端——
|
||||
# 換掉佔位、拿到真網址之後,寫入之前,把要放進頁面的每一個連結交給 jsc-gitea 的
|
||||
# tools/link-check.sh,結束碼 0 才寫。有 DEAD 就不寫那一頁或那一個區塊,把連不到的清單回報出去。
|
||||
# 驗證一律走 API,不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會
|
||||
# 把好連結判成壞的。金鑰失效(結束碼 7)要與「連不到」(結束碼 1)分開看,兩者混用,一次金鑰
|
||||
# 過期就會把整批還在的頁判成死連結。
|
||||
#
|
||||
# --- 目錄頁的比對鍵是 H2 標題,不是那個連結 ---
|
||||
#
|
||||
# 目錄頁一台機器一個 H2 區塊,標題寫成內容頁頁名 MONITOR_{HASH},upsert 就拿那個標題當鍵。
|
||||
# wiki-contents.sh 比對的是標題文字,拿含網址的連結當鍵太脆:GITEA_HOST 換掉、
|
||||
# JSC_WIKI_REPO_MONITOR 換過存取庫、Gitea 對頁名的網址編碼有差,那一條文字就變了,鍵對不上就走
|
||||
# 附加那一支,同一台機器多出第二個區塊,舊區塊從此不再更新。這一頁每 15 分鐘寫一次,重複區塊
|
||||
# 累積得很快。頁名只由 {主機名}/{登入帳號} 決定,上面三件事都動不到它。
|
||||
# 裸 HASH 照樣在區塊裡留一條:標題是 MONITOR_ 加上它,那一條讓人不必從標題切字串就抄得到。
|
||||
#
|
||||
# --- 執行狀態事件為什麼由這支排空 ---
|
||||
#
|
||||
# 事件流記的是每一支技能與每一支 hook 的執行結果。技能的 start 由 hook 免費記下,end 只能由
|
||||
# 技能自己在收尾時寫,所以「有 start 沒有配對的 end」就是那一輪中止了——那是這整套機制唯一
|
||||
# 分得出中止的訊號,也是這一項最重要的產出。
|
||||
#
|
||||
# 排空與彙整放在這支,不放在技能本文,有三個理由:
|
||||
# 1. drain 是消耗性讀取:它一讀完就把位移往前推,同一批事件不會再出現第二次。讀回來的內容
|
||||
# 只存在對話裡的話,模型少抄一行就是那一批事件永遠消失。寫成檔案才留得住。
|
||||
# 2. 一輪的事件動輒上百行 JSON,逐行判 status 與配對 start/end 交給模型做,既慢又會出錯。
|
||||
# 3. rotate 一定要緊接在 drain 後面跑。中間隔得越久,那段時間新寫進來的事件被搬進備份檔
|
||||
# 而從此不會被排空的機率越高。兩件事綁在同一支腳本的同一次執行,那個空窗才最小。
|
||||
#
|
||||
# 配對以 {session}+{name} 為鍵,不只看 name:五支 CLI 併發時同一支技能會有好幾個工作階段同時
|
||||
# 在跑,只看 name 會讓 A 工作階段的 end 去配掉 B 工作階段的 start,中止就被蓋掉了。
|
||||
#
|
||||
# 沒配對到的 start 會留在 $JSC_HOME/assistant/events-open.tsv,跨輪繼續配對。不留的話,一支
|
||||
# 跑超過一個巡檢週期的技能每一輪都會被報成中止——巡檢週期預設兩分鐘,那種誤報會多到沒人看。
|
||||
# 開著超過心跳門檻才算「疑似中止」,門檻以內的算「進行中」,兩種分開列。超過一天還沒配對到的
|
||||
# 就從檔案裡丟掉,那個檔案才不會無止境長大。
|
||||
#
|
||||
# 這一項失敗(找不到腳本、drain 回非預期結束碼、rotate 失敗)一律只讓這一項標成失敗或記一筆
|
||||
# 警示,不中止整輪:回報鏈自己壞掉,不可以把被回報的那一輪也拖下去。
|
||||
#
|
||||
# --- collect 的輸出 ---
|
||||
#
|
||||
@@ -103,17 +141,25 @@
|
||||
# item= 一項一行,欄位 status(ok、empty、fail)、rc、note
|
||||
# verdict= 正常、警示、異常
|
||||
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
|
||||
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。四項全過卻判成警示
|
||||
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示
|
||||
# 時,原因只寫在這裡
|
||||
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一列用
|
||||
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一個區塊用
|
||||
# pending= 本輪待人處理的筆數
|
||||
# events_total= 本輪排空到的事件筆數
|
||||
# events_bad= 其中 status 不是 ok 的筆數
|
||||
# events_unpaired= 有 start 沒有配對 end、而且已經開超過心跳門檻的筆數(疑似中止)
|
||||
# events_running= 有 start 沒有配對 end,但還在門檻以內的筆數(還在跑)
|
||||
# events_rotated= rotated、not-needed、failed、skipped 四選一
|
||||
# events_file= 本輪排空到的原始事件,一行一筆 JSON。drain 是消耗性讀取,這個檔案是
|
||||
# 那一批事件在被彙整之外唯一留下的完整原文
|
||||
# latest_file= 「最新一輪」那一塊,整塊換掉舊頁同名那一塊
|
||||
# summary_file= 「近 24 輪摘要」那一塊,表格裡先放本輪這一列,舊頁的資料列接在下面
|
||||
# summary_row_file= 只有本輪那一列,方便直接插到既有表格最上面
|
||||
# newpage_file= MONITOR_{HASH} 不存在時要建的整頁內容,三塊都已經排好
|
||||
# contents_file= MONITOR_CONTENTS 那一列的欄位值。row= 就是整列 markdown,第一欄是連結,
|
||||
# 網址的位置留 {監控頁絕對網址} 佔位,由呼叫端換掉,理由見下一段;第二欄是
|
||||
# 裸 HASH,upsert 拿那一欄當鍵,理由見再下一段
|
||||
# contents_file= 目錄頁上本機那一個 H2 區塊,整塊 markdown:`## {頁名}` 那一行、
|
||||
# 空行,然後各條 `- {欄位名}:{值}`。「監控頁」那一條是 [{頁名}]({絕對網址})
|
||||
# 這種連結,網址的位置留 {監控頁絕對網址} 佔位,由呼叫端換掉、驗過再寫,理由見
|
||||
# 下一段。upsert 拿 H2 標題當鍵,理由見再下一段
|
||||
#
|
||||
# 環境變數:
|
||||
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc
|
||||
@@ -122,6 +168,7 @@
|
||||
# JSC_ASSIST_RESTART_GATE_SH restart-gate.sh 路徑覆寫
|
||||
# JSC_ASSIST_USAGE_STATS_SH usage-stats.sh 路徑覆寫
|
||||
# JSC_ASSIST_HASH_ID hash-id 路徑覆寫
|
||||
# JSC_ASSIST_REPORT_STATUS_SH report-status.sh 路徑覆寫
|
||||
# JSC_ASSIST_PATROL_LOCK_TTL 鎖的逾時秒數;未設定時取心跳門檻,取不到就用 300
|
||||
set -u
|
||||
|
||||
@@ -130,6 +177,9 @@ STATE_DIR="$JSC_HOME/assistant"
|
||||
CURRENT="$JSC_HOME/current"
|
||||
LOCK="$STATE_DIR/patrol.lock"
|
||||
PREV_SNAP="$STATE_DIR/usage-prev.tsv"
|
||||
# 還沒配對到 end 的 start,跨輪留在這裡。放 $JSC_HOME/assistant 而不放 $RD:$RD 每一輪重寫,
|
||||
# 放那裡就等於不跨輪,跑超過一個週期的技能每輪都會被報成中止。
|
||||
EVENTS_OPEN="$STATE_DIR/events-open.tsv"
|
||||
RD="$STATE_DIR/patrol"
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||
@@ -162,7 +212,7 @@ warn_if_not_current() {
|
||||
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
|
||||
@@ -221,6 +271,10 @@ mtime_of() { # $1=檔案;印出修改時間,取不到印「-」
|
||||
# markdown 表格欄位裡的 `|` 會把欄切開,一律跳脫;換行壓成空白。
|
||||
cell() { printf '%s' "$1" | tr '\n' ' ' | sed 's/|/\\|/g'; }
|
||||
|
||||
# 條列一條的值。換行一律壓成空白:一條 bullet 裡的換行會被讀成另一條,或者讓區塊提早結束。
|
||||
# `|` 在條列裡沒有特殊意義,所以不跳脫——跳脫過的 `\|` 反而會原樣顯示在頁面上。
|
||||
oneline() { printf '%s' "$1" | tr '\n' ' '; }
|
||||
|
||||
# 記一個讀不到的來源。同一輪多項失敗就串起來,供監控頁「讀不到的來源」那一列用。
|
||||
add_failed_source() { # $1=路徑或來源名稱
|
||||
if [ -z "$FAILED_SOURCES" ]; then FAILED_SOURCES="$1"; else FAILED_SOURCES="$FAILED_SOURCES、$1"; fi
|
||||
@@ -581,7 +635,7 @@ d09() {
|
||||
printf '| pid | %s。只給要找行程的人參考,不參與判定 |\n' "$(cell "${_pid:--}")"
|
||||
printf '| 心跳檔 | `%s` |\n' "$(cell "${_file:--}")"
|
||||
printf '\n心跳的判準只看 `ts` 距現在有沒有超過門檻,不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳,不參與判定。\n'
|
||||
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看這一塊上面的「本輪判定」。\n'
|
||||
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪各項都成功——各項的成敗看這一塊上面的「本輪判定」。\n'
|
||||
} >>"$RD/d09.md"
|
||||
|
||||
case "$_st" in
|
||||
@@ -592,7 +646,217 @@ d09() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- 待辦簿筆數(只供目錄頁那一列用)---
|
||||
# --- D-11 執行狀態事件 ---
|
||||
|
||||
# 一輪最多列幾筆明細。不設上限的話,一次壞掉的 hook 每次提示寫一筆,一輪就能把整頁灌爆,
|
||||
# 而灌爆的頁沒有人讀得完,等於這一節白寫。列不下的用一句話講清楚還有幾筆。
|
||||
EV_MAX_ROWS=50
|
||||
|
||||
# 把事件流的一行 JSON 攤成定位字元分隔的欄位。不引 JSON 解析器:巡檢跑在 cron 上,能倚賴的
|
||||
# 只有系統本來就有的工具,多一個相依就是多一種在某台機器上跑不起來的方式。
|
||||
# 取值以「鍵名加冒號加引號」定位,取到下一個引號為止。detail 裡若有被跳脫的引號會在那裡被切斷,
|
||||
# 那只影響顯示的長度,不影響筆數與配對,所以不為它多寫一套解析。
|
||||
events_to_tsv() { # $1=原始事件檔 $2=輸出檔
|
||||
awk '
|
||||
function jstr(s, k, r) {
|
||||
if (match(s, "\"" k "\":\"")) {
|
||||
r = substr(s, RSTART + length(k) + 4)
|
||||
if (match(r, "\"")) return substr(r, 1, RSTART - 1)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
function jnum(s, k, r) {
|
||||
if (match(s, "\"" k "\":[0-9]+")) { r = substr(s, RSTART, RLENGTH); sub(/^.*:/, "", r); return r }
|
||||
return ""
|
||||
}
|
||||
{
|
||||
gsub(/\t/, " ")
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", \
|
||||
jstr($0, "ts"), jstr($0, "cli"), jstr($0, "session"), jstr($0, "kind"), jstr($0, "name"), \
|
||||
jstr($0, "phase"), jstr($0, "status"), jnum($0, "exit"), jstr($0, "detail")
|
||||
}' "$1" >"$2" 2>/dev/null
|
||||
}
|
||||
|
||||
d11() {
|
||||
D11_STATUS=fail; D11_RC=0; D11_NOTE=''
|
||||
EV_TOTAL=0; EV_BAD=0; EV_UNPAIRED=0; EV_RUNNING=0; EV_ROTATED=skipped
|
||||
{
|
||||
printf '### 執行狀態事件\n\n'
|
||||
printf '資料出自 `%s`,由 `jsc-hooks` 的 `tools/report-status.sh drain` 排空,位移記在 `%s`。\n\n' \
|
||||
"\$JSC_HOME/usage/events.jsonl" "\$JSC_HOME/usage/scan-state/events.offset"
|
||||
printf '技能的 `start` 由 hook 記,`end` 只能由技能自己在收尾時寫。所以**有 `start` 沒有配對的 `end` 就是那一輪中止了**,配對以 `session` 加 `name` 為鍵。\n\n'
|
||||
} >"$RD/d11.md"
|
||||
|
||||
if ! _rs=$(find_tool hooks tools/report-status.sh "${JSC_ASSIST_REPORT_STATUS_SH:-}"); then
|
||||
D11_NOTE='找不到 jsc-hooks 的 tools/report-status.sh'
|
||||
D11_RC=127
|
||||
add_failed_source 'jsc-hooks/tools/report-status.sh'
|
||||
printf '**這一項失敗**:%s。這一輪沒有執行狀態事件,不是「每一支都跑成功」。\n' "$D11_NOTE" >>"$RD/d11.md"
|
||||
add_pending '執行狀態事件讀不到,jsc-hooks 沒裝或版本太舊' '執行狀態事件' '/jsc-cli:doctor'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 排空。結束碼 3 是「沒有新事件」,那是正常狀態,不是失敗——每一輪都排空,多數輪次本來
|
||||
# 就沒有新事件。JSC_HOME 明寫成環境變數傳下去:這支腳本裡的 JSC_HOME 不見得是匯出的,
|
||||
# 子行程自己算預設值時,兩邊指到同一個目錄才算數。
|
||||
: >"$RD/events.raw"
|
||||
_rc=0
|
||||
JSC_HOME="$JSC_HOME" "$_rs" drain >"$RD/events.raw" 2>"$RD/d11.err" </dev/null || _rc=$?
|
||||
if [ "$_rc" -ne 0 ] && [ "$_rc" -ne 3 ]; then
|
||||
D11_RC="$_rc"
|
||||
D11_NOTE="report-status.sh drain 回 $_rc"
|
||||
add_failed_source "$_rs"
|
||||
printf '**這一項失敗**:%s。訊息:%s\n' "$D11_NOTE" "$(cell "$(cat "$RD/d11.err" 2>/dev/null)")" >>"$RD/d11.md"
|
||||
add_pending '事件流排空失敗,本輪沒有執行狀態證據' '執行狀態事件' '/jsc-hooks:repair'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 輪替緊接在排空後面跑,而且只在排空成功時跑。排空失敗時位移的狀態是未知的,這時候輪替
|
||||
# 會把還沒排空的事件搬進備份檔,那一批從此不會再出現在任何一輪。
|
||||
_rrc=0
|
||||
_rout=$(JSC_HOME="$JSC_HOME" "$_rs" rotate 2>>"$RD/d11.err" </dev/null) || _rrc=$?
|
||||
if [ "$_rrc" -ne 0 ]; then
|
||||
EV_ROTATED=failed
|
||||
add_warn '事件流輪替失敗'
|
||||
add_pending "事件流輪替回 $_rrc,檔案會一直長大" '執行狀態事件' '/jsc-hooks:repair'
|
||||
elif [ -n "$_rout" ]; then
|
||||
EV_ROTATED=rotated
|
||||
else
|
||||
EV_ROTATED=not-needed
|
||||
fi
|
||||
|
||||
events_to_tsv "$RD/events.raw" "$RD/events.tsv"
|
||||
|
||||
_now=$(date +%s)
|
||||
_ttl="${HEARTBEAT_TTL:-}"
|
||||
case "$_ttl" in ''|*[!0-9]*) _ttl=300 ;; esac
|
||||
|
||||
: >"$RD/events-bad.tsv"; : >"$RD/events-unpaired.tsv"; : >"$RD/events-open.next"
|
||||
[ -f "$EVENTS_OPEN" ] || : >"$EVENTS_OPEN"
|
||||
# 兩個輸入檔:先讀上一輪留下來還開著的 start,再讀本輪排空到的事件。用 FILENAME 分辨是
|
||||
# 哪一個檔案,不用 NR==FNR:上一輪那個檔案是空的時候,NR==FNR 會把本輪第一筆事件誤當成
|
||||
# 舊資料,而「上一輪沒有開著的 start」正是最常見的情況。
|
||||
awk -F' ' -v prevf="$EVENTS_OPEN" -v now="$_now" -v ttl="$_ttl" -v maxage=86400 \
|
||||
-v openf="$RD/events-open.next" -v badf="$RD/events-bad.tsv" -v unpf="$RD/events-unpaired.tsv" '
|
||||
FILENAME == prevf { k = $1 SUBSEP $2; okind[k] = $3; oseen[k] = $4; ots[k] = $5; next }
|
||||
{
|
||||
total++
|
||||
if ($7 != "ok") { bad++; printf "%s\t%s\t%s\t%s\t%s\t%s\n", $1, $4, $5, $7, $8, $9 >badf }
|
||||
k = $3 SUBSEP $5
|
||||
if ($6 == "start") {
|
||||
okind[k] = $4
|
||||
# 第一次看到才記時間:同一支技能在同一個工作階段重複開場時,年紀要從最早那一次算起。
|
||||
if (!(k in oseen)) { oseen[k] = now; ots[k] = $1 }
|
||||
} else if ($6 == "end") { delete okind[k]; delete oseen[k]; delete ots[k] }
|
||||
}
|
||||
END {
|
||||
for (k in oseen) {
|
||||
age = now - oseen[k]
|
||||
# 開超過一天的丟掉。留著只會讓這個檔案無止境長大,而那麼久沒收尾的 start 早就報過了。
|
||||
if (age > maxage) continue
|
||||
split(k, p, SUBSEP)
|
||||
printf "%s\t%s\t%s\t%s\t%s\n", p[1], p[2], okind[k], oseen[k], ots[k] >openf
|
||||
if (age >= ttl) { unpaired++; printf "%s\t%s\t%s\t%s\t%s\n", p[2], okind[k], p[1], ots[k], age >unpf }
|
||||
else running++
|
||||
}
|
||||
printf "total=%d bad=%d unpaired=%d running=%d\n", total, bad, unpaired, running
|
||||
}' "$EVENTS_OPEN" "$RD/events.tsv" >"$RD/d11.counts" 2>>"$RD/d11.err"
|
||||
|
||||
_counts=$(cat "$RD/d11.counts" 2>/dev/null)
|
||||
for _kv in $_counts; do
|
||||
case "$_kv" in
|
||||
total=*) EV_TOTAL="${_kv#total=}" ;;
|
||||
bad=*) EV_BAD="${_kv#bad=}" ;;
|
||||
unpaired=*) EV_UNPAIRED="${_kv#unpaired=}" ;;
|
||||
running=*) EV_RUNNING="${_kv#running=}" ;;
|
||||
esac
|
||||
done
|
||||
for _v in EV_TOTAL EV_BAD EV_UNPAIRED EV_RUNNING; do
|
||||
eval "_x=\$$_v"
|
||||
case "$_x" in ''|*[!0-9]*) eval "$_v=0" ;; esac
|
||||
done
|
||||
|
||||
# 換上這一輪之後還開著的 start。換不上不算整項失敗:下一輪頂多重算一次年紀,不會漏報。
|
||||
cp "$RD/events-open.next" "$EVENTS_OPEN" 2>/dev/null \
|
||||
|| printf '**未配對清單存檔失敗**:`%s` 寫不進去,下一輪的年紀要重算。\n\n' "$EVENTS_OPEN" >>"$RD/d11.md"
|
||||
|
||||
{
|
||||
printf '| 項目 | 內容 |\n'
|
||||
printf '| --- | --- |\n'
|
||||
printf '| 本輪事件數 | %s |\n' "$EV_TOTAL"
|
||||
printf '| 非 ok 事件數 | %s |\n' "$EV_BAD"
|
||||
printf '| 有 start 沒有 end(開超過 %s 秒,疑似中止) | %s |\n' "$_ttl" "$EV_UNPAIRED"
|
||||
printf '| 有 start 沒有 end(未達門檻,還在跑) | %s |\n' "$EV_RUNNING"
|
||||
printf '| 事件流輪替 | %s |\n' "$EV_ROTATED"
|
||||
printf '\n#### 非 ok 事件明細\n\n'
|
||||
} >>"$RD/d11.md"
|
||||
|
||||
if [ -s "$RD/events-bad.tsv" ]; then
|
||||
{
|
||||
printf '| 時間 | 類別 | 名稱 | status | 結束碼 | detail |\n'
|
||||
printf '| --- | --- | --- | --- | ---: | --- |\n'
|
||||
} >>"$RD/d11.md"
|
||||
_n=0
|
||||
while IFS=' ' read -r _ets _ekind _ename _est _eex _edt; do
|
||||
[ -n "$_ename" ] || continue
|
||||
_n=$(( _n + 1 ))
|
||||
[ "$_n" -le "$EV_MAX_ROWS" ] || continue
|
||||
printf '| %s | %s | %s | %s | %s | %s |\n' \
|
||||
"$(cell "$_ets")" "$(cell "$_ekind")" "$(cell "$_ename")" \
|
||||
"$(cell "$_est")" "$(cell "${_eex:--}")" "$(cell "${_edt:--}")" >>"$RD/d11.md"
|
||||
done <"$RD/events-bad.tsv"
|
||||
[ "$_n" -gt "$EV_MAX_ROWS" ] \
|
||||
&& printf '\n本輪非 ok 事件共 %s 筆,上表只列前 %s 筆。完整原文在 `%s`。\n' \
|
||||
"$_n" "$EV_MAX_ROWS" "$RD/events.raw" >>"$RD/d11.md"
|
||||
else
|
||||
printf '本輪沒有 status 不是 ok 的事件。\n' >>"$RD/d11.md"
|
||||
fi
|
||||
|
||||
{
|
||||
printf '\n#### 有 start 沒有配對的 end\n\n'
|
||||
printf '這一節就是中止的證據。`start` 由 hook 免費記下,`end` 要技能自己寫,所以只有 `start` 的那一筆,代表那一支技能沒有跑到收尾那一步。\n\n'
|
||||
} >>"$RD/d11.md"
|
||||
if [ -s "$RD/events-unpaired.tsv" ]; then
|
||||
{
|
||||
printf '| 名稱 | 類別 | session | start 時間 | 已開著(秒) |\n'
|
||||
printf '| --- | --- | --- | --- | ---: |\n'
|
||||
} >>"$RD/d11.md"
|
||||
_n=0
|
||||
while IFS=' ' read -r _uname _ukind _usess _uts _uage; do
|
||||
[ -n "$_uname" ] || continue
|
||||
_n=$(( _n + 1 ))
|
||||
[ "$_n" -le "$EV_MAX_ROWS" ] || continue
|
||||
printf '| %s | %s | %s | %s | %s |\n' \
|
||||
"$(cell "$_uname")" "$(cell "$_ukind")" "$(cell "$_usess")" \
|
||||
"$(cell "$_uts")" "$(cell "$_uage")" >>"$RD/d11.md"
|
||||
done <"$RD/events-unpaired.tsv"
|
||||
[ "$_n" -gt "$EV_MAX_ROWS" ] \
|
||||
&& printf '\n未配對的 `start` 共 %s 筆,上表只列前 %s 筆。\n' "$_n" "$EV_MAX_ROWS" >>"$RD/d11.md"
|
||||
else
|
||||
printf '本輪沒有開超過門檻又沒收尾的 `start`。\n' >>"$RD/d11.md"
|
||||
fi
|
||||
printf '\n未達門檻的 `start` 不列進上表,它們多半只是還在跑。跨輪繼續配對,紀錄留在 `%s`;不跨輪的話,跑超過一個巡檢週期的技能每一輪都會被報成中止。\n' \
|
||||
"$EVENTS_OPEN" >>"$RD/d11.md"
|
||||
|
||||
if [ "$EV_BAD" -gt 0 ]; then
|
||||
add_warn '有非 ok 的執行狀態事件'
|
||||
add_pending "本輪有 $EV_BAD 筆執行狀態不是 ok 的事件" '執行狀態事件' '照明細表的名稱找那一支技能或 hook'
|
||||
fi
|
||||
if [ "$EV_UNPAIRED" -gt 0 ]; then
|
||||
add_warn '有技能只有 start 沒有 end'
|
||||
add_pending "本輪有 $EV_UNPAIRED 支技能只有 start 沒有 end,那幾輪中止了" '執行狀態事件' '照明細表的名稱重跑那一支技能'
|
||||
fi
|
||||
|
||||
if [ "$EV_TOTAL" -gt 0 ]; then
|
||||
D11_STATUS=ok
|
||||
else
|
||||
D11_STATUS=empty
|
||||
printf '\n來源讀得到,本輪沒有新事件(`drain` 回 3)。那是正常狀態,不是失敗:多數輪次本來就沒有新的技能或 hook 跑過。\n' >>"$RD/d11.md"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- 待辦簿筆數(只供目錄頁那一個區塊用)---
|
||||
|
||||
count_tasks() {
|
||||
TASKS_TOTAL=0; TASKS_FAILING=0
|
||||
@@ -635,9 +899,9 @@ compose() {
|
||||
printf '| 巡檢時間 | %s |\n' "$AT"
|
||||
printf '| 觸發方式 | %s |\n' "$TRIGGER"
|
||||
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 心跳自述、D-11 執行狀態事件。成功 %s 項、失敗 %s 項 |\n' "$OK_COUNT" "$FAIL_COUNT"
|
||||
printf '| 讀不到的來源 | %s |\n' "$(cell "${FAILED_SOURCES:-無}")"
|
||||
# 警示來源緊接在讀不到的來源後面:兩列語意相近,而且四項讀取全部成功、判定卻是警示
|
||||
# 警示來源緊接在讀不到的來源後面:兩列語意相近,而且各項讀取全部成功、判定卻是警示
|
||||
# 時,這一塊裡只有這一列講得出原因,跟摘要表那一欄是同一個理由。
|
||||
printf '| 警示來源 | %s |\n' "$(cell "${WARN_SOURCES:-無}")"
|
||||
if [ "$LOCK_BROKEN" -eq 1 ]; then
|
||||
@@ -646,6 +910,7 @@ compose() {
|
||||
printf '\n'
|
||||
cat "$RD/d09.md"; printf '\n'
|
||||
cat "$RD/d01.md"; printf '\n'
|
||||
cat "$RD/d11.md"; printf '\n'
|
||||
printf '### hook 執行期錯誤\n\n'
|
||||
printf '**這一輪不做這一項。** D-02 hook 錯誤巡檢還沒實作,這一節沒有資料不代表沒有 hook 錯誤。要現在查就跑 `/jsc-hooks:hooks-install` 的錯誤掃描,或直接跑 `jsc-hooks` 的 `tools/scan-hook-errors.sh`。\n\n'
|
||||
cat "$RD/d04.md"; printf '\n'
|
||||
@@ -661,8 +926,10 @@ compose() {
|
||||
} >"$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"
|
||||
@@ -671,7 +938,7 @@ compose() {
|
||||
{
|
||||
printf '## 近 24 輪摘要\n\n'
|
||||
printf '一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。\n\n'
|
||||
printf '| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |\n'
|
||||
printf '| 巡檢時間 | 本輪判定 | 各項成敗 | 待人處理 | 警示來源 |\n'
|
||||
printf '| --- | --- | --- | ---: | --- |\n'
|
||||
cat "$RD/summary-row.md"
|
||||
} >"$RD/summary.md"
|
||||
@@ -684,14 +951,14 @@ compose() {
|
||||
printf '> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。\n'
|
||||
printf '> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。\n'
|
||||
printf '> 目錄頁 `MONITOR_CONTENTS` 在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和這頁不同庫。\n'
|
||||
printf '> 那一頁只更新自己那一列,別台機器的列一個字都不動,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`。\n\n'
|
||||
printf '> 那一頁一台機器一個 H2 區塊,只更新自己那一個區塊,別台機器的區塊一個字都不動,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`。\n\n'
|
||||
printf '```mermaid\nflowchart LR\n'
|
||||
printf ' A[巡檢一輪] --> B[收攏四項結果]\n'
|
||||
printf ' A[巡檢一輪] --> B[收攏各項結果]\n'
|
||||
printf ' B --> C[讀回舊頁]\n'
|
||||
printf ' C --> D[換掉最新一輪那一塊]\n'
|
||||
printf ' D --> E[本輪摘要列插到表格最上面,截到 24 列]\n'
|
||||
printf ' E --> F[整頁寫回]\n'
|
||||
printf ' F --> G[wiki-contents.sh upsert 更新目錄頁自己那一列]\n'
|
||||
printf ' F --> G[wiki-contents.sh upsert 更新目錄頁自己那一個區塊]\n'
|
||||
printf ' G --> H[最後才寫心跳]\n'
|
||||
printf '```\n\n'
|
||||
printf '## 本頁基本資料\n\n'
|
||||
@@ -706,24 +973,24 @@ compose() {
|
||||
cat "$RD/summary.md"
|
||||
} >"$RD/newpage.md"
|
||||
|
||||
{
|
||||
printf 'page=%s\n' "$PAGE"
|
||||
printf 'host=%s\n' "$HOST"
|
||||
printf 'user=%s\n' "$USER_NAME"
|
||||
printf 'heartbeat=%s\n' "$HEARTBEAT_STATE"
|
||||
printf 'last_patrol=%s\n' "$AT"
|
||||
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
|
||||
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
|
||||
printf 'hash=%s\n' "$HASH"
|
||||
# 第一欄是連結,網址留佔位由呼叫端換掉,理由見檔頭「目錄頁與內容頁分屬兩個存取庫」。
|
||||
# 目錄頁那一個 H2 區塊。標題就是內容頁頁名,欄位一行一條,順序照範本從上到下。
|
||||
# 「監控頁」那一條是 [{頁名}]({絕對網址}) 這種連結,網址留佔位由呼叫端換掉、過完 link-check.sh
|
||||
# 才寫,理由見檔頭「目錄頁與內容頁分屬兩個存取庫」與「連結先驗證連得到」。
|
||||
# 連結文字先寫死成頁名:頁名這裡就知道,只有網址要等內容頁寫成才查得到。
|
||||
# 第二欄是裸 HASH,upsert 拿它當鍵。鍵不能用第一欄那個連結:那一格含 GITEA_HOST 與頁名的
|
||||
# 網址編碼,主機位址、存取庫或編碼一變,整格文字就變了,鍵對不上就每輪多附一列。裸 HASH
|
||||
# 只跟 {主機名}/{登入帳號} 有關,那三件事都動不到它。
|
||||
printf 'row=| [%s](%s) | %s | %s | %s | %s | %s | %s | %s |\n' \
|
||||
"$PAGE" '{監控頁絕對網址}' "$HASH" "$HOST" "$USER_NAME" "$HEARTBEAT_STATE" "$AT" \
|
||||
"$TASKS_TOTAL" "$TASKS_FAILING"
|
||||
} >"$RD/contents.tsv"
|
||||
# upsert 拿 H2 標題當鍵,不拿那條連結:連結含 GITEA_HOST 與頁名的網址編碼,主機位址、存取庫或
|
||||
# 編碼一變,那一條文字就變了,鍵對不上就每輪多附一個區塊。頁名只跟 {主機名}/{登入帳號} 有關,
|
||||
# 那三件事都動不到它。裸 HASH 照樣留一條,讓人不必從標題切字串就抄得到。
|
||||
{
|
||||
printf '## %s\n\n' "$PAGE"
|
||||
printf -- '- 監控頁:[%s](%s)\n' "$PAGE" '{監控頁絕對網址}'
|
||||
printf -- '- HASH:%s\n' "$HASH"
|
||||
printf -- '- 主機:%s\n' "$(oneline "$HOST")"
|
||||
printf -- '- 帳號:%s\n' "$(oneline "$USER_NAME")"
|
||||
printf -- '- 心跳:%s\n' "$(oneline "$HEARTBEAT_STATE")"
|
||||
printf -- '- 最後巡檢:%s\n' "$(oneline "$AT")"
|
||||
printf -- '- 待辦筆數:%s\n' "$TASKS_TOTAL"
|
||||
printf -- '- 連續失敗項:%s\n' "$TASKS_FAILING"
|
||||
} >"$RD/contents-entry.md"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -771,8 +1038,10 @@ case "$CMD" in
|
||||
d01
|
||||
d04
|
||||
d07
|
||||
d11
|
||||
count_tasks
|
||||
tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS"
|
||||
tally "$D11_STATUS"
|
||||
|
||||
HASH=''
|
||||
if _hi=$(find_tool gitea tools/hash-id "${JSC_ASSIST_HASH_ID:-}"); then
|
||||
@@ -794,17 +1063,24 @@ case "$CMD" in
|
||||
printf 'item=D-04 status=%s rc=%s note=%s\n' "$D04_STATUS" "$D04_RC" "$D04_NOTE"
|
||||
printf 'item=D-07 status=%s rc=%s note=%s\n' "$D07_STATUS" "$D07_RC" "$D07_NOTE"
|
||||
printf 'item=D-09 status=%s rc=%s note=%s\n' "$D09_STATUS" "$D09_RC" "$D09_NOTE"
|
||||
printf 'item=D-11 status=%s rc=%s note=%s\n' "$D11_STATUS" "$D11_RC" "$D11_NOTE"
|
||||
printf 'verdict=%s\n' "$VERDICT"
|
||||
printf 'failed_sources=%s\n' "${FAILED_SOURCES:-無}"
|
||||
printf 'warn_sources=%s\n' "${WARN_SOURCES:-無}"
|
||||
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
|
||||
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
|
||||
printf 'pending=%s\n' "$PEND_COUNT"
|
||||
printf 'events_total=%s\n' "$EV_TOTAL"
|
||||
printf 'events_bad=%s\n' "$EV_BAD"
|
||||
printf 'events_unpaired=%s\n' "$EV_UNPAIRED"
|
||||
printf 'events_running=%s\n' "$EV_RUNNING"
|
||||
printf 'events_rotated=%s\n' "$EV_ROTATED"
|
||||
printf 'events_file=%s\n' "$RD/events.raw"
|
||||
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 'contents_file=%s\n' "$RD/contents.tsv"
|
||||
printf 'contents_file=%s\n' "$RD/contents-entry.md"
|
||||
|
||||
[ "$OK_COUNT" -eq 0 ] && exit 3
|
||||
[ "$FAIL_COUNT" -gt 0 ] && exit 1
|
||||
|
||||
+74
-25
@@ -17,8 +17,9 @@
|
||||
# 3 這台機器沒有可用的排程機制:認不得作業系統,或 crontab 與 schtasks 都找不到
|
||||
# 4 排程操作失敗:讀不到現有排程(且失敗原因不是「沒有排程」)、寫入或刪除回非 0
|
||||
# 5 回讀驗證失敗:寫入回 0 但條目不在,或移除回 0 但條目還在,又或其他人的條目數量對不上
|
||||
# 6 用法錯誤:不認得的子命令、不認得的工作代號、缺參數、判不出要用哪一支 CLI 跑巡檢,
|
||||
# 或 --period 給的週期塞不進心跳的過期門檻
|
||||
# 6 用法錯誤:不認得的子命令、不認得的工作代號、缺參數、判不出要用哪一支 CLI 跑巡檢、
|
||||
# --period 給的週期塞不進心跳的過期門檻,或 JSC_HOME 解不出絕對路徑(條目裡的根目錄
|
||||
# 只要不是絕對路徑,那一輪就叫不到任何工具)
|
||||
#
|
||||
# --- 排程只叫巡檢,心跳由巡檢寫 ---
|
||||
#
|
||||
@@ -80,12 +81,29 @@
|
||||
# 被擋。頁寫不成就不寫心跳,於是排程裝著卻永遠空轉。所以條目自帶 JSC_GITEA_CONFIRM=yes:
|
||||
# 無人值守的那一輪本來就沒有人可以按同意,擋下來也沒有人會看到。
|
||||
#
|
||||
# --- 根目錄從條目餵進去,不由那一輪自己解 ---
|
||||
#
|
||||
# 巡檢那一輪要用字面絕對路徑叫工具,所以它得先知道根目錄。那一輪自己解不出來:解路徑的
|
||||
# 指令(`readlink -f "$JSC_HOME/current"`、`ls -d "$JSC_HOME/current"`)在無人值守的工作
|
||||
# 階段實測一律被擋,連 `ls -d /root/.jsc/current` 這種字面唯讀指令沒有允許規則也照擋。
|
||||
# 能寫成字面的話又不必解了。所以根目錄只能從外面餵進去。
|
||||
# 本腳本是在機器上、由人叫起來的,解得到根目錄,也解得起。install 於是把解好的字面根目錄
|
||||
# 寫進條目的提示文字,那一輪讀提示就拿得到,一個指令都不用跑。
|
||||
# 提示裡的格式固定是「工具根目錄={字面絕對路徑}」:技能靠這一段取值,人也讀得懂。
|
||||
# 寫進條目的一定是展開後的字面值,不是 $JSC_HOME:條目裡留變數,那一輪拿到的還是變數。
|
||||
#
|
||||
# --- 裝完要開哪些權限 ---
|
||||
#
|
||||
# 排程那一輪跑在沒有人的工作階段,跳出權限詢問就是卡住整輪,而且卡到鎖逾時才有下一輪。
|
||||
# install 成功之後會把那一輪需要的 allow 規則印出來,一行一條。
|
||||
# 路徑一律走 $JSC_HOME/current/{外掛名}:那是一組不帶版本的連結,指向該外掛在快取裡的最新
|
||||
# 版,由 deploy 維護。規則就是那組確切路徑,比對得準,外掛升版也不用回頭改設定。
|
||||
# 實測歸納出兩條判準,印規則一律照它走:
|
||||
# 一、無人值守時只有允許清單上的完整字面指令跑得動。沒有「預設安全的唯讀指令」這回事:
|
||||
# `ls -d` 這種指令一樣要有自己的規則,不然照擋。
|
||||
# 二、路徑中段的萬用字元不匹配。規則與指令都必須是完整字面,所以規則裡不寫版本號的
|
||||
# 萬用字元,也不寫 $JSC_HOME 或 ~。
|
||||
# 路徑一律走 $JSC_HOME/current/{外掛名},而且印出來的是展開後的字面值:那是一組不帶版本的
|
||||
# 連結,指向該外掛在快取裡的最新版,由 deploy 維護。規則就是那組確切路徑,比對得準,外掛
|
||||
# 升版也不用回頭改設定;改指到快取的實體路徑反而會因為帶版本號而每次升版都失效。
|
||||
# 檔案寫入只認 Edit(...),Write(...) 規則沒有作用,所以不印 Write。
|
||||
# 連結不在就先警告:那一輪會因為找不到工具而失敗。連結由 deploy 建,本腳本不代建——排程
|
||||
# 腳本自己去補外掛的部署結構,等於兩個地方管同一件事,壞掉的時候查不出是誰建的。
|
||||
@@ -157,6 +175,20 @@ die() { # $1=結束碼 $2=訊息
|
||||
|
||||
note() { printf '[jsc][助理排程]:%s\n' "$1" >&2; }
|
||||
|
||||
# 條目與允許規則共用的字面根目錄。兩邊共用同一個值是刻意的:規則放行哪一組路徑,那一輪就
|
||||
# 只能用哪一組路徑叫工具,兩邊各算各的就會差開,而差開的那一輪是被靜靜擋掉,沒有訊號。
|
||||
# 相對路徑一律先解成絕對。相對路徑寫進條目等於指向 cron 的工作目錄,那一輪叫不到任何工具。
|
||||
ROOT="$CURRENT"
|
||||
case "$ROOT" in
|
||||
/*) ;;
|
||||
*)
|
||||
_r=$(CDPATH= cd -- "$ROOT" 2>/dev/null && pwd -L) || _r=''
|
||||
[ -n "$_r" ] || die 6 "JSC_HOME 是相對路徑($JSC_HOME),$ROOT 也解不出絕對路徑。條目與允許規則都需要字面絕對路徑,請把 JSC_HOME 設成絕對路徑再跑一次。"
|
||||
ROOT="$_r" ;;
|
||||
esac
|
||||
# 條目提示文字裡的根目錄那一段。技能靠這一段取值,所以格式固定,不隨 CLI 變。
|
||||
ROOT_TOKEN="工具根目錄=$ROOT"
|
||||
|
||||
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序:先環境變數覆寫,再
|
||||
# $JSC_HOME/current 那一組連結,然後開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
||||
# current 排在快取前面是刻意的:技能與權限規則都以 current 為準,腳本內部再自己去挑另一個
|
||||
@@ -266,17 +298,18 @@ spec_of() {
|
||||
# 判不出 CLI,或那一支的執行檔不在 PATH 上,都回非 0 由主流程回 6,不猜。
|
||||
# 執行檔一律用 `command -v` 解成絕對路徑:cron 的 PATH 只有 /usr/bin 與 /bin,裸的指令名
|
||||
# 每一輪都是 not found,而那一輪不會有人看到錯誤訊息。
|
||||
# 每一支 CLI 的提示文字都接上根目錄那一段:那一輪自己解不出根目錄,只能從提示裡拿。
|
||||
patrol_command() {
|
||||
[ -n "$PATROL_CMD" ] && { printf '%s' "$PATROL_CMD"; return 0; }
|
||||
_cli="$CLI"
|
||||
[ -n "$_cli" ] || _cli="${JSC_CLI:-}"
|
||||
[ -n "$_cli" ] || { [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && _cli=claude; }
|
||||
case "$_cli" in
|
||||
claude) _bin='claude'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
codex) _bin='codex'; _args="exec '\$assistant 跑一輪巡檢'" ;;
|
||||
copilot) _bin='copilot'; _args='-p "跑一輪助理巡檢"' ;;
|
||||
antigravity) _bin='agy'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
kiro) _bin='kiro-cli'; _args='-p "跑一輪助理巡檢"' ;;
|
||||
claude) _bin='claude'; _args="-p \"/jsc-assist:assistant 跑一輪巡檢 $ROOT_TOKEN\"" ;;
|
||||
codex) _bin='codex'; _args="exec '\$assistant 跑一輪巡檢 $ROOT_TOKEN'" ;;
|
||||
copilot) _bin='copilot'; _args="-p \"跑一輪助理巡檢 $ROOT_TOKEN\"" ;;
|
||||
antigravity) _bin='agy'; _args="-p \"/jsc-assist:assistant 跑一輪巡檢 $ROOT_TOKEN\"" ;;
|
||||
kiro) _bin='kiro-cli'; _args="-p \"跑一輪助理巡檢 $ROOT_TOKEN\"" ;;
|
||||
*) printf '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。\n' >&2
|
||||
return 1 ;;
|
||||
esac
|
||||
@@ -422,7 +455,14 @@ case "$CMD:$JOBS" in
|
||||
die 6 "$_why"
|
||||
fi
|
||||
ENV_PREFIX=$(env_prefix "$TMPD/snapnames")
|
||||
SNAPSHOT_NAMES=$(cat "$TMPD/snapnames" 2>/dev/null) ;;
|
||||
SNAPSHOT_NAMES=$(cat "$TMPD/snapnames" 2>/dev/null)
|
||||
# 提示文字裡沒有根目錄那一段,無人值守那一輪就拿不到根目錄,只能停下回報。本腳本自己
|
||||
# 產生的指令一律帶著,所以會走到這裡的只有 --patrol-cmd 與 JSC_ASSIST_PATROL_CMD。
|
||||
# 這裡只警告不中止:自訂指令有可能根本不是叫這支技能,中止會把那條路擋掉。
|
||||
case "$PATROL_RESOLVED" in
|
||||
*"$ROOT_TOKEN"*) ;;
|
||||
*) note "自訂的巡檢指令裡沒有「$ROOT_TOKEN」。那一輪自己解不出根目錄,只能從提示文字拿,拿不到就會停下回報,什麼都不記。請把這一段原樣加進提示文字裡。" ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# --- 裝完要開的權限 ---
|
||||
@@ -436,13 +476,21 @@ print_allow_rules() {
|
||||
# gitea.sh 一定要有自己這一條。`Skill(jsc-gitea:wiki)` 只放行「叫用那支技能」,技能裡的
|
||||
# 每一個 Bash 呼叫仍然各自受檢,少了這一條,那一輪會在寫監控頁時靜靜被擋——頁寫不成就
|
||||
# 不寫心跳,外面只看得到心跳過期,看不出是權限擋的。
|
||||
# 目錄頁那一列改由 wiki-contents.sh 寫,所以它也要有自己這一條:巡檢那一輪會直接叫它,
|
||||
# 目錄頁那一個區塊改由 wiki-contents.sh 寫,所以它也要有自己這一條:巡檢那一輪會直接叫它,
|
||||
# 少了規則就會停在權限詢問,而那一輪沒有人可以按同意。
|
||||
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" \
|
||||
"$CURRENT/jsc-gitea/tools/wiki-contents.sh"; do
|
||||
# link-check.sh 同理:兩次寫入前都要先驗連結,少了這一條,驗證那一步就停在權限詢問,那一輪
|
||||
# 什麼都寫不成。它排在寫入之前,所以擋住它等於整輪報廢。
|
||||
# report-status.sh 是執行狀態事件那一支,這一輪會直接叫它兩次以上:排空與輪替由 patrol.sh
|
||||
# 代跑(那是已放行指令的子行程,不會再問一次),但收尾那一筆 skill-end 是技能自己用 Bash 叫的,
|
||||
# 那一次就要這一條規則。少了它,那一輪會停在最後一步的權限詢問,而排程那一輪沒有人可以按
|
||||
# 同意:那一輪的收尾事件寫不出去,start 永遠配不到 end,下一輪就把一輪其實做完的巡檢報成中止。
|
||||
for _s in "$ROOT/jsc-assist/tools/schedule.sh" \
|
||||
"$ROOT/jsc-assist/tools/patrol.sh" \
|
||||
"$ROOT/jsc-hooks/hooks/heartbeat.sh" \
|
||||
"$ROOT/jsc-hooks/tools/report-status.sh" \
|
||||
"$ROOT/jsc-gitea/tools/gitea.sh" \
|
||||
"$ROOT/jsc-gitea/tools/wiki-contents.sh" \
|
||||
"$ROOT/jsc-gitea/tools/link-check.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"
|
||||
@@ -459,9 +507,9 @@ print_allow_rules() {
|
||||
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
|
||||
[ -e "$ROOT/$_p" ] && continue
|
||||
if [ -z "$_miss" ]; then _miss="$_p"; _paths="$ROOT/$_p"
|
||||
else _miss="$_miss,$_p"; _paths="$_paths、$ROOT/$_p"; fi
|
||||
done
|
||||
if [ -n "$_miss" ]; then
|
||||
printf 'current_links=missing:%s\n' "$_miss"
|
||||
@@ -499,7 +547,7 @@ schtasks_install() {
|
||||
printf 'installed=%s task=%s\n' "$_job" "$_tn"
|
||||
_rc=0
|
||||
done
|
||||
printf 'ttl=%s period=%s legacy_removed=%s log=%s\n' "$TTL" "$PERIOD" "$_legacy" "$LOG"
|
||||
printf 'ttl=%s period=%s legacy_removed=%s patrol_root=%s log=%s\n' "$TTL" "$PERIOD" "$_legacy" "$ROOT" "$LOG"
|
||||
return "$_rc"
|
||||
}
|
||||
|
||||
@@ -558,8 +606,8 @@ crontab_install() {
|
||||
for _job in $JOBS; do
|
||||
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job" | mask_secret)"
|
||||
done
|
||||
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")" "${SNAPSHOT_NAMES:-無}"
|
||||
printf 'dryrun=crontab action=write ttl=%s period=%s legacy_removed=%s others_kept=%s total_lines=%s env_snapshot=%s patrol_root=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_others" "$(count_lines "$_new")" "${SNAPSHOT_NAMES:-無}" "$ROOT"
|
||||
printf -- '--- 寫回後的 crontab ---\n'
|
||||
mask_secret <"$_new"
|
||||
return 0
|
||||
@@ -583,8 +631,8 @@ crontab_install() {
|
||||
for _job in $JOBS; do
|
||||
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job" | mask_secret)"
|
||||
done
|
||||
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s env_snapshot=%s log=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_kept" "${SNAPSHOT_NAMES:-無}" "$LOG"
|
||||
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s env_snapshot=%s patrol_root=%s log=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_kept" "${SNAPSHOT_NAMES:-無}" "$ROOT" "$LOG"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -661,7 +709,8 @@ case "$CMD" in
|
||||
# 一樣要開權限,只是還要先把 cron 服務叫起來。
|
||||
print_allow_rules
|
||||
check_current_links
|
||||
note '上面這幾條 allow 規則要先開,排程那一輪才不會停在權限詢問——那一輪沒有人可以按同意。規則放行的是 $JSC_HOME/current 那一組路徑,巡檢也只能用那一組路徑叫工具。檔案寫入只認 Edit,Write 規則沒有作用。'
|
||||
note "上面這幾條 allow 規則要先開,排程那一輪才不會停在權限詢問——那一輪沒有人可以按同意。規則與指令都要是完整字面:無人值守時只有清單上的完整字面指令跑得動,沒有預設放行的唯讀指令;路徑中段的萬用字元也不匹配,版本號寫成 * 的規則一樣擋。規則放行的是 $ROOT 那一組路徑,巡檢也只能用那一組路徑叫工具。檔案寫入只認 Edit,Write 規則沒有作用。"
|
||||
note "條目的提示文字帶著「$ROOT_TOKEN」:那一輪自己解不出根目錄,解路徑的指令在無人值守時一樣被擋,所以根目錄由這一筆條目餵進去。這一段被改掉或刪掉,那一輪會停下回報,什麼都不記。"
|
||||
note "條目帶著安裝當下的環境變數快照(${SNAPSHOT_NAMES:-無}),其中含 Gitea 金鑰:crontab 檔案請保持只有本人讀得到。這幾個變數改過就要重跑一次 install,條目才會跟著換。"
|
||||
[ "$DRYRUN" -eq 1 ] && exit 0
|
||||
_svc=$(service_state)
|
||||
|
||||
Reference in New Issue
Block a user