|
|
|
@@ -0,0 +1,101 @@
|
|
|
|
|
---
|
|
|
|
|
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).
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
- **`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`.
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
> 助理已啟動,第一次心跳寫好了。排程還沒接線,心跳不會自動更新;過了 {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`.
|
|
|
|
|
|
|
|
|
|
1. **Read the heartbeat through the script.** Run `jsc-hooks/hooks/heartbeat.sh report` and split the line on spaces, taking `file=` last so a path containing spaces stays intact. Map `state=` to the verdict: `fresh` → `新鮮`, `stale` → `過期`, `invalid` → `心跳檔損壞`, `absent` → `不存在`. Print `助理未運行` for `stale`, `invalid` and `absent`. Never re-derive the verdict from `ts` yourself, and never treat `invalid` as fresh. On exit 2 or 6, follow that code's row, record the heartbeat state as unknown, and carry on to step 2 — the task book is still worth printing. Completion condition: the heartbeat state holds one of `新鮮`, `過期`, `心跳檔損壞`, `不存在` or unknown, and `ts`, `age`, `ttl`, `pid`, `cli`, `session` and `file` are recorded as read or as empty.
|
|
|
|
|
|
|
|
|
|
2. **Read the task book.** Take the assistant directory from the `file=` path of step 1, list the regular files directly under its `tasks/` subdirectory, and parse each one as `key=value` lines. Branch on the outcome.
|
|
|
|
|
|
|
|
|
|
| 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 column |
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
3. **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.
|