Files
assist/skills/assistant/SKILL.md
T
jiantw83 02bfc70f4e fix(assistant): 讓執行路徑走錯與警示原因都看得見
兩支腳本開跑就檢查自己的執行路徑,監控頁的摘要表與最新一輪那一塊補上
「警示來源」。

兩件都是實測之後補的。用全新的 CLI 行程跑,代理人會把工具路徑解回外掛
快取那一份,不是指定的工作樹那一份。原本的對策只在技能說明書裡寫明禁
止,那是文件約束,擋不住真的走錯的那一輪。權限閘門只放行 current 那一
組確切路徑,走錯就被靜靜擋掉,那一輪不寫心跳,外面只看得到心跳過期。
另一頭,沙箱跑出來的摘要列是「警示、四項全過、待人處理 0」,讀的人看
不出警示哪來。查過不是缺陷:四項來源都讀得到,是讀到的內容有警示。摘
要表的用途本來就是一眼看出從哪一輪開始壞,少了原因那一欄就做不到。

巡檢與排程兩支腳本開跑時先比對自己是不是從 current 底下被叫起來的,不
是就往 stderr 印一行警告,點名實際路徑、應該用的路徑與理由。刻意只警告
不中止:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉,真
正的失敗本來就發生在權限閘門那裡。摘要表從四欄加到五欄,最新一輪那一
塊也補一列,值取每一處設警示時收下來的短理由,多個用頓號串,沒有就寫
「無」。腳本裡每一處把判定改成警示的地方,一律改走同一支函式順手收下
理由,collect 的輸出多印一行 warn_sources。技能主文、行為清單與監控頁
範本跟著寫明這兩件事。

功能範圍是助理的一輪巡檢與系統排程安裝。
2026-09-01 18:54:05 +08:00

43 KiB
Raw Blame History

name, description
name description
assistant Start, inspect, patrol or stop the background assistant: jsc-hooks/hooks/heartbeat.sh owns the freshness verdict, tools/schedule.sh the system scheduler, tools/patrol.sh one round. The heartbeat is written by a completed round and by nothing else, so the schedule carries the patrol entry only, its period from the heartbeat TTL; start runs one round then installs that entry - absolute CLI path, environment snapshot, unattended write confirmation, which cron lacks - status prints heartbeat, schedule and task book read-only, stop removes the entry before clearing the heartbeat. One round reads four independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, and the heartbeat's own report - then rewrites wiki MONITOR_{HASH} through jsc-gitea:wiki as three fixed blocks: basic data untouched, the latest round replaced whole, a 24-row summary table. A round that cannot record its result writes no heartbeat; one that starts while the previous holds the lock stands down. Use when someone starts, patrols or stops the assistant, or asks whether it runs and what is queued; not for environment health checks (jsc-cli:doctor), not for skill usage counts (jsc-log:stats).

assistant — start, status, patrol, stop

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 four operations that touch that evidence: patrol writes it, status reads it, stop clears it, and start bootstraps the whole loop.

$JSC_HOME/current/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.

$JSC_HOME/current/jsc-assist/tools/schedule.sh owns every system-scheduler operation: installing an entry, removing it, and reading which entries exist. Never call crontab or schtasks from this skill, and never edit a crontab by hand.

$JSC_HOME/current/jsc-assist/tools/patrol.sh owns one patrol round: taking the round lock, reading the four sources, composing the monitor page's blocks, and — after the page carries this round — writing the heartbeat. Never re-read a source this skill already handed to that script, and never compose a block by hand; the script prints the file paths.

All three flows have fixed inputs and outputs, so all three live in scripts. The task book is the only thing this skill reads for itself, and that is one directory listing.

Tool paths

Every tool below is addressed through $JSC_HOME/current/{plugin}, and $JSC_HOME falls back to ~/.jsc exactly as everywhere else in this skill:

What it does Path to run
one patrol round $JSC_HOME/current/jsc-assist/tools/patrol.sh
the system scheduler $JSC_HOME/current/jsc-assist/tools/schedule.sh
the heartbeat $JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh
the wiki, through jsc-gitea:wiki $JSC_HOME/current/jsc-gitea/tools/gitea.sh

A Skill(...) rule permits invoking that skill and nothing more. Every Bash call inside it is still checked on its own, so jsc-gitea:wiki reaching the wiki depends on gitea.sh carrying its own rule — without it the round is refused locally, before any request leaves the machine, and the monitor page never gets written.

Never build a tool path out of the base directory the CLI hands you in the skill prompt. That directory points into the plugin cache and carries a version segment, and the permission gate allows exactly the four paths above and nothing else. A cache path is therefore refused silently: the round stops on a permission prompt nobody can answer, records nothing, writes no heartbeat, and the refusal looks exactly like a broken tool. Read the paths off this table every time — not off the prompt, not off a previous transcript, not off crontab -l.

Both scripts check this for themselves: run from anywhere outside $JSC_HOME/current, they print a [WARN] line on stderr naming the path they were started from and the path they should have been started from, and then carry on. That line means this round is on the wrong path — quote it, fix the path, and do not treat the round's success as proof that the path was fine.

current is a set of version-free links that jsc-cli:deploy maintains, so an upgrade moves the cache and leaves these paths alone. When one of them is missing, report the missing link and say jsc-cli:deploy has to run; never fall back to a cache path to get the round through, and never create the link here.

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; running one round, patrolling, or a scheduled wake-up is patrol; stopping, halting or shutting it down is stop. When the request names none of the four, or names more than one, ask through the jsc-ask:ask decision tree with those four as the options, each stating its effect — start runs one round and installs the scheduled entry that keeps running rounds, status changes nothing, patrol runs one round and writes one heartbeat, stop removes that entry and deletes the heartbeat. The one exception: a patrol invocation never asks anything at all (see 界線 1 below). Never guess, and never run a second operation the caller did not ask for. Completion condition: exactly one of start, status, patrol, stop is chosen and named in the report.

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 entry appends 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/patrol.lock/ patrol.sh only the round lock, a directory. info holds round, pid, started
$JSC_HOME/assistant/patrol/ patrol.sh only one round's scratch files, including latest.md, summary.md, summary-row.md, newpage.md and contents.tsv
$JSC_HOME/assistant/usage-prev.tsv patrol.sh only last recorded round's cumulative usage counts, so the next round can print a real per-round delta

$JSC_HOME 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 last patrol round finished more than one TTL ago 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 — no patrol round has ever finished 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 $JSC_HOME/current/jsc-assist/tools/schedule.sh is the only thing here that touches it. One job exists, written as exactly one entry carrying the fixed marker # jsc-assist:assistant patrol:

Job Period Runs Installed by start
patrol derived from the heartbeat TTL (*/2 * * * * at the default TTL of 300 seconds) one patrol round through the caller's CLI yes, always
heartbeat — nothing. This job existed in the previous version and is no longer installable no — install heartbeat exits 6

The heartbeat job is gone on purpose. It used to call heartbeat.sh write every minute, which made a fresh heartbeat prove only that cron was alive. Anything that writes a heartbeat outside a finished patrol round brings that back, so install heartbeat is refused, and install patrol deletes any leftover heartbeat entry from an older install and reports legacy_removed=1. Say that number in the report — a surviving legacy entry silently undoes this whole design.

The period is derived, never guessed. The heartbeat now moves once per patrol round, so the round period has to fit inside the freshness threshold. schedule.sh reads the machine's effective threshold from heartbeat.sh report's ttl= field and picks the largest whole-hour-dividing minute count P with 2 × P × 60 < ttl: one missed round still reads fresh, two missed rounds read stale. At the default 300 seconds that is every 2 minutes; raise JSC_ASSISTANT_HEARTBEAT_TTL to 1800 and it becomes every 12 minutes. Report both numbers (ttl=, period=) so the operator can see the trade-off and change it in one place. --period overrides the calculation and is checked against the same inequality; a period that does not fit exits 6 rather than installing a schedule that keeps the heartbeat permanently stale.

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 entries. 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 markers 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. The command is installed with </dev/null, so nothing it runs can block on input. A patrol round that stops to ask for a tool permission hangs that round, and the lock it holds stands the next round down until the lock ages out — which is why patrol asks nothing, of anybody, ever.
  • The entry carries its own environment. cron gives it a short PATH, no settings file and no tty, so schedule.sh writes three things into the entry: the CLI resolved to an absolute path with command -v, a snapshot of the wiki variables taken at install time (GITEA_HOST, GITEA_TOKEN, JSC_HOME, JSC_ASSISTANT_HEARTBEAT_TTL and every set JSC_WIKI_REPO*), and JSC_GITEA_CONFIRM=yes, because the write confirmation only recognises a tty and an unattended round has nobody to confirm. Two consequences belong in every report: the entry holds a copy of the token, so the crontab file has to stay readable by its owner alone, and a changed variable only reaches the entry after another install. install prints the snapshotted names in env_snapshot= and masks the token in every entry it prints — never print an entry read from crontab -l yourself.
  • install prints the permission rules that round needs. One allow_rule= line each, with * in the path's version segment. Hand them to the operator verbatim: an unattended round that hits a permission prompt hangs until the lock ages out, and nobody is there to approve it. Write(...) rules do nothing for file writes — only Edit(...) is recognised — so never turn a printed Edit rule into a Write one.

What a fresh heartbeat actually proves

The heartbeat is written in exactly one place: $JSC_HOME/current/jsc-assist/tools/patrol.sh finish, and finish is called only after that round's result is on the monitor page. So the verdict 新鮮 now proves one thing that is worth proving — the last patrol round ran to the end and its result was recorded — and it still does not prove three others:

  • Not that the round was clean. Four sources are read independently and a round with three failures still records and still beats. The health of a round is 本輪判定 on the monitor page, never the heartbeat.
  • Not that any task in the book moved. The task rows — last_run, next_run, fail_count — are the only evidence about work.
  • Not that the round did anything about what it found. The patrol reports; a human acts. 界線 6.

The failure this design buys is the one worth having: a round that cannot read its sources, cannot reach the wiki, or dies half way writes no heartbeat, so the heartbeat ages past the TTL and every reader sees 過期. A silent patrol is now indistinguishable from a stopped assistant, which is exactly right. status still prints the heartbeat, the schedule and the task book as three separate facts, and 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_HOME/current/jsc-hooks/hooks/heartbeat.sh was not found, so the TTL cannot be read and the period cannot be derived 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, install heartbeat, a --period that does not fit the TTL, the patrol CLI could not be determined, or that CLI's executable is not on PATH so no absolute path can be written A defect in the call or a CLI that is not installed, not a state of the machine. The stderr line names which one it is; quote it, correct the command line, and run it once more. Report a second exit 6 as a defect in this skill and stop

patrol.sh exit codes

One table for all three subcommands. Read collect's codes carefully: 1 and 3 are results, not aborts. A round with failed items still has a result to record, and refusing to record it would hide the failure instead of showing it.

Code Meaning What to do
0 collect: all four items read to the end, empty sources included. finish: heartbeat written, snapshot promoted, lock released. abort: lock released Carry on with the operation's next step
1 collect: partial success — at least one item failed and at least one produced a result Write the page anyway. The latest-round block already marks the failed items and the round verdict is 警示. Name the failed items and their note= text in the report
2 finish: $JSC_HOME/current/jsc-hooks/hooks/heartbeat.sh was not found The round completed and is recorded, but no heartbeat exists to prove it. Report the round as recorded and the heartbeat as not written, say jsc-hooks 0.3.7 or newer has to be installed, and run $JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {id} to release the lock
3 collect: all four items failed Write the page anyway, with verdict 異常. A page listing four failures is the signal; a missing page is not. Then carry on to finish as usual — the round did complete
4 Another round holds the lock (collect), or the lock is no longer this round's (finish, abort) Not a failure. On collect: report 本輪讓開 and name the holder and its age from the printed lock=busy line, then write nothing and stop. On finish: the previous round overran and was taken over, so this round's result does not count — report it, write no heartbeat, and stop
5 Filesystem failure — the lock could not be created or released, a scratch file could not be written, the snapshot could not be promoted, or heartbeat.sh write returned non-zero Serious. Report it loudly with the stderr text and the path. On a finish failure the round is recorded but unproven: say so plainly and never claim the round beat
6 Usage error — an unknown subcommand, a missing --round, or an option with no value A defect in the call. Correct it and run it once more; report a second exit 6 as a defect in this skill and stop

Boundaries

The six limits in AGENTS.md「助理的界線」 hold for all four operations. Four 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. 界線 2.
  • A patrol round asks nothing. It runs from cron with nobody present, so there is no one to answer and a question hangs the round. Every branch in the patrol steps below resolves without a question: a missing source is recorded as missing, an ambiguous result is recorded verbatim, and a round that cannot proceed aborts and reports. Never call jsc-ask:ask from patrol. 界線 1.
  • A patrol round rewrites the monitor page as three fixed blocks. Read the old page back first; keep 本頁基本資料 as it stands, replace 最新一輪 whole, put this round's row on top of the summary table and cut it to 24; then put the whole page. The contents page gets its own row updated and nobody else's. A page that could not be read is a page that does not get written — the summary table only survives if the old one came back. 界線 4.
  • A patrol round reports; it never acts on what it found. The 待人處理 rows name an entry point for a human. The patrol does not run that entry point, does not fix a hook, does not update a plugin and does not touch a repository. 界線 3 and 界線 6.
  • 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 last patrol round finished", and the schedule entry is what keeps rounds running, so a stop that leaves either behind leaves a lie behind. 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

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.

The round lock is the one thing a crash does leave behind, and it ages out the same way: patrol.sh collect breaks a lock older than the heartbeat TTL, takes it, and prints lock_broken=1 so the takeover lands on the monitor page instead of happening quietly. The overrun round that lost its lock then gets exit 4 from finish and writes no heartbeat, which is correct — it never reached the end.

That property holds only while nothing fakes a heartbeat. write is called by $JSC_HOME/current/jsc-assist/tools/patrol.sh finish and nowhere else. start does not call it, status does not call it, stop does not call it, no scheduled entry calls it, and no other skill calls it. A heartbeat written by anything that is not a finished round says a round finished when none did, 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 proves the loop works before it schedules it: one patrol round first, then the scheduled entry. It installs no daemon and writes no bare heartbeat.

  1. Run one patrol round. Follow every step of the patrol operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (collect exit 4, 5 or 6, an empty hash=, a failed wiki write, or finish exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 2, and do not claim a started assistant. A round that completed with failed items (collect exit 1 or 3) is still a completed round — carry on to step 2 and name the failures in the closing report. Completion condition: patrol.sh finish exited 0, or the failure report naming the step and the code has been printed and no start was claimed.

  2. Confirm the heartbeat. Run $JSC_HOME/current/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 round 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. Install the patrol entry. Run $JSC_HOME/current/jsc-assist/tools/schedule.sh install patrol. Judge the result by the schedule.sh exit-code table, and keep the printed entry=, ttl=, period=, legacy_removed=, others_kept=, env_snapshot=, every allow_rule= line and service= 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. Exit 6 with a CLI executable that is not on PATH is the second one: nothing was installed, and the fix is to install that CLI or to pass --patrol-cmd, not to write a bare command name into the entry. On 2, 3, 4, 5 or 6 nothing is scheduled — report the code, say the round ran but no further round will, and do not claim the assistant will stay alive. Completion condition: the exit code is recorded, and on exit 0 the entry line, the TTL, the period, the legacy count, the surviving-entry count, the snapshotted variable names and the allow rules are recorded with it.

  4. Report the start. Print the round's verdict and its four item results, the monitor page that was written, the heartbeat path, the local time of ts, the TTL in seconds, pid, cli and session as hints, then the scheduler mechanism, the derived period, the installed entry line as the script printed it with the token already masked, how many legacy heartbeat entries were removed, and how many other entries were left untouched. Then hand over the two operator items the install printed: the allow_rule= lines verbatim, so the unattended round never meets a permission prompt, and the reminder that the entry holds a snapshot of the listed variables including the token — keep the crontab file readable by its owner alone, and run install again after any of those variables changes. Close with the notice that matches step 3's outcome, printed literally with {ttl} replaced by the TTL just read and {period} by the derived period:

    Step 3 Notice
    exit 0 助理已啟動,第一輪巡檢跑完了,結果寫上監控頁了,心跳也寫了。排程接上了,之後每 {period} 分鐘跑一輪,每一輪跑完才寫一次心跳。心跳新鮮代表上一輪巡檢真的做完了;那一輪四項有沒有全過,看監控頁的本輪判定。
    exit 1 助理已啟動,第一輪巡檢跑完了,排程條目也寫進去了,但 cron 服務沒在跑,那一筆一次都不會被執行。心跳過了 {ttl} 秒就會過期。請先跑 sudo service cron start,重開 WSL 之後要再跑一次。
    其他結束碼 助理已啟動,第一輪巡檢跑完了,但排程沒接上(結束碼 {code})。不會再有下一輪,心跳過了 {ttl} 秒就會過期,屆時請再跑一次 start。

    Completion condition: the report carries the round verdict, the monitor page name, the path, the local heartbeat time, the TTL, the period, the three hint fields, the scheduler outcome, the allow rules and the snapshot reminder, and exactly one notice above appears with the real numbers.

patrol

One round: read four sources, record the result, then beat. Everything before the heartbeat is read-only except the round's own scratch files. Ask nobody anything.

  1. Collect. Run $JSC_HOME/current/jsc-assist/tools/patrol.sh collect --trigger 排程 (use --trigger 手動 when a person asked for this round). Judge the exit code by the patrol.sh table. Exit 4 stands the round down — report the holder and its age from the printed lock=busy line, and stop; write no page and no heartbeat. Exit 5 and 6 stop the round the same way, with the code and the stderr text. Exit 0, 1 and 3 all carry on to step 2. Record round=, lock_broken=, hash=, page=, verdict=, failed_sources=, warn_sources=, pending=, every item= line, and the file paths latest_file=, summary_file=, summary_row_file=, newpage_file= and contents_file=. Completion condition: the round id, the page name and the five file paths are recorded, or the stand-down or the failure was reported and the round stopped.

  2. Check the page name. An empty hash= means jsc-gitea/tools/hash-id could not be found or could not run, so there is no page to write to and nothing can be recorded. Run $JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}, report that the round found its results but has nowhere to put them, name jsc-gitea as missing, and stop. Never invent a page name — a hand-made name lands the content on a page nobody reads. Completion condition: page= holds a MONITOR_{HASH} name, or the abort ran and the round was reported as unrecorded.

  3. Rebuild MONITOR_{HASH} from its three blocks. Hand it to jsc-gitea:wiki with page type MONITOR: read the page back first, then build the new body out of what came back plus this round's files, in this order and with nothing else on the page:

    Block Where it comes from
    本頁基本資料 the old page, byte for byte from its heading to the line before 最新一輪. Never rewritten, never re-derived
    最新一輪 the whole content of latest_file, replacing the old block entirely
    近 24 輪摘要 summary_file, which already holds the heading, the five-column table header (巡檢時間、本輪判定、四項成敗、待人處理、警示來源) and this round's row; then the old table's data rows in their old order underneath, cut so the table holds at most 24 rows

    Put the whole page. An old-format page — per-round sections stacked up, no summary table — has no rows to carry over: keep its 本頁基本資料 block, drop the stacked sections, let the table start with this round's row, and say in the report that the page was converted. Only exit 4 from the read permits creating the page instead, and then the body is the whole content of newpage_file, which already carries all three blocks. Exit 7 and exit 8 mean the old content is unknown: create nothing, write nothing — rebuilding a page from an unknown original throws the summary table away. On any write failure — including exit 3 with no wiki repo configured for MONITOR, which the patrol cannot ask about — run $JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}, report the code, and stop. No record, no heartbeat. Completion condition: the put or the create returned success and the page holds exactly three blocks with the summary table at 24 rows or fewer and this round's row on top, or the abort ran and the round was reported as unrecorded with its exit code.

  4. Update this machine's row in MONITOR_CONTENTS. Take the row= line from contents_file — it is already the finished table row. Hand it to jsc-gitea:wiki: read the whole page, match the row whose 主機 and 帳號 columns both equal this round's host= and user=, overwrite that row's remaining columns, and put the whole page back. No matching row means append one. Never rebuild this page the way the content page is rebuilt, and never touch another machine's row — every other row here belongs to a machine that is not this one, and one careless whole-page write deletes their records. On failure, run $JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}, report the code, and stop. Completion condition: exactly one row carries this machine's 主機 and 帳號 values, every other row is byte-identical to what was read, and the put returned success.

  5. Write the heartbeat. Run $JSC_HOME/current/jsc-assist/tools/patrol.sh finish --round {round}. This is the last step for a reason: it is the only thing that turns a fresh heartbeat into a true statement. Judge the exit code by the patrol.sh table — 2, 4 and 5 all mean the round is recorded but unproven, and each has its own report line there. Completion condition: finish exited 0, or the failure was reported as "recorded but no heartbeat" with its code.

  6. Report the round. Print the round verdict and, when it is 警示, the warn_sources= text that says why — a round can read all four sources and still come out 警示, and that column is the only place the reason appears; then one line per item with its status= and, for a failure, its note=; the monitor page name and the contents row that was written; whether the heartbeat was written; and, when lock_broken=1, that the previous round's lock was taken over because it had aged past the TTL. Close with the 待人處理 rows from the latest-round block, verbatim, and nothing else — the patrol names an entry point and stops there. Completion condition: all four items appear in the report, the heartbeat outcome is stated as written or not written, and no suggestion in 待人處理 was acted on.

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_HOME/current/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. Read the schedule. Run $JSC_HOME/current/jsc-assist/tools/schedule.sh status. It writes nothing. Record mechanism=, service=, ttl=, period= and the installed= value of both jobs. A heartbeat job reported as installed is a leftover from an older version: say so, and say start or schedule.sh install patrol removes it. 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. 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, derived period, 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 period, and the row count equals the task count from step 2.

  5. Say what the two blocks together mean. Four combinations get an explicit sentence, because each one reads as something it is not:

    Heartbeat Schedule Say
    新鮮 patrol installed, service running 上一輪巡檢跑完了,結果也記上監控頁了,排程還在跑。那一輪四項有沒有全過,要看監控頁的本輪判定
    新鮮 not installed, or service stopped 上一輪巡檢跑完了,但沒有排程在叫下一輪,過了 TTL 心跳就會過期
    過期 or 不存在 patrol installed, service running 排程裝著卻沒有新的心跳,巡檢自己跑失敗了,去看 $JSC_HOME/assistant/schedule.log 與監控頁的最新一輪
    any heartbeat job installed 舊版的心跳排程還留著,它會蓋掉「心跳等於巡檢跑完」這件事。請跑一次 start,或 schedule.sh install patrol 把它清掉

    Completion condition: every matching sentence is printed, or none of the four 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

  1. Record what is being stopped. Run $JSC_HOME/current/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 no round has finished; 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. Remove the schedule first. Run $JSC_HOME/current/jsc-assist/tools/schedule.sh remove all — both job names, so the patrol entry and any leftover heartbeat entry from an older install both go. This comes before the clear and never after: clear first and the next scheduled round 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 rounds will keep running 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 the next round rewrites 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. Clear the heartbeat. Run $JSC_HOME/current/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 a round just finished 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.

  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:

    助理已停止,排程移除了,心跳也清掉了,其他人的排程一筆都沒動。靠心跳判定的 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.

Round lock and a round that will not stop leaving one behind

A patrol round holds $JSC_HOME/assistant/patrol.lock from collect to finish or abort, which spans the wiki writes — the slow part. Two consequences bind every branch above:

  • Every path out of a started round ends in finish or abort. Steps 2, 3 and 4 of patrol each name their abort. A round that stops without either leaves the lock standing until it ages out, which stands the next rounds down for up to one TTL. There is no third option.
  • stop does not remove the lock. It is not a state file that records work, but it is also not this skill's to delete while a round may still be using it; it ages out on its own within one TTL. If an operator reports that every round stands down, tell them the holder and age from the lock=busy line and let them decide — 界線 5 keeps destructive cleanup with the human.