What: - 新增 assistant 技能,提供啟動、查現況、停止三個操作。 - 刪掉原本獨立的 status 技能,行為清單與 README 的技能目錄一起改。 - 三份 manifest 的版本一起提升,對 jsc-hooks 的下限提到含心跳腳本的那一版。 Why: - 助理的生命週期是一件事,拆在兩支技能裡,靠描述自動觸發的 CLI 等於擲骰子挑一支。準則也明寫技能目標不得重複。 - 刪除的代價這時候最低:這個 domain 才剛落地,status 是它唯一一支技能,除了自己的文件沒有別的東西指向它。晚一步等各處都引用了再收攏,成本差很多。 How: - 心跳的判定一律交給 jsc-hooks 的心跳腳本,三個操作都讀它的回報,不自己解析心跳檔。判定有兩份就會漂移,狀態與訊息就會對不上。 - 那支腳本的每一個結束碼都在技能裡有明確處置,包含「腳本自己沒跑起來」那一種——那時候既不能說助理在跑,也不能說助理停了。 - 時間戳壞掉一律當成不新鮮,絕不退回判成新鮮。 - 異常結束不需要額外的清理機制:心跳是時間戳,過了門檻自動失效。反過來說,stop 以外的任何路徑都不該寫心跳,否則就是留一個假心跳。 - stop 清心跳是它的職責,不算助理界線裡「不刪狀態檔」那一條。技能內文把這個例外寫明白,免得日後照界線把 stop 砍掉。 - stop 的收尾同時講兩件事:心跳清掉之後閘門會擋下技能呼叫,以及閘門目前還沒接線所以這一刻擋不到誰。前者是設計後果,停助理的人一定要知道;後者不講就是說一件還沒成真的事。 - 排程這一輪不做,start 只寫第一次心跳,並講明心跳不會自動更新。 Who: 助理落地的第二塊:心跳有了,接著要有人寫它、讀它、清它。
14 KiB
name, description
| name | description |
|---|---|
| assistant | 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). |
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. 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.
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.
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/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 |
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 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 astopthat leaves it behind leaves a lie behind. Clearing it is the whole job ofstop, and it is the only deletion any operation here performs:stoptouches nothing undertasks/, no worktree and no wiki page. Do not "restore" this limit later by taking theclearcall 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 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.
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.
-
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. -
Report the start. Print the heartbeat path, the local time of
ts, the TTL in seconds, andpid,cli,sessionas hints. Then print this literally, with{ttl}replaced by the TTL just read:助理已啟動,第一次心跳寫好了。排程還沒接線,心跳不會自動更新;過了 {ttl} 秒心跳就會過期,屆時請再跑一次 start。
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.
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. -
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 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 and the row count equals the task count from step 2. -
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 and an emptytasks/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_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 step 2, becauseclearon 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. -
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 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: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, 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 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.