What: - 新增 tools/schedule.sh,三個子命令:install 安裝、remove 移除、status 查現況。兩筆工作分別是每分鐘的心跳與每十五分鐘的巡檢。 - 助理主體的 start 接上安裝、stop 接上移除,行為清單與 README 跟著更新。 Why: - 心跳要有人定時寫,不然啟動之後過幾分鐘就自己過期,助理看起來像沒在跑。 - 排程這件事有標準的輸入與輸出,準則要求下放腳本,技能只描述何時呼叫。 How: - 條目行尾帶固定標記,安裝先濾掉自己的舊條目再追加。絕不覆寫整份排程,別人的條目一行都不碰;寫完回讀驗證,出現兩筆或別人的行數對不上就報錯。 - 裝完檢查排程服務在不在跑。這台機器是 WSL,預設不啟動 cron,裝了條目卻一次都不會執行——不檢查就會宣稱一件沒發生的事。 - 條目一律帶 </dev/null。排程是非互動環境,任何等輸入的東西都會把整筆卡死。 - 輸出寫到 JSC_HOME 底下,不寫進任何專案目錄,免得多出未追蹤檔污染別人的變更盤點。 - 巡檢那一筆預設不裝,要明著指定才會裝。巡檢本體還沒實作,裝了只會每十五分鐘失敗一次;巡檢指令也不猜 CLI,判不出來就停下,猜錯的代價是每十五分鐘跑一支不存在的執行檔。 - stop 先移除排程再清心跳,順序不能反。反過來的話清完下一分鐘排程就補寫一次,stop 等於騙人。 Who: 助理落地的第三塊。這裡留下一個要在巡檢那一輪解掉的問題:心跳目前由排程直接寫,所以心跳新鮮只證明排程活著,不證明助理做了事。往後應該改由巡檢跑完那一輪去寫,心跳才等於工作訊號。限制已寫進技能內文與說明文件。
24 KiB
name, description
| name | description |
|---|---|
| assistant | 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
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.
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
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
| Path | Read by | Format |
|---|---|---|
$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 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.
The verdict is time-based only. A heartbeat counts as fresh when the file exists and its ts is less than the TTL behind now (300 seconds by default, JSC_ASSISTANT_HEARTBEAT_TTL overrides it). pid liveness is never tested: five CLIs and container processes cannot see each other's pids, so a live-looking pid proves nothing and a missing one proves nothing either. Report pid as a hint for whoever has to find a blocking process, and give it no weight in the verdict.
heartbeat.sh exit codes
Every call in every operation below is judged by this table. Report the code you got, then take the row's action — never retry a code silently, and never downgrade a failure into a success.
| Code | Meaning | What to do |
|---|---|---|
| 0 | write wrote the heartbeat, clear finished and the file is gone, report printed its line, check says fresh |
Carry on with the operation's next step. For report, the state still has to be read out of the printed state= field |
| 1 | check: the heartbeat exists but is at or past the TTL — the assistant ran and has stopped |
Report 助理未運行, name the age in seconds, and say the assistant has to be started again. report returns this state as state=stale with exit 0 |
| 2 | The script did not run at all — it failed to load its lib.sh |
Report that the heartbeat state is unknown, name the script path and the code, and stop the operation. Never claim the assistant is running, and never claim it is stopped |
| 3 | check: no heartbeat file — the assistant has never been started |
Report 助理未運行 and say to run start. report returns this state as state=absent with exit 0. In stop this state cannot appear, because clear treats a missing file as success |
| 4 | check: the heartbeat exists but its ts is missing, empty or not a number — the file is damaged, the assistant is not merely stopped |
Treat it as not fresh; falling back to fresh is forbidden. Report the file as damaged, say the state cannot be read from it, and tell the operator to run stop and then start to rebuild it. report returns this state as state=invalid with exit 0 |
| 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 |
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
installmeans the entry is on disk and will never fire. Report that as a failure of the start, namesudo 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
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.
stopclearing 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 astopthat 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 ofstop, and they are the only deletions any operation here performs, both of them entries this skill installed itself.stoptouches nothing undertasks/, nobody else's cron entry, no worktree and no wiki page. Do not "restore" this limit later by taking either removal out ofstop.
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.
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 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.
-
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:writeexited 0, or the failure report naming the code and the path has been printed and no start was claimed. -
Confirm what was written. Run
jsc-hooks/hooks/heartbeat.sh reportand read itsstate=,ts=,ttl=,pid=,cli=,session=andfile=fields.state=freshis the expected result. Any other state right after a successfulwritemeans 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 eitherstate=freshwas recorded with its seven fields, or the mismatch was reported. -
Install the heartbeat entry. Run
tools/schedule.sh install heartbeat— that job name only, neverpatroland neverallunless 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 printedentry=,others_kept=andservice=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. -
Report the start. Print the heartbeat path, the local time of
ts, the TTL in seconds,pid,cliandsessionas 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: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
Read-only throughout. This operation creates, modifies and deletes nothing under $JSC_HOME, and it never calls write or clear.
-
Read the heartbeat through the script. Run
jsc-hooks/hooks/heartbeat.sh reportand split the line on spaces, takingfile=last so a path containing spaces stays intact. Mapstate=to the verdict:fresh→新鮮,stale→過期,invalid→心跳檔損壞,absent→不存在. Print助理未運行forstale,invalidandabsent. Never re-derive the verdict fromtsyourself, and never treatinvalidas fresh. On exit 2 or 6, follow that code's row, record the heartbeat state as unknown, and carry on to step 2 — the task book is still worth printing. Completion condition: the heartbeat state holds one of新鮮,過期,心跳檔損壞,不存在or unknown, andts,age,ttl,pid,cli,sessionandfileare recorded as read or as empty. -
Read the task book. Take the assistant directory from the
file=path of step 1, list the regular files directly under itstasks/subdirectory, and parse each one askey=valuelines. Branch on the outcome.Outcome Do Directory absent Report zero entries. This is a normal result, not an error Directory present, no files Report zero entries A file cannot be read, or holds no recognisable key Keep it as one row, put the file name in the title column, name the read or parse error in that row, and carry on with the remaining files A key is missing from a readable file Print -in that columnCompletion condition: every file under
tasks/produced exactly one row, or zero entries was reported. -
Read the schedule. Run
tools/schedule.sh status. It writes nothing. Recordmechanism=,service=and theinstalled=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. -
Print the status table. Lead with the heartbeat block — verdict, last heartbeat time rendered from
tsin 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 carryingstate,title,next_runandfail_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. -
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.logCompletion condition: the matching sentence is printed, or none of the three combinations applied.
-
Flag the repeatedly failing tasks. Append 已連續失敗 N 次 to every row whose
fail_countis above 0, withNtaken 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 withfail_countabove 0 carries the marker and its number matches the file. -
Finish successfully.
助理未運行, an absenttasks/directory, an emptytasks/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_HOMEhas been created, modified or deleted.
stop
-
Record what is being stopped. Run
jsc-hooks/hooks/heartbeat.sh reportfirst and keep itsstate=,ts=,pid=,cli=andfile=fields for the closing report — after the clear they are gone for good.state=absentmeans the assistant was already stopped; say so and still run steps 2 and 3, because a scheduled entry can outlive its heartbeat andclearon 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. -
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 keepremoved=andothers_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 -lto look, then remove the line carrying# jsc-assist:assistantby hand), and skip steps 3 and 4 — clearing a heartbeat that cron rewrites a minute later only hides the problem. Completion condition:removeexited 0 with its counts recorded, or the failure report has been printed and no stop was claimed. -
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 runstatusto 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:clearexited 0, or the failure report naming the code, the path and the manual fix has been printed and no stop was claimed. -
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:
助理已停止,排程移除了,心跳也清掉了,其他人的排程一筆都沒動。靠心跳判定的 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.