收尾寫一筆 skill-end 事件,執行狀態才回報得到助理 #12

Merged
admin merged 2 commits from feat/status-report into develop 2026-09-02 08:04:03 +00:00
7 changed files with 390 additions and 39 deletions
Showing only changes of commit 3dac1475df - Show all commits
+2 -2
View File
@@ -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`。工具一律用 `$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` 先移除排程再清掉心跳,順序不能反。這支不參與閘門判定、不做決策、巡檢那一路全程不問人。
<!-- JSC-SKILLS:END -->
@@ -44,7 +44,7 @@ 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/patrol.sh` | 一輪巡檢的收攏與收口。三個子命令:`collect` 取鎖、讀五項來源、組出監控頁的「最新一輪」與「近 24 輪摘要」兩塊、本輪的摘要列與目錄頁那一列(那一列的第一欄是連結,網址留佔位,等監控頁寫成之後由呼叫端用 `gitea.sh wiki-url` 的絕對網址換掉;第 2 欄是裸 HASH,upsert 拿那一欄當鍵);`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` 印的那一個,寫入前先過 `jsc-gitea/tools/link-check.sh`、結束碼 0 才寫;但那一格含主機位址與網址編碼,會變,所以不當鍵 |
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。頁面固定三塊:本頁基本資料建頁時寫一次就不動、最新一輪每輪整塊換掉、近 24 輪摘要一輪一列且最新的在最上面。軌跡留在摘要表,完整內容只留最新一輪,頁面才讀得完 |
File diff suppressed because one or more lines are too long
+50 -10
View File
@@ -1,6 +1,6 @@
---
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. 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).'
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 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. 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
@@ -11,7 +11,7 @@ The background assistant runs where nobody is watching it. Its heartbeat is the
`$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.
`$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.
`$JSC_HOME/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.
@@ -24,13 +24,14 @@ Every tool below is addressed through `$JSC_HOME/current/{plugin}`, and `$JSC_HO
| 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 status event stream | `$JSC_HOME/current/jsc-hooks/tools/report-status.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` |
| the link check every write depends on | `$JSC_HOME/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, the directory row 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 six 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.
@@ -58,6 +59,24 @@ A round with no link to write skips the call and says so; a round that cannot ve
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:
`$JSC_HOME/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 row 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 |
@@ -68,6 +87,8 @@ Run exactly one operation per invocation. Take it from the request: starting, la
| `$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/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.
@@ -133,16 +154,29 @@ The failure this design buys is the one worth having: a round that cannot read i
| 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 |
## 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.
`$JSC_HOME/current/jsc-assist/tools/patrol.sh collect` owns the whole of it — it calls `$JSC_HOME/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
One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3 are results, not aborts.** A round with failed items still has a result to record, and refusing to record it would hide the failure instead of showing it.
| Code | Meaning | What to do |
| --- | --- | --- |
| 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 |
| 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 |
@@ -179,7 +213,7 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
| Step 3 | Notice |
| --- | --- |
| exit 0 | 助理已啟動,第一輪巡檢跑完了,結果寫上監控頁了,心跳也寫了。排程接上了,之後每 {period} 分鐘跑一輪,每一輪跑完才寫一次心跳。心跳新鮮代表上一輪巡檢真的做完了;那一輪四項有沒有全過,看監控頁的本輪判定。 |
| exit 0 | 助理已啟動,第一輪巡檢跑完了,結果寫上監控頁了,心跳也寫了。排程接上了,之後每 {period} 分鐘跑一輪,每一輪跑完才寫一次心跳。心跳新鮮代表上一輪巡檢真的做完了;那一輪各項有沒有全過,看監控頁的本輪判定。 |
| exit 1 | 助理已啟動,第一輪巡檢跑完了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 `sudo service cron start`,重開 WSL 之後要再跑一次。 |
| 其他結束碼 | 助理已啟動,第一輪巡檢跑完了,但排程沒接上(結束碼 {code})。不會再有下一輪,心跳過了 {ttl} 秒就會過期,屆時請再跑一次 start。 |
@@ -187,10 +221,12 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
## 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.
**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 `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report that the round found its results but has nowhere to put them, name `jsc-gitea` as missing, and stop.
**Never invent a page name, 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.
@@ -201,7 +237,7 @@ 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 |
**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 `$JSC_HOME/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 `$JSC_HOME/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.
@@ -233,7 +269,11 @@ One round: read four sources, record the result, then beat. Everything before th
5. **Write the heartbeat.** Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh finish --round {round}`. This is the last step for a reason: it is the only thing that turns a fresh heartbeat into a true statement. Judge the exit code by the patrol.sh table — 2, 4 and 5 all mean the round is recorded but unproven, and each has its own report line there. Completion condition: `finish` exited 0, or the failure was reported as "recorded but no heartbeat" with its code.
6. **Report the round.** Print the round verdict and, when it is `警示`, the `warn_sources=` text that says why — a round can read all four sources and still come out `警示`, and that column is the only place the reason appears; then one line per item with its `status=` and, for a failure, its `note=`; the monitor page name, 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 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 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.
**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
@@ -260,7 +300,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` 把它清掉 |
+9
View File
@@ -27,6 +27,15 @@
| 待辦筆數 | 待辦簿現有筆數 | 心跳新鮮而筆數為 0,代表助理空轉,沒有東西可跑 |
| 連續失敗項 | 待辦簿裡 `fail_count` 大於 0 的筆數 | 待辦簿的項目失敗不會自動暫停,每輪都重試。這一欄讓壞掉的項目在目錄頁就現形 |
### 為什麼沒有「本輪非 ok 事件數」這一欄
執行狀態事件的筆數只放在監控頁的「執行狀態事件」那一節,這一頁不加欄。兩個理由:
- **這一頁的欄不是自己一台機器說了算。** 每一列是一台機器,欄位卻是共用的:表頭跟著建頁的那一台走,之後每一台只更新自己那一列。新加一欄,只有跑到新版的機器會寫出多一格的列,其餘機器的列還是舊的格數,表頭也還是舊的——同一張表混著兩種格數,多出來的那一格對不到任何欄名。目錄頁沒有整頁改寫的路可以走:整頁覆蓋等於刪掉別台機器的紀錄。
- **這個數字離開監控頁就會被讀錯。** 它算的是「上一次排空之後到這一輪之間」的事件,視窗長度隨巡檢週期與上一輪的成敗變動。放在監控頁上,同一節裡就有事件總數、未配對的 `start` 與明細表可以對照;抽一個數字放到目錄頁,0 會被讀成「這台機器很健康」,但它同樣可能只是那一段時間沒有任何技能跑過。
要判斷一台機器有沒有問題,這一頁上的「心跳」與「最後巡檢」就夠帶人往下翻;細節一律回監控頁看。
## 寫入規則
這一頁是共用目錄,別台機器的列一律原樣保留。寫入一律用 `jsc-gitea/tools/wiki-contents.sh upsert`,不手工改頁。
+43 -6
View File
@@ -8,7 +8,7 @@
```mermaid
flowchart LR
A[巡檢一輪] --> B[收攏四項結果]
A[巡檢一輪] --> B[收攏各項結果]
B --> C[讀回舊頁]
C --> D[換掉最新一輪那一塊]
D --> E[本輪摘要列插到表格最上面,截到 24 列]
@@ -35,7 +35,7 @@ flowchart LR
這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。
六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
七個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。
| 項目 | 內容 |
| --- | --- |
@@ -58,7 +58,7 @@ flowchart LR
這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一頁寫成之後才寫。
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看這一塊上面的「本輪判定」。
心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪各項都成功——那要看這一塊上面的「本輪判定」。
### 技能與呼叫鏈使用統計
@@ -68,6 +68,40 @@ 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。
@@ -116,17 +150,19 @@ flowchart LR
| 項目 | 來源子節 | 建議入口 |
| --- | --- | --- |
| {一句話講完要處理什麼} | {上面六個子節之一} | {技能名或指令} |
| {一句話講完要處理什麼} | {上面七個子節之一} | {技能名或指令} |
## 近 24 輪摘要
一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。
| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |
| 巡檢時間 | 本輪判定 | 各項成敗 | 待人處理 | 警示來源 |
| --- | --- | --- | ---: | --- |
| {yyyy-MM-dd HH:mm} | {正常、警示、異常 三選一} | {成功項數}/{總項數} | {待人處理筆數} | {警示原因,多個用頓號串;沒有就寫「無」} |
「警示來源」那一欄不能省。四項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄是 4/4,沒有這一欄的話,看的人不知道警示哪來。理由要短,一眼讀完,像「心跳過期」「版本查詢失敗」「重啟閘門未清」「上一輪逾時被接手」。
「警示來源」那一欄不能省。各項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄是滿分,沒有這一欄的話,看的人不知道警示哪來。理由要短,一眼讀完,像「心跳過期」「版本查詢失敗」「重啟閘門未清」「上一輪逾時被接手」「有技能只有 start 沒有 end」。
第三欄的欄名寫「各項成敗」,不寫項數。巡檢項目會增加,欄名寫死數字就要跟著改,而舊頁那些列的欄名不會跟著改,同一張表就會有兩種欄名。
## 寫入規則
@@ -141,3 +177,4 @@ flowchart LR
- 整頁寫成之後,才回頭更新目錄頁自己那一列,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`,別台機器的列一個字都不動。目錄頁那一欄的連結同樣先驗過才寫。
- 這一頁沒寫成就不寫心跳,讓它過期。心跳代表的是「這一輪的結果記在這一頁上了」。
- 目錄頁只是索引。目錄頁的存取庫沒設定(結束碼 3)時照樣寫心跳,並把那一筆列進待人處理;其餘寫入失敗才不寫心跳。
- 執行狀態事件那一節的內容由 `tools/patrol.sh collect` 排空、彙整好,寫頁的人原樣採用,不自己再跑一次 `drain`。`drain` 是消耗性讀取:它一讀完就把位移往前推,同一批事件不會再出現第二次,第二次跑只會拿到 3,或者把下一輪的事件提前吃掉。
+277 -17
View File
@@ -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 的既有缺陷照實記 ---
#
@@ -100,6 +103,30 @@
# 列,舊列從此不再更新。這一頁每 15 分鐘寫一次,重複列累積得很快。裸 HASH 只由
# {主機名}/{登入帳號} 決定,上面三件事都動不到它。
#
# --- 執行狀態事件為什麼由這支排空 ---
#
# 事件流記的是每一支技能與每一支 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 的輸出 ---
#
# stdout 是 key=value,一行一個鍵,供呼叫端逐行取值。監控頁要用的 markdown 不印在
@@ -113,10 +140,17 @@
# item= 一項一行,欄位 status(ok、empty、fail)、rc、note
# verdict= 正常、警示、異常
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。四項全過卻判成警示
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示
# 時,原因只寫在這裡
# 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= 只有本輪那一列,方便直接插到既有表格最上面
@@ -133,6 +167,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
@@ -141,6 +176,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)
@@ -173,7 +211,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
@@ -592,7 +630,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
@@ -603,6 +641,216 @@ 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() {
@@ -646,9 +894,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
@@ -657,6 +905,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'
@@ -672,8 +921,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"
@@ -682,7 +933,7 @@ compose() {
{
printf '## 近 24 輪摘要\n\n'
printf '一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。\n\n'
printf '| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |\n'
printf '| 巡檢時間 | 本輪判定 | 各項成敗 | 待人處理 | 警示來源 |\n'
printf '| --- | --- | --- | ---: | --- |\n'
cat "$RD/summary-row.md"
} >"$RD/summary.md"
@@ -697,7 +948,7 @@ compose() {
printf '> 目錄頁 `MONITOR_CONTENTS` 在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和這頁不同庫。\n'
printf '> 那一頁只更新自己那一列,別台機器的列一個字都不動,寫入交給 `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'
@@ -783,8 +1034,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
@@ -806,12 +1059,19 @@ 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"
+5
View File
@@ -440,9 +440,14 @@ print_allow_rules() {
# 少了規則就會停在權限詢問,而那一輪沒有人可以按同意。
# link-check.sh 同理:兩次寫入前都要先驗連結,少了這一條,驗證那一步就停在權限詢問,那一輪
# 什麼都寫不成。它排在寫入之前,所以擋住它等於整輪報廢。
# report-status.sh 是執行狀態事件那一支,這一輪會直接叫它兩次以上:排空與輪替由 patrol.sh
# 代跑(那是已放行指令的子行程,不會再問一次),但收尾那一筆 skill-end 是技能自己用 Bash 叫的,
# 那一次就要這一條規則。少了它,那一輪會停在最後一步的權限詢問,而排程那一輪沒有人可以按
# 同意:那一輪的收尾事件寫不出去,start 永遠配不到 end,下一輪就把一輪其實做完的巡檢報成中止。
for _s in "$CURRENT/jsc-assist/tools/schedule.sh" \
"$CURRENT/jsc-assist/tools/patrol.sh" \
"$CURRENT/jsc-hooks/hooks/heartbeat.sh" \
"$CURRENT/jsc-hooks/tools/report-status.sh" \
"$CURRENT/jsc-gitea/tools/gitea.sh" \
"$CURRENT/jsc-gitea/tools/wiki-contents.sh" \
"$CURRENT/jsc-gitea/tools/link-check.sh"; do