feat(schedule): 助理的系統排程接線
What: - 新增 tools/schedule.sh,三個子命令:install 安裝、remove 移除、status 查現況。兩筆工作分別是每分鐘的心跳與每十五分鐘的巡檢。 - 助理主體的 start 接上安裝、stop 接上移除,行為清單與 README 跟著更新。 Why: - 心跳要有人定時寫,不然啟動之後過幾分鐘就自己過期,助理看起來像沒在跑。 - 排程這件事有標準的輸入與輸出,準則要求下放腳本,技能只描述何時呼叫。 How: - 條目行尾帶固定標記,安裝先濾掉自己的舊條目再追加。絕不覆寫整份排程,別人的條目一行都不碰;寫完回讀驗證,出現兩筆或別人的行數對不上就報錯。 - 裝完檢查排程服務在不在跑。這台機器是 WSL,預設不啟動 cron,裝了條目卻一次都不會執行——不檢查就會宣稱一件沒發生的事。 - 條目一律帶 </dev/null。排程是非互動環境,任何等輸入的東西都會把整筆卡死。 - 輸出寫到 JSC_HOME 底下,不寫進任何專案目錄,免得多出未追蹤檔污染別人的變更盤點。 - 巡檢那一筆預設不裝,要明著指定才會裝。巡檢本體還沒實作,裝了只會每十五分鐘失敗一次;巡檢指令也不猜 CLI,判不出來就停下,猜錯的代價是每十五分鐘跑一支不存在的執行檔。 - stop 先移除排程再清心跳,順序不能反。反過來的話清完下一分鐘排程就補寫一次,stop 等於騙人。 Who: 助理落地的第三塊。這裡留下一個要在巡檢那一輪解掉的問題:心跳目前由排程直接寫,所以心跳新鮮只證明排程活著,不證明助理做了事。往後應該改由巡檢跑完那一輪去寫,心跳才等於工作訊號。限制已寫進技能內文與說明文件。
This commit is contained in:
@@ -26,7 +26,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
### `assistant`
|
### `assistant`
|
||||||
|
|
||||||
助理主體,三個操作:`start` 啟動、`status` 查現況、`stop` 停止。心跳的寫入、判定與清除一律交給 `jsc-hooks` 的 `hooks/heartbeat.sh`,判定只有那一份。`start` 寫下第一次心跳,並講明排程還沒接線、心跳不會自動更新。`status` 全程唯讀,讀心跳現況與待辦簿,印成一張表;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 清掉心跳,並講明心跳清掉之後閘門會擋下技能呼叫、但閘門目前還沒接線。這支不碰 wiki、不參與閘門判定。
|
助理主體,三個操作:`start` 啟動、`status` 查現況、`stop` 停止。心跳的寫入、判定與清除一律交給 `jsc-hooks` 的 `hooks/heartbeat.sh`,判定只有那一份;系統排程一律交給 `tools/schedule.sh`,技能自己不碰 crontab 與 schtasks。`start` 寫下第一次心跳,再裝上每分鐘寫一次心跳的排程;cron 服務沒在跑就照實講條目不會被執行。`status` 全程唯讀,讀心跳、排程與待辦簿,印成三塊;助理沒在跑就印「助理未運行」,不當成錯誤。`stop` 先移除排程再清掉心跳,順序不能反——反了下一分鐘 cron 會再寫一次心跳。這支不碰 wiki、不參與閘門判定。心跳新鮮只證明排程活著,不證明助理做了事。
|
||||||
|
|
||||||
<!-- JSC-SKILLS:END -->
|
<!-- JSC-SKILLS:END -->
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
| 檔案 | 用途 |
|
| 檔案 | 用途 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `tools/schedule.sh` | 助理系統排程的安裝、移除與查現況。三個子命令 `install`、`remove`、`status`,兩筆工作 `heartbeat`(每 60 秒)與 `patrol`(每 15 分鐘)。Linux、WSL 與 macOS 走 crontab,Windows 走 schtasks。條目行尾帶固定標記 `# jsc-assist:assistant {工作}`,只動自己那一筆,別人的排程一行都不碰。裝完會檢查排程服務在不在跑,沒跑就回 1——WSL 預設不啟動 cron。巡檢那一筆預設不裝,巡檢本體還沒實作。`--dry-run` 只印組出來的條目與寫回後的內容,什麼都不動 |
|
||||||
| `references/behaviors.md` | 本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 `plugins/meta` 的 `references/guidelines.md`「技能行為清單」 |
|
| `references/behaviors.md` | 本 domain 的技能行為清單:一支技能一節,五列記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,供稽核與驗證比對。格式合約見 `plugins/meta` 的 `references/guidelines.md`「技能行為清單」 |
|
||||||
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本。一列代表一台機器,雜湊來源是 `{主機名}/{登入帳號}`。寫入語意是**只更新自己那一列**:比對主機與帳號兩欄,別台機器的列原樣保留,禁止整頁覆蓋 |
|
| `templates/monitor-contents.md` | 目錄頁 `MONITOR_CONTENTS` 的範本。一列代表一台機器,雜湊來源是 `{主機名}/{登入帳號}`。寫入語意是**只更新自己那一列**:比對主機與帳號兩欄,別台機器的列原樣保留,禁止整頁覆蓋 |
|
||||||
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。寫入語意與目錄頁相反,是**一律附加一節、不覆寫**:一次巡檢一節,節標題帶時間戳,既有的節一個字都不動 |
|
| `templates/monitor-page.md` | 內容頁 `MONITOR_{HASH}` 的範本。記的是這台機器的巡檢軌跡。寫入語意與目錄頁相反,是**一律附加一節、不覆寫**:一次巡檢一節,節標題帶時間戳,既有的節一個字都不動 |
|
||||||
@@ -55,6 +56,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `heartbeat` | 心跳檔,欄位 `ts`、`pid`、`cli`、`session`。判準只看 `ts`,不看 pid 存活——五支 CLI 與容器裡的行程互相看不到彼此的 pid |
|
| `heartbeat` | 心跳檔,欄位 `ts`、`pid`、`cli`、`session`。判準只看 `ts`,不看 pid 存活——五支 CLI 與容器裡的行程互相看不到彼此的 pid |
|
||||||
| `tasks/{id}` | 待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
|
| `tasks/{id}` | 待辦簿,一筆一檔。一筆一檔是為了讓並行寫入不互相覆寫 |
|
||||||
|
| `schedule.log` | 排程條目的輸出。刻意放在存取庫外面:寫進專案會多出未追蹤檔,污染別人的變更盤點 |
|
||||||
|
|
||||||
## 相關 domain
|
## 相關 domain
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 觸發時機 | 要啟動助理、要停止助理,或要問助理現在還在不在跑、待辦簿剩下哪幾筆時用。三個操作 `start`、`status`、`stop` 都走這一支。執行環境健檢不走這支,走 `jsc-cli:doctor`。技能使用次數不走這支,走 `jsc-log:stats` |
|
| 觸發時機 | 要啟動助理、要停止助理,或要問助理現在還在不在跑、待辦簿剩下哪幾筆時用。三個操作 `start`、`status`、`stop` 都走這一支。執行環境健檢不走這支,走 `jsc-cli:doctor`。技能使用次數不走這支,走 `jsc-log:stats` |
|
||||||
| 關鍵步驟 | 先認出使用者要的是哪一個操作。`start`:跑 `heartbeat.sh write` 寫第一次心跳、跑 `heartbeat.sh report` 確認寫進去了、印出心跳路徑與時間並註明排程還沒接線、心跳過了門檻要再跑一次 start。`status`:跑 `heartbeat.sh report` 取心跳現況、把 `state` 對映成新鮮、過期、心跳檔損壞、不存在、不自己解析心跳檔也不自己判定、從 `file=` 解出助理目錄後列出 `tasks/` 底下每一個檔案並解析 `state`、`title`、`next_run`、`fail_count`、印成一張表、`fail_count` 大於 0 的列標上「已連續失敗 N 次」。`stop`:先跑 `heartbeat.sh report` 留下原本的狀態、再跑 `heartbeat.sh clear` 清掉心跳、印出停止訊息並說明心跳清掉之後閘門會擋人、同時說明閘門還沒接線所以現在擋不到人 |
|
| 關鍵步驟 | 先認出使用者要的是哪一個操作。`start`:跑 `heartbeat.sh write` 寫第一次心跳、跑 `heartbeat.sh report` 確認寫進去了、跑 `tools/schedule.sh install heartbeat` 裝每分鐘那一筆排程、依結束碼選一段收尾訊息印出——排程接上、排程寫進去了但 cron 沒在跑、排程沒接上三種各一段。巡檢那一筆預設不裝,要呼叫端指名 `patrol` 或 `all` 才裝,而且要先講明巡檢本體還沒實作、裝了每一輪都會失敗。`status`:跑 `heartbeat.sh report` 取心跳現況、把 `state` 對映成新鮮、過期、心跳檔損壞、不存在、不自己解析心跳檔也不自己判定、從 `file=` 解出助理目錄後列出 `tasks/` 底下每一個檔案並解析 `state`、`title`、`next_run`、`fail_count`、跑 `tools/schedule.sh status` 取排程現況、印成心跳、排程、待辦三塊、`fail_count` 大於 0 的列標上「已連續失敗 N 次」、心跳與排程兜起來會誤讀的三種組合各補一句話。`stop`:先跑 `heartbeat.sh report` 留下原本的狀態、再跑 `tools/schedule.sh remove all` 移除排程、最後才跑 `heartbeat.sh clear` 清掉心跳、印出停止訊息並說明心跳清掉之後閘門會擋人、同時說明閘門還沒接線所以現在擋不到人 |
|
||||||
| 外部呼叫 | `jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。呼叫端沒講清楚要哪一個操作時,走 `jsc-ask:ask` 的決策樹問。不碰 wiki、不參與閘門判定 |
|
| 外部呼叫 | `jsc-hooks/hooks/heartbeat.sh` 的 `write`、`report`、`clear` 三個子命令,六個結束碼各有處置:0 往下走、1 與 3 印「助理未運行」、2 回報判不出狀態並停下、4 當成不新鮮並回報心跳檔損壞、5 是嚴重狀況要吵出來且不得回報成功、6 是呼叫寫錯要更正後重跑。本 domain 的 `tools/schedule.sh` 的 `install`、`remove`、`status` 三個子命令,七個結束碼各有處置:0 往下走、1 是條目裝了但 cron 沒在跑要照實講不會執行、2 是缺 jsc-hooks、3 是這台機器沒有排程機制、4 是排程操作失敗要原樣引用 stderr、5 是回讀驗證失敗要叫人自己去看 `crontab -l`、6 是呼叫寫錯。crontab 與 schtasks 一律經那支腳本,技能自己不碰。另外唯讀 `$JSC_HOME/assistant/tasks/` 底下的檔案。呼叫端沒講清楚要哪一個操作時,走 `jsc-ask:ask` 的決策樹問。不碰 wiki、不參與閘門判定 |
|
||||||
| 完成條件 | `start` 要 `write` 回 0 且 `report` 回 `state=fresh`,才算啟動成功;`write` 回 5 一律回報失敗並停下,不得宣稱啟動。`status` 要印出現況表,或印出「助理未運行」並說明原因;心跳不存在、待辦簿目錄不存在、待辦簿零筆,三種都算正常結束。`stop` 要 `clear` 回 0,並印出帶兩段話的停止訊息;`clear` 回 5 就回報心跳檔還在、助理沒有確實停掉,不印停止訊息 |
|
| 完成條件 | `start` 要 `write` 回 0 且 `report` 回 `state=fresh`,才算啟動成功;`write` 回 5 一律回報失敗並停下,不得宣稱啟動;`schedule.sh install` 回 1 要講明條目不會被執行與 `sudo service cron start`,不得宣稱排程會定時執行。`status` 要印出現況表,或印出「助理未運行」並說明原因;心跳不存在、待辦簿目錄不存在、待辦簿零筆、排程沒裝,四種都算正常結束。`stop` 要 `schedule.sh remove all` 先回 0、`clear` 再回 0,並印出帶三段話的停止訊息;`remove` 非 0 就回報排程還在、助理停不掉,不清心跳也不印停止訊息;`clear` 回 5 就回報心跳檔還在、助理沒有確實停掉,不印停止訊息 |
|
||||||
| 可驗證跡象 | `start` 之後 `$JSC_HOME/assistant/heartbeat` 存在,`ts` 是剛才的時間。`stop` 之後同一個路徑不存在。`status` 無寫入跡象,只有回報內容。三個操作都不動 `tasks/` 底下的檔案,也不動 worktree 與 wiki 頁 |
|
| 可驗證跡象 | `start` 之後 `$JSC_HOME/assistant/heartbeat` 存在,`ts` 是剛才的時間,`crontab -l` 找得到一筆帶 `# jsc-assist:assistant heartbeat` 的條目,而且只有一筆。`stop` 之後心跳路徑不存在,`crontab -l` 找不到任何 `# jsc-assist:assistant` 條目。兩者都不動別人的排程條目,條目數量前後相同。`status` 無寫入跡象,只有回報內容。三個操作都不動 `tasks/` 底下的檔案,也不動 worktree 與 wiki 頁。排程的 log 一律在 `$JSC_HOME/assistant/schedule.log`,不落在任何存取庫 |
|
||||||
|
|||||||
+76
-17
@@ -1,23 +1,26 @@
|
|||||||
---
|
---
|
||||||
name: assistant
|
name: assistant
|
||||||
description: Start, inspect, or stop the background assistant, with jsc-hooks/hooks/heartbeat.sh owning the single freshness verdict. start writes the first heartbeat and says no scheduler keeps it alive yet; status turns heartbeat.sh report plus the task book under $JSC_HOME/assistant/tasks/ into one read-only table; stop clears the heartbeat and states what a cleared heartbeat means for the jsc skill gate. A heartbeat that cannot be written or cleared (exit 5) is reported as a failure, never as success. Use when someone starts the assistant, stops it, or asks whether it is running and what is queued; not for environment health checks (jsc-cli:doctor), not for skill usage counts (jsc-log:stats).
|
description: Start, inspect, or stop the background assistant, with jsc-hooks/hooks/heartbeat.sh owning the single freshness verdict and tools/schedule.sh owning the system scheduler. start writes the first heartbeat and installs the one-minute heartbeat entry through crontab or schtasks; status turns heartbeat.sh report, schedule.sh status and the task book under $JSC_HOME/assistant/tasks/ into one read-only table; stop removes the schedule first, then clears the heartbeat, and states what a cleared heartbeat means for the jsc skill gate. Both scripts only ever touch their own marked entry, never the rest of the user's crontab. A heartbeat that cannot be written or cleared, and a schedule installed on a machine whose cron service is not running, are reported as failures, never as success. Use when someone starts the assistant, stops it, or asks whether it is running and what is queued; not for environment health checks (jsc-cli:doctor), not for skill usage counts (jsc-log:stats).
|
||||||
---
|
---
|
||||||
|
|
||||||
# assistant — start, status, stop
|
# assistant — start, status, 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 three operations that touch that evidence: `start` writes it, `status` reads it, `stop` clears it.
|
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 three operations that touch that evidence: `start` writes it, `status` reads it, `stop` clears it.
|
||||||
|
|
||||||
`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. This skill adds no `tools/` script of its own: the heartbeat logic already lives in that script, and the task book is one directory listing.
|
`jsc-hooks/hooks/heartbeat.sh` owns every heartbeat operation, including the freshness verdict. Never read, parse, write or delete `$JSC_HOME/assistant/heartbeat` directly — one verdict, one source.
|
||||||
|
|
||||||
|
`tools/schedule.sh` owns every system-scheduler operation: installing an entry, removing it, and reading which entries exist. Never call `crontab` or `schtasks` from this skill, and never edit a crontab by hand. Both flows have fixed inputs and outputs, so both live in scripts; the task book is the only thing this skill reads for itself, and that is one directory listing.
|
||||||
|
|
||||||
## Pick the operation
|
## Pick the operation
|
||||||
|
|
||||||
Run exactly one operation per invocation. Take it from the request: starting, launching or waking the assistant is `start`; asking whether it runs, what it is doing, or what is queued is `status`; stopping, halting or shutting it down is `stop`. When the request names none of the three, or names more than one, ask through the `jsc-ask:ask` decision tree with those three as the options, each stating its effect — `start` writes a heartbeat, `status` changes nothing, `stop` deletes the heartbeat. Never guess, and never run a second operation the caller did not ask for. Completion condition: exactly one of `start`, `status`, `stop` is chosen and named in the report.
|
Run exactly one operation per invocation. Take it from the request: starting, launching or waking the assistant is `start`; asking whether it runs, what it is doing, or what is queued is `status`; stopping, halting or shutting it down is `stop`. When the request names none of the three, or names more than one, ask through the `jsc-ask:ask` decision tree with those three as the options, each stating its effect — `start` writes a heartbeat and installs the scheduled entry that keeps writing it, `status` changes nothing, `stop` removes that entry and deletes the heartbeat. Never guess, and never run a second operation the caller did not ask for. Completion condition: exactly one of `start`, `status`, `stop` is chosen and named in the report.
|
||||||
|
|
||||||
## Data sources
|
## Data sources
|
||||||
|
|
||||||
| Path | Read by | Format |
|
| Path | Read by | Format |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `$JSC_HOME/assistant/heartbeat` | `heartbeat.sh` only, never this skill | `key=value` lines: `ts`, `pid`, `cli`, `session` |
|
| `$JSC_HOME/assistant/heartbeat` | `heartbeat.sh` only, never this skill | `key=value` lines: `ts`, `pid`, `cli`, `session` |
|
||||||
|
| `$JSC_HOME/assistant/schedule.log` | nobody here — the scheduled entries append to it | free text; point the operator at it when a scheduled round misbehaves |
|
||||||
| `$JSC_HOME/assistant/tasks/{id}` | this skill, read-only | `key=value` lines, one task per file: `id`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`) |
|
| `$JSC_HOME/assistant/tasks/{id}` | this skill, read-only | `key=value` lines, one task per file: `id`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`) |
|
||||||
|
|
||||||
`$JSC_HOME` defaults to `~/.jsc`. `heartbeat.sh report` prints the resolved heartbeat path in its `file=` field, so take the assistant directory from there rather than rebuilding it.
|
`$JSC_HOME` defaults to `~/.jsc`. `heartbeat.sh report` prints the resolved heartbeat path in its `file=` field, so take the assistant directory from there rather than rebuilding it.
|
||||||
@@ -38,32 +41,74 @@ Every call in every operation below is judged by this table. Report the code you
|
|||||||
| 5 | Filesystem failure — `write` could not write the file, or `clear` could not delete it and the file is still there | Serious. Report it loudly with the stderr text and the path, and follow the operation's own step for this code. Never report the operation as done |
|
| 5 | Filesystem failure — `write` could not write the file, or `clear` could not delete it and the file is still there | Serious. Report it loudly with the stderr text and the path, and follow the operation's own step for this code. Never report the operation as done |
|
||||||
| 6 | Usage error — an unknown subcommand, or none at all | This is a defect in the call, not a state of the assistant. Report the exact command line that was run, correct it to one of `write`, `check`, `report`, `clear`, 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 none at all | This is a defect in the call, not a state of the assistant. Report the exact command line that was run, correct it to one of `write`, `check`, `report`, `clear`, and run it once more. Report a second exit 6 as a defect in this skill and stop |
|
||||||
|
|
||||||
|
## The scheduler
|
||||||
|
|
||||||
|
Nothing in a background assistant runs on its own. The system scheduler is what makes it periodic, and `tools/schedule.sh` is the only thing here that touches it. Two jobs exist, each written as exactly one entry carrying the fixed marker `# jsc-assist:assistant {job}`:
|
||||||
|
|
||||||
|
| Job | Period | Runs | Installed by `start` |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `heartbeat` | every 60 seconds (`* * * * *`) | `jsc-hooks/hooks/heartbeat.sh write` | yes, always |
|
||||||
|
| `patrol` | every 15 minutes (`*/15 * * * *`) | one round of the assistant's patrol | no — only on an explicit request |
|
||||||
|
|
||||||
|
**The patrol body is not implemented yet (M-04).** Installing that entry today buys a failure every fifteen minutes and a log full of them, so `start` never installs it. It goes in only when the caller names it — `schedule.sh install patrol` or `schedule.sh install all` — and the caller has to be told what they are asking for before it is run. Its command line also cannot be guessed: pass `--cli` or `--patrol-cmd`, or the script exits 6 rather than scheduling a binary that may not exist.
|
||||||
|
|
||||||
|
The mechanism follows the platform: `crontab` on Linux, WSL and macOS, `schtasks` on Windows. macOS keeps `crontab` — a `launchd` user who wants a plist writes it themselves; this skill does not generate one.
|
||||||
|
|
||||||
|
Four properties of that script matter enough to state here, because a report that ignores any of them is wrong:
|
||||||
|
|
||||||
|
- **It only ever touches its own entry.** Install filters out its own old entries by marker and appends the new one; it never rewrites a crontab it failed to read, and it counts everybody else's lines before and after to prove none went missing. Remove takes out its own marker only. Say this in the report — the operator is entitled to know their own cron entries survived.
|
||||||
|
- **A written entry is not a running entry.** WSL does not start cron by default, and this is the machine's most likely state. Exit 1 from `install` means the entry is on disk and will never fire. Report that as a failure of the start, name `sudo service cron start`, and say it has to be run again after every WSL restart. Never soften exit 1 into "scheduling is set up".
|
||||||
|
- **The log lives at `$JSC_HOME/assistant/schedule.log`**, deliberately outside every repository. Do not offer to move it into a project.
|
||||||
|
- **The entry runs with no human present.** Every command is installed with `</dev/null`, so nothing it runs can block on input. A patrol command that stops to ask for a tool permission still hangs the round, which is one more reason the patrol entry waits for M-04.
|
||||||
|
|
||||||
|
### What a fresh heartbeat actually proves
|
||||||
|
|
||||||
|
Once the heartbeat entry is installed, cron writes a heartbeat every minute for as long as the machine is up. So the verdict 新鮮 proves the scheduler is alive — and nothing more. It does not prove a patrol ran, that any task in the book moved, or that the assistant did a single useful thing. **Never turn a fresh heartbeat into a claim about work done.** `status` prints the heartbeat, the schedule and the task book as three separate facts for exactly this reason, and the task rows — `last_run`, `next_run`, `fail_count` — are the only evidence about work. The same limit binds whatever gate reads this heartbeat later: a fresh heartbeat is grounds for not blocking, never grounds for saying the assistant is doing its job.
|
||||||
|
|
||||||
|
## schedule.sh exit codes
|
||||||
|
|
||||||
|
| Code | Meaning | What to do |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 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-hooks/hooks/heartbeat.sh` was not found | 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, or the patrol CLI could not be determined | A defect in the call, not a state of the machine. Correct the command line and run it once more; report a second exit 6 as a defect in this skill and stop |
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
The six limits in `AGENTS.md`「助理的界線」 hold for all three operations. Two of them need saying out loud here:
|
The six limits in `AGENTS.md`「助理的界線」 hold for all three operations. Two 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.
|
- **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.
|
||||||
- **`stop` clearing the heartbeat 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 assistant is alive", so a `stop` that leaves it behind leaves a lie behind. Clearing it is the whole job of `stop`, and it is the only deletion any operation here performs: `stop` touches nothing under `tasks/`, no worktree and no wiki page. Do not "restore" this limit later by taking the `clear` call out of `stop`.
|
- **`stop` clearing the heartbeat and removing the schedule is not a breach of 界線 5「不刪除狀態檔」.** That limit protects state that records work — the task book, worktrees, wiki pages — from a background process nobody is watching. The heartbeat records one fact only, "the assistant is alive", and the schedule entry is what keeps writing it, so a `stop` that leaves either behind leaves a lie behind: the next minute cron writes a fresh heartbeat over a stopped assistant. 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`.
|
||||||
|
|
||||||
## Crash exit needs no cleanup
|
## Crash exit needs no cleanup
|
||||||
|
|
||||||
An assistant that is killed, crashes, or dies with the machine writes no farewell. It does not need to. The heartbeat is a timestamp, not a lock: the last one written stays on disk, ages past the TTL on its own, and every reader from then on sees 過期. No shutdown handler, no cleanup hook and no pid check is involved, so there is nothing left that can fail to run.
|
An assistant that is killed, crashes, or dies with the machine writes no farewell. It does not need to. The heartbeat is a timestamp, not a lock: the last one written stays on disk, ages past the TTL on its own, and every reader from then on sees 過期. No shutdown handler, no cleanup hook and no pid check is involved, so there is nothing left that can fail to run.
|
||||||
|
|
||||||
That property holds only while nothing fakes a heartbeat. **`write` is called by `start` and by the scheduler that keeps a running assistant alive — nowhere else.** `status` never writes one, `stop` never writes one, and no other skill writes one. A heartbeat written by anything that is not a live assistant says a dead assistant is alive, and the reader has no way to tell the difference.
|
That property holds only while nothing fakes a heartbeat. **`write` is called by `start` and by the scheduled entry that keeps a running assistant alive — nowhere else.** `status` never writes one, `stop` never writes one, and no other skill writes one. A heartbeat written by anything that is not a live assistant says a dead assistant is alive, 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
|
||||||
|
|
||||||
Scheduling is not wired in this round. `start` writes the first heartbeat and reports; it installs no timer, no cron entry and no daemon. Until the scheduler lands, the heartbeat is never refreshed on its own and expires once the TTL passes.
|
`start` writes the first heartbeat and installs the heartbeat entry in the system scheduler, so the heartbeat keeps being refreshed once this session is gone. It installs no patrol entry and no daemon.
|
||||||
|
|
||||||
1. **Write the first heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh write`. On exit 5 the assistant cannot start: without a heartbeat its own gate reads it as not running, so report the failure, quote the script's stderr line and the heartbeat path, name the likely causes (a full disk, a permission problem on `$JSC_HOME/assistant/`, or something other than a regular file sitting at the heartbeat path), and stop — do not run step 2, and do not report a started assistant. On exit 2 or 6, follow that code's row in the exit-code table and stop. Completion condition: `write` exited 0, or the failure report naming the code and the path has been printed and no start was claimed.
|
1. **Write the first heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh write`. On exit 5 the assistant cannot start: without a heartbeat its own gate reads it as not running, so report the failure, quote the script's stderr line and the heartbeat path, name the likely causes (a full disk, a permission problem on `$JSC_HOME/assistant/`, or something other than a regular file sitting at the heartbeat path), and stop — do not run step 2, and do not report a started assistant. On exit 2 or 6, follow that code's row in the exit-code table and stop. Completion condition: `write` exited 0, or the failure report naming the code and the path has been printed and no start was claimed.
|
||||||
|
|
||||||
2. **Confirm what was written.** Run `jsc-hooks/hooks/heartbeat.sh report` and read its `state=`, `ts=`, `ttl=`, `pid=`, `cli=`, `session=` and `file=` fields. `state=fresh` is the expected result. Any other state right after a successful `write` 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 what was written.** Run `jsc-hooks/hooks/heartbeat.sh report` and read its `state=`, `ts=`, `ttl=`, `pid=`, `cli=`, `session=` and `file=` fields. `state=fresh` is the expected result. Any other state right after a successful `write` 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. **Report the start.** Print the heartbeat path, the local time of `ts`, the TTL in seconds, and `pid`, `cli`, `session` as hints. Then print this literally, with `{ttl}` replaced by the TTL just read:
|
3. **Install the heartbeat entry.** Run `tools/schedule.sh install heartbeat` — that job name only, never `patrol` and never `all` unless the caller asked for the patrol entry in this same request and was told it fails every round until M-04 lands. Judge the result by the schedule.sh exit-code table, and keep the printed `entry=`, `others_kept=` and `service=` fields for the report. Exit 1 is the case to get right: the entry is installed and inert, so step 4 reports a started assistant whose heartbeat will expire, not a scheduled one. On 2, 3, 4, 5 or 6 nothing is scheduled — report the code, say the heartbeat was written but will expire in one TTL, and do not claim the assistant will stay alive. Completion condition: the exit code is recorded, and on exit 0 the printed entry line and the surviving-entry count are recorded with it.
|
||||||
|
|
||||||
> 助理已啟動,第一次心跳寫好了。排程還沒接線,心跳不會自動更新;過了 {ttl} 秒心跳就會過期,屆時請再跑一次 start。
|
4. **Report the start.** Print the heartbeat path, the local time of `ts`, the TTL in seconds, `pid`, `cli` and `session` as hints, then the scheduler mechanism, the installed entry line, and how many other entries were left untouched. Close with the notice that matches step 3's outcome, printed literally with `{ttl}` replaced by the TTL just read:
|
||||||
|
|
||||||
Completion condition: the report carries the path, the local heartbeat time, the TTL and the three hint fields, and the notice above appears with the real TTL substituted.
|
| Step 3 | Notice |
|
||||||
|
| --- | --- |
|
||||||
|
| exit 0 | 助理已啟動,第一次心跳寫好了,排程也接上了,之後每分鐘寫一次心跳。心跳新鮮只證明排程活著,不證明助理做了事——巡檢本體還沒實作,這一輪沒有裝巡檢那一筆。 |
|
||||||
|
| exit 1 | 助理已啟動,第一次心跳寫好了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 `sudo service cron start`,重開 WSL 之後要再跑一次。 |
|
||||||
|
| 其他結束碼 | 助理已啟動,第一次心跳寫好了,但排程沒接上(結束碼 {code})。心跳不會自動更新,過了 {ttl} 秒就會過期,屆時請再跑一次 start。 |
|
||||||
|
|
||||||
|
Completion condition: the report carries the path, the local heartbeat time, the TTL, the three hint fields and the scheduler outcome, and exactly one notice above appears with the real TTL, and the real code where the row calls for it.
|
||||||
|
|
||||||
## status
|
## status
|
||||||
|
|
||||||
@@ -82,20 +127,34 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
|||||||
|
|
||||||
Completion condition: every file under `tasks/` produced exactly one row, or zero entries was reported.
|
Completion condition: every file under `tasks/` produced exactly one row, or zero entries was reported.
|
||||||
|
|
||||||
3. **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 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 and the row count equals the task count from step 2.
|
3. **Read the schedule.** Run `tools/schedule.sh status`. It writes nothing. Record `mechanism=`, `service=` and the `installed=` value of both jobs. 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. **Flag the repeatedly failing tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Completion condition: every row with `fail_count` above 0 carries the marker and its number matches the file.
|
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, 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 row count equals the task count from step 2.
|
||||||
|
|
||||||
5. **Finish successfully.** `助理未運行`, an absent `tasks/` directory and an empty `tasks/` directory are normal results — never exit non-zero for any of them. Reserve a failure report for a condition none of the tables above covers, and state which path and which error produced it. Completion condition: the report is printed and nothing under `$JSC_HOME` has been created, modified or deleted.
|
5. **Say what the two blocks together mean.** Three combinations get an explicit sentence, because each one reads as something it is not:
|
||||||
|
|
||||||
|
| Heartbeat | Schedule | Say |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 新鮮 | heartbeat installed, service running | 排程活著,心跳是它寫的。這不代表助理做了事,做了什麼看下面的待辦表 |
|
||||||
|
| 新鮮 | not installed, or service stopped | 心跳還新鮮,但沒有排程在維持它,過了 TTL 就會過期 |
|
||||||
|
| 過期 or 不存在 | heartbeat installed, service running | 排程裝著卻沒有心跳,排程那一筆自己失敗了,去看 `$JSC_HOME/assistant/schedule.log` |
|
||||||
|
|
||||||
|
Completion condition: the matching sentence is printed, or none of the three combinations applied.
|
||||||
|
|
||||||
|
6. **Flag the repeatedly failing tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Completion condition: every row with `fail_count` above 0 carries the marker and its number matches the file.
|
||||||
|
|
||||||
|
7. **Finish successfully.** `助理未運行`, an absent `tasks/` directory, an empty `tasks/` directory and an uninstalled schedule are normal results — never exit non-zero for any of them. Reserve a failure report for a condition none of the tables above covers, and state which path and which error produced it. Completion condition: the report is printed and nothing under `$JSC_HOME` has been created, modified or deleted.
|
||||||
|
|
||||||
## stop
|
## stop
|
||||||
|
|
||||||
1. **Record what is being stopped.** Run `jsc-hooks/hooks/heartbeat.sh report` first and keep its `state=`, `ts=`, `pid=`, `cli=` and `file=` fields for the closing report — after the clear they are gone for good. `state=absent` means the assistant was already stopped; say so and still run step 2, because `clear` on a missing file is a success and leaves the outcome unambiguous. On exit 2 or 6, follow that code's row, record the previous state as unknown, and carry on to step 2. Completion condition: the previous state and its fields are recorded, or the previous state is recorded as unknown with its code.
|
1. **Record what is being stopped.** Run `jsc-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 the assistant was already stopped; 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. **Clear the heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh clear`. On exit 5 the file is still there: report the failure with the script's stderr line and the path, say plainly that every reader still sees a heartbeat claiming the assistant is running 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 3 — 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.
|
2. **Remove the schedule first.** Run `tools/schedule.sh remove all` — both jobs, so a patrol entry somebody installed by hand goes too. This comes before the clear and never after: clear first and the next cron minute 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 the entry will keep writing heartbeats 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 cron rewrites a minute later 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. **Report the stop and what it means for the gate.** Print the previous state and heartbeat time from step 1, then this literally:
|
3. **Clear the heartbeat.** Run `jsc-hooks/hooks/heartbeat.sh clear`. On exit 5 the file is still there: report the failure with the script's stderr line and the path, say plainly that every reader still sees a heartbeat claiming the assistant is running 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.
|
||||||
|
|
||||||
> 助理已停止,心跳清掉了。靠心跳判定的 jsc 技能閘門一讀到沒有心跳就會擋下技能呼叫;閘門目前還沒接線,所以這一刻誰都擋不到。要再工作就先跑一次 start。
|
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:
|
||||||
|
|
||||||
Say it exactly this way. The blocking is the designed consequence of a cleared heartbeat, and whoever stops the assistant has to know it is coming; the second clause is the part that keeps the notice honest while the gate is still unwired. When the gate is wired, that clause is what gets rewritten — not the first one. Completion condition: the notice appears with both clauses, and the previous state and heartbeat time are printed above it.
|
> 助理已停止,排程移除了,心跳也清掉了,其他人的排程一筆都沒動。靠心跳判定的 jsc 技能閘門一讀到沒有心跳就會擋下技能呼叫;閘門目前還沒接線,所以這一刻誰都擋不到。要再工作就先跑一次 start。
|
||||||
|
|
||||||
|
Say it exactly this way. The blocking is the designed consequence of a cleared heartbeat, and whoever stops the assistant has to know it is coming; the clause about the gate being unwired is the part that keeps the notice honest while that is still true. When the gate is wired, that clause is what gets rewritten — not the rest. Completion condition: the notice appears with all three clauses, and the previous state, the heartbeat time and the removal counts are printed above it.
|
||||||
|
|||||||
Executable
+446
@@ -0,0 +1,446 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# schedule.sh — 助理系統排程的安裝、移除與查現況(供 jsc-assist:assistant 呼叫)。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# schedule.sh install [heartbeat|patrol|all] [--dry-run] [--cli {代號}] [--patrol-cmd {指令}]
|
||||||
|
# schedule.sh remove [heartbeat|patrol|all] [--dry-run]
|
||||||
|
# schedule.sh status [--dry-run]
|
||||||
|
#
|
||||||
|
# 工作代號省略時一律是 heartbeat。心跳每 60 秒寫一次,巡檢每 15 分鐘跑一輪。
|
||||||
|
# 巡檢那一筆要自己指名(`patrol` 或 `all`)才會裝——巡檢本體還沒實作,裝了每一輪都會失敗。
|
||||||
|
#
|
||||||
|
# 結束碼:
|
||||||
|
# 0 成功:install 條目寫進去也回讀得到、排程服務在跑;remove 移除完成,或本來就沒裝;
|
||||||
|
# status 印完現況(有沒有裝都算成功,看 installed 欄)
|
||||||
|
# 1 install 寫進去了,但排程服務沒在跑——條目不會被執行。WSL 預設不啟動 cron,這一碼
|
||||||
|
# 多半就是它。呼叫端一律照實講「排程裝了但不會執行」,不可以宣稱會定時執行
|
||||||
|
# 2 找不到 jsc-hooks 的 hooks/heartbeat.sh。心跳沒有東西可跑,整支停下
|
||||||
|
# 3 這台機器沒有可用的排程機制:認不得作業系統,或 crontab 與 schtasks 都找不到
|
||||||
|
# 4 排程操作失敗:讀不到現有排程(且失敗原因不是「沒有排程」)、寫入或刪除回非 0
|
||||||
|
# 5 回讀驗證失敗:寫入回 0 但條目不在,或移除回 0 但條目還在,又或其他人的條目數量對不上
|
||||||
|
# 6 用法錯誤:不認得的子命令、不認得的工作代號、缺參數,或判不出要用哪一支 CLI 跑巡檢
|
||||||
|
#
|
||||||
|
# --- 只動自己那一筆 ---
|
||||||
|
#
|
||||||
|
# 每一筆條目行尾都帶固定標記「# jsc-assist:assistant {工作代號}」,安裝與移除都靠它比對。
|
||||||
|
# 安裝先用 `grep -vF` 濾掉自己這幾個工作的舊條目,再把新條目追加上去,整份寫回;**絕不**
|
||||||
|
# 把 crontab 當空的重寫。`crontab -l` 在沒有任何排程時會回非 0,錯誤訊息才分得出是「沒有
|
||||||
|
# 排程」還是「權限不足」——後者當成空的寫回去,會把使用者整份排程刪光,所以這裡讀不懂
|
||||||
|
# 錯誤訊息就直接回 4,不猜。
|
||||||
|
# 寫回之後還會數行數:其他人的條目一行都不能少,少了就回 5。
|
||||||
|
#
|
||||||
|
# --- 排程服務沒在跑,等於沒裝 ---
|
||||||
|
#
|
||||||
|
# 條目寫進去不代表會被執行。WSL 預設不啟動 cron,要 `sudo service cron start`,而且重開
|
||||||
|
# WSL 之後要再啟動一次。安裝完一律檢查服務在不在跑,沒跑就回 1 並講清楚,不可以只回報
|
||||||
|
# 「排程已建立」。
|
||||||
|
#
|
||||||
|
# --- 排程是非互動環境 ---
|
||||||
|
#
|
||||||
|
# 條目一律接 `</dev/null`,跑的東西讀不到標準輸入,卡不住。heartbeat.sh 本來就不讀 stdin,
|
||||||
|
# 這一條是給巡檢那一筆用的:助理跳出權限詢問就會整輪卡死,等於排程沒跑。
|
||||||
|
#
|
||||||
|
# --- log 放哪裡 ---
|
||||||
|
#
|
||||||
|
# $JSC_HOME/assistant/schedule.log(JSC_HOME 未設定時為 ~/.jsc)。刻意放在專案外面:寫進
|
||||||
|
# 任何存取庫都會多出未追蹤檔,污染別人的變更盤點。
|
||||||
|
#
|
||||||
|
# 環境變數:
|
||||||
|
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc
|
||||||
|
# JSC_ASSIST_CRONTAB_CMD crontab 執行檔,預設 crontab。crontab 不在標準路徑,或要用
|
||||||
|
# 假的 crontab 驗濾除邏輯時才設
|
||||||
|
# JSC_ASSIST_PATROL_CMD 巡檢要跑的指令,優先於 --patrol-cmd 以外的所有推斷
|
||||||
|
# JSC_CLI 目前是哪一支 CLI,決定巡檢預設指令
|
||||||
|
set -u
|
||||||
|
|
||||||
|
MARK_PREFIX='# jsc-assist:assistant'
|
||||||
|
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||||
|
STATE_DIR="$JSC_HOME/assistant"
|
||||||
|
LOG="$STATE_DIR/schedule.log"
|
||||||
|
CRONTAB_CMD="${JSC_ASSIST_CRONTAB_CMD:-crontab}"
|
||||||
|
TASK_PREFIX='jsc-assist-assistant'
|
||||||
|
|
||||||
|
DRYRUN=0
|
||||||
|
CLI=''
|
||||||
|
PATROL_CMD="${JSC_ASSIST_PATROL_CMD:-}"
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||||
|
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
usage: schedule.sh install [heartbeat|patrol|all] [--dry-run] [--cli 代號] [--patrol-cmd 指令]
|
||||||
|
schedule.sh remove [heartbeat|patrol|all] [--dry-run]
|
||||||
|
schedule.sh status [--dry-run]
|
||||||
|
EOF
|
||||||
|
exit 6
|
||||||
|
}
|
||||||
|
|
||||||
|
die() { # $1=結束碼 $2=訊息
|
||||||
|
printf '[jsc][助理排程][ERR]:%s\n' "$2" >&2
|
||||||
|
exit "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
note() { printf '[jsc][助理排程]:%s\n' "$1" >&2; }
|
||||||
|
|
||||||
|
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序比照 jsc-hooks lib.sh 的
|
||||||
|
# jsc_gitea_sh():先環境變數,再開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
||||||
|
heartbeat_sh() {
|
||||||
|
if [ -n "${JSC_HOOKS_HOOKS:-}" ] && [ -f "$JSC_HOOKS_HOOKS/heartbeat.sh" ]; then
|
||||||
|
printf '%s\n' "$JSC_HOOKS_HOOKS/heartbeat.sh"; return 0
|
||||||
|
fi
|
||||||
|
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||||||
|
for _c in "$_root/../hooks/hooks/heartbeat.sh" "$_root/../jsc-hooks/hooks/heartbeat.sh"; do
|
||||||
|
[ -f "$_c" ] && { (CDPATH= cd -- "$(dirname -- "$_c")" && printf '%s/heartbeat.sh\n' "$(pwd)"); return 0; }
|
||||||
|
done
|
||||||
|
_c=$(ls -d "$_root"/../../jsc-hooks/*/hooks/heartbeat.sh \
|
||||||
|
"$_root"/../../hooks/*/hooks/heartbeat.sh \
|
||||||
|
"$HOME"/.claude/plugins/cache/*/jsc-hooks/*/hooks/heartbeat.sh 2>/dev/null \
|
||||||
|
| sort | tail -n1)
|
||||||
|
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||||
|
_c=$(command -v heartbeat.sh 2>/dev/null || true)
|
||||||
|
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
os_kind() {
|
||||||
|
case "$(uname -s 2>/dev/null)" in
|
||||||
|
Linux) printf 'linux' ;;
|
||||||
|
Darwin) printf 'darwin' ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) printf 'windows' ;;
|
||||||
|
*) printf 'unknown' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 這台機器要用哪一種排程機制。Linux、WSL 與 macOS 都用 crontab;macOS 若要改用 launchd
|
||||||
|
# 請自行改寫,本腳本不代為產生 plist。
|
||||||
|
mechanism() {
|
||||||
|
case "$(os_kind)" in
|
||||||
|
windows) command -v schtasks >/dev/null 2>&1 && { printf 'schtasks'; return 0; } ;;
|
||||||
|
linux|darwin) command -v "$CRONTAB_CMD" >/dev/null 2>&1 && { printf 'crontab'; return 0; } ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 排程服務在不在跑。回 running、stopped 或 unknown。
|
||||||
|
service_state() {
|
||||||
|
case "$(os_kind)" in
|
||||||
|
darwin)
|
||||||
|
# macOS 的 cron 由 launchd 隨用隨起,查不到行程不代表沒在跑,一律回 unknown。
|
||||||
|
printf 'unknown'; return 0 ;;
|
||||||
|
windows)
|
||||||
|
printf 'unknown'; return 0 ;;
|
||||||
|
esac
|
||||||
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
|
if pgrep -x cron >/dev/null 2>&1 || pgrep -x crond >/dev/null 2>&1; then
|
||||||
|
printf 'running'
|
||||||
|
else
|
||||||
|
printf 'stopped'
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v ps >/dev/null 2>&1; then
|
||||||
|
if ps -e 2>/dev/null | grep -qE '[ /](cron|crond)$'; then printf 'running'; else printf 'stopped'; fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
marker_of() { printf '%s %s' "$MARK_PREFIX" "$1"; }
|
||||||
|
|
||||||
|
# 數行數。用 grep -c '' 不用 wc -l:最後一行沒有換行時 wc -l 會少數一行。
|
||||||
|
# grep 數到 0 會回非 0,數字照樣印得出來,所以只在完全沒有輸出時才補 0。
|
||||||
|
count_lines() { _n=$(grep -c '' "$1" 2>/dev/null); [ -n "$_n" ] || _n=0; printf '%s' "$_n"; }
|
||||||
|
|
||||||
|
# 數管線進來的行數,語意同 count_lines。
|
||||||
|
count_stdin() { _n=$(grep -c '' 2>/dev/null); [ -n "$_n" ] || _n=0; printf '%s' "$_n"; }
|
||||||
|
|
||||||
|
spec_of() {
|
||||||
|
case "$1" in
|
||||||
|
heartbeat) printf '* * * * *' ;;
|
||||||
|
patrol) printf '*/15 * * * *' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 巡檢要跑的指令。優先序:--patrol-cmd 或 JSC_ASSIST_PATROL_CMD > 依 CLI 代號推斷。
|
||||||
|
# 判不出 CLI 就回非 0,由主流程回 6,不猜——猜錯會每 15 分鐘跑一支不存在的執行檔。
|
||||||
|
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) printf 'claude -p "/jsc-assist:patrol"' ;;
|
||||||
|
codex) printf "codex exec '\$patrol'" ;;
|
||||||
|
copilot) printf 'copilot -p "跑一輪助理巡檢"' ;;
|
||||||
|
antigravity) printf 'agy -p "/jsc-assist:patrol"' ;;
|
||||||
|
kiro) printf 'kiro-cli -p "跑一輪助理巡檢"' ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 組出一筆 crontab 條目。`%` 在 crontab 是換行符號,一律跳脫。
|
||||||
|
cron_entry() { # $1=工作代號
|
||||||
|
_spec=$(spec_of "$1")
|
||||||
|
case "$1" in
|
||||||
|
heartbeat) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule '$HEARTBEAT' write" ;;
|
||||||
|
patrol) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule $PATROL_RESOLVED" ;;
|
||||||
|
esac
|
||||||
|
printf '%s %s </dev/null >>%s 2>&1 %s' \
|
||||||
|
"$_spec" "$_cmd" "'$LOG'" "$(marker_of "$1")" | sed 's/%/\\%/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
task_name() { printf '%s-%s' "$TASK_PREFIX" "$1"; }
|
||||||
|
|
||||||
|
# 讀現有的 crontab 到 $1。沒有任何排程時 crontab -l 會回非 0,那算正常;讀不懂的錯誤
|
||||||
|
# 一律回 4,不當成空的——當成空的寫回去會把使用者整份排程刪光。
|
||||||
|
cron_read() { # $1=輸出檔
|
||||||
|
_err="$TMPD/err"
|
||||||
|
if "$CRONTAB_CMD" -l >"$1" 2>"$_err"; then return 0; fi
|
||||||
|
if [ ! -s "$_err" ] || grep -qiE 'no crontab|沒有 crontab' "$_err"; then
|
||||||
|
: >"$1"; return 0
|
||||||
|
fi
|
||||||
|
die 4 "讀不到現有排程,原因不是「沒有排程」:$(tr '\n' ' ' <"$_err")"
|
||||||
|
}
|
||||||
|
|
||||||
|
cron_lines_for() { # $1=crontab 檔 $2=工作代號;印出該工作的條目
|
||||||
|
grep -F "$(marker_of "$2")" "$1" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 參數解析 ---
|
||||||
|
|
||||||
|
CMD="${1:-}"
|
||||||
|
[ -n "$CMD" ] || usage
|
||||||
|
shift
|
||||||
|
case "$CMD" in install|remove|status) ;; *) usage ;; esac
|
||||||
|
|
||||||
|
JOBS='heartbeat'
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
case "$1" in
|
||||||
|
heartbeat) JOBS='heartbeat'; shift ;;
|
||||||
|
patrol) JOBS='patrol'; shift ;;
|
||||||
|
all) JOBS='heartbeat patrol'; shift ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRYRUN=1; shift ;;
|
||||||
|
--cli) [ "$#" -ge 2 ] || usage; CLI="$2"; shift 2 ;;
|
||||||
|
--patrol-cmd) [ "$#" -ge 2 ] || usage; PATROL_CMD="$2"; shift 2 ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
HEARTBEAT=$(heartbeat_sh) \
|
||||||
|
|| die 2 '找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可跑。請先安裝 jsc-hooks 0.3.7 以上。'
|
||||||
|
MECH=$(mechanism) \
|
||||||
|
|| die 3 "這台機器沒有可用的排程機制(作業系統:$(os_kind),找不到 $CRONTAB_CMD 或 schtasks)。"
|
||||||
|
|
||||||
|
# 巡檢指令在這裡就解出來。放進 cron_entry 再解的話,那支是在命令替換的子行程裡跑,
|
||||||
|
# 判不出 CLI 時 die 只結束子行程,主流程會帶著空指令繼續往下裝。
|
||||||
|
PATROL_RESOLVED=''
|
||||||
|
case "$CMD:$JOBS" in
|
||||||
|
install:*patrol*)
|
||||||
|
PATROL_RESOLVED=$(patrol_command) \
|
||||||
|
|| die 6 '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TMPD=$(mktemp -d) || die 4 '建不出暫存目錄。'
|
||||||
|
trap 'rm -rf "$TMPD"' EXIT
|
||||||
|
|
||||||
|
# --- schtasks(Windows)---
|
||||||
|
|
||||||
|
schtasks_install() {
|
||||||
|
_rc=0
|
||||||
|
for _job in $JOBS; do
|
||||||
|
_tn=$(task_name "$_job")
|
||||||
|
case "$_job" in
|
||||||
|
heartbeat) _mo=1; _run="sh \"$HEARTBEAT\" write" ;;
|
||||||
|
patrol) _mo=15; _run=$(patrol_command) || die 6 '判不出要用哪一支 CLI 跑巡檢,請帶 --cli 或 --patrol-cmd。' ;;
|
||||||
|
esac
|
||||||
|
_tr="cmd /c $_run <NUL >> \"$LOG\" 2>&1"
|
||||||
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
|
printf 'dryrun=schtasks job=%s task=%s interval=%s cmd=%s\n' "$_job" "$_tn" "$_mo" "$_tr"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# /F 只覆蓋同名任務,也就是本腳本自己那一筆,不影響別人的排程。
|
||||||
|
schtasks /Create /TN "$_tn" /SC MINUTE /MO "$_mo" /F /TR "$_tr" >/dev/null 2>&1 \
|
||||||
|
|| die 4 "schtasks 建立 $_tn 失敗。"
|
||||||
|
schtasks /Query /TN "$_tn" >/dev/null 2>&1 || die 5 "schtasks 建立 $_tn 回 0,卻查不到這個任務。"
|
||||||
|
_rc=0
|
||||||
|
done
|
||||||
|
return "$_rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
schtasks_remove() {
|
||||||
|
for _job in $JOBS; do
|
||||||
|
_tn=$(task_name "$_job")
|
||||||
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
|
printf 'dryrun=schtasks job=%s task=%s action=delete\n' "$_job" "$_tn"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! schtasks /Query /TN "$_tn" >/dev/null 2>&1; then
|
||||||
|
note "$_job 本來就沒有排程,不用移除。"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
schtasks /Delete /TN "$_tn" /F >/dev/null 2>&1 || die 4 "schtasks 刪除 $_tn 失敗。"
|
||||||
|
schtasks /Query /TN "$_tn" >/dev/null 2>&1 && die 5 "schtasks 刪除 $_tn 回 0,任務卻還在。"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
schtasks_status() {
|
||||||
|
printf 'mechanism=schtasks service=%s log=%s\n' "$(service_state)" "$LOG"
|
||||||
|
for _job in heartbeat patrol; do
|
||||||
|
_tn=$(task_name "$_job")
|
||||||
|
if schtasks /Query /TN "$_tn" >/dev/null 2>&1; then
|
||||||
|
printf 'job=%s installed=yes task=%s\n' "$_job" "$_tn"
|
||||||
|
else
|
||||||
|
printf 'job=%s installed=no task=%s\n' "$_job" "$_tn"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- crontab(Linux、WSL、macOS)---
|
||||||
|
|
||||||
|
crontab_install() {
|
||||||
|
_cur="$TMPD/cur"; _new="$TMPD/new"
|
||||||
|
cron_read "$_cur"
|
||||||
|
|
||||||
|
cp "$_cur" "$_new"
|
||||||
|
# 先濾掉自己這幾個工作的舊條目,再追加新的。重跑不會疊成兩筆,別人的條目原樣留著。
|
||||||
|
for _job in $JOBS; do
|
||||||
|
grep -vF "$(marker_of "$_job")" "$_new" >"$TMPD/f" 2>/dev/null || true
|
||||||
|
mv "$TMPD/f" "$_new"
|
||||||
|
done
|
||||||
|
_others=$(count_lines "$_new")
|
||||||
|
for _job in $JOBS; do
|
||||||
|
cron_entry "$_job" >>"$_new"
|
||||||
|
printf '\n' >>"$_new"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
|
for _job in $JOBS; do
|
||||||
|
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job")"
|
||||||
|
done
|
||||||
|
printf 'dryrun=crontab action=write others_kept=%s total_lines=%s\n' \
|
||||||
|
"$_others" "$(count_lines "$_new")"
|
||||||
|
printf -- '--- 寫回後的 crontab ---\n'
|
||||||
|
cat "$_new"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||||
|
"$CRONTAB_CMD" "$_new" >/dev/null 2>"$TMPD/err" \
|
||||||
|
|| die 4 "crontab 寫入失敗:$(tr '\n' ' ' <"$TMPD/err")"
|
||||||
|
|
||||||
|
_chk="$TMPD/chk"; cron_read "$_chk"
|
||||||
|
for _job in $JOBS; do
|
||||||
|
[ -n "$(cron_lines_for "$_chk" "$_job")" ] \
|
||||||
|
|| die 5 "crontab 寫入回 0,卻讀不到 $_job 的條目。"
|
||||||
|
[ "$(cron_lines_for "$_chk" "$_job" | count_stdin)" -eq 1 ] \
|
||||||
|
|| die 5 "$_job 的條目不只一筆,排程會重複執行。"
|
||||||
|
done
|
||||||
|
_kept=$(grep -vF "$MARK_PREFIX" "$_chk" 2>/dev/null | count_stdin)
|
||||||
|
[ "$_kept" -eq "$_others" ] \
|
||||||
|
|| die 5 "別人的排程條目從 $_others 筆變成 $_kept 筆,寫回不完整。"
|
||||||
|
|
||||||
|
for _job in $JOBS; do
|
||||||
|
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job")"
|
||||||
|
done
|
||||||
|
printf 'others_kept=%s log=%s\n' "$_kept" "$LOG"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
crontab_remove() {
|
||||||
|
_cur="$TMPD/cur"; _new="$TMPD/new"
|
||||||
|
cron_read "$_cur"
|
||||||
|
|
||||||
|
_hit=0
|
||||||
|
cp "$_cur" "$_new"
|
||||||
|
for _job in $JOBS; do
|
||||||
|
if [ -n "$(cron_lines_for "$_new" "$_job")" ]; then
|
||||||
|
_hit=$((_hit + 1))
|
||||||
|
else
|
||||||
|
note "$_job 本來就沒有排程,不用移除。"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
grep -vF "$(marker_of "$_job")" "$_new" >"$TMPD/f" 2>/dev/null || true
|
||||||
|
mv "$TMPD/f" "$_new"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$_hit" -eq 0 ]; then
|
||||||
|
printf 'removed=0 others_kept=%s\n' "$(count_lines "$_cur")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
|
printf 'dryrun=crontab action=remove jobs=%s removed=%s\n' "$JOBS" "$_hit"
|
||||||
|
printf -- '--- 寫回後的 crontab ---\n'
|
||||||
|
cat "$_new"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$CRONTAB_CMD" "$_new" >/dev/null 2>"$TMPD/err" \
|
||||||
|
|| die 4 "crontab 寫入失敗:$(tr '\n' ' ' <"$TMPD/err")"
|
||||||
|
|
||||||
|
_chk="$TMPD/chk"; cron_read "$_chk"
|
||||||
|
for _job in $JOBS; do
|
||||||
|
[ -z "$(cron_lines_for "$_chk" "$_job")" ] \
|
||||||
|
|| die 5 "crontab 刪除回 0,$_job 的條目卻還在。"
|
||||||
|
done
|
||||||
|
_before=$(grep -vF "$MARK_PREFIX" "$_cur" 2>/dev/null | count_stdin)
|
||||||
|
_after=$(grep -vF "$MARK_PREFIX" "$_chk" 2>/dev/null | count_stdin)
|
||||||
|
[ "$_before" -eq "$_after" ] \
|
||||||
|
|| die 5 "別人的排程條目從 $_before 筆變成 $_after 筆,移除動到了不該動的東西。"
|
||||||
|
|
||||||
|
printf 'removed=%s others_kept=%s\n' "$_hit" "$_after"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
crontab_status() {
|
||||||
|
_cur="$TMPD/cur"; cron_read "$_cur"
|
||||||
|
printf 'mechanism=crontab service=%s log=%s\n' "$(service_state)" "$LOG"
|
||||||
|
for _job in heartbeat patrol; do
|
||||||
|
_line=$(cron_lines_for "$_cur" "$_job")
|
||||||
|
if [ -n "$_line" ]; then
|
||||||
|
printf 'job=%s installed=yes entry=%s\n' "$_job" "$_line"
|
||||||
|
else
|
||||||
|
printf 'job=%s installed=no entry=\n' "$_job"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 主流程 ---
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
install)
|
||||||
|
case "$MECH" in
|
||||||
|
crontab) crontab_install ;;
|
||||||
|
schtasks) schtasks_install ;;
|
||||||
|
esac
|
||||||
|
[ "$DRYRUN" -eq 1 ] && exit 0
|
||||||
|
_svc=$(service_state)
|
||||||
|
if [ "$_svc" = stopped ]; then
|
||||||
|
printf 'service=stopped\n'
|
||||||
|
die 1 '排程條目寫進去了,但 cron 服務沒在跑,條目一次都不會被執行。WSL 預設不啟動 cron:請跑 `sudo service cron start`,而且重開 WSL 之後要再啟動一次。'
|
||||||
|
fi
|
||||||
|
printf 'service=%s\n' "$_svc"
|
||||||
|
[ "$_svc" = unknown ] && note '判不出排程服務在不在跑,請自行確認條目真的會被執行。'
|
||||||
|
exit 0 ;;
|
||||||
|
remove)
|
||||||
|
case "$MECH" in
|
||||||
|
crontab) crontab_remove ;;
|
||||||
|
schtasks) schtasks_remove ;;
|
||||||
|
esac
|
||||||
|
exit 0 ;;
|
||||||
|
status)
|
||||||
|
case "$MECH" in
|
||||||
|
crontab) crontab_status ;;
|
||||||
|
schtasks) schtasks_status ;;
|
||||||
|
esac
|
||||||
|
exit 0 ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user