feat(seed): 依委派清單種入與重建助理的內建定期檢查項

待辦簿做好了但是空的,也沒有東西會去填它。內建的定期檢查項該排哪些、什麼時候排、多久一次,這些資料就在委派清單裡——每一支非不交的技能都有時間點與週期兩欄。這一輪把清單接成待辦簿的資料來源。

反查靠新增的一個欄位,它非有不可。清單移除一支之後要刪掉對應那筆,但待辦簿的識別碼是建立時間加標題的雜湊,跟技能名無關。拿標題當鍵等於把措辭變成介面,改一個字舊那筆就再也認不出來,於是每次重建都刪不掉舊的又加一筆新的,同一個檢查每輪做兩次。拿動作當鍵,提醒類那十一筆全是同一個字、一支都分不出來,而觸發類那幾筆會撞上使用者自己交辦、動作剛好是同一支技能的那一筆——撞上就是把使用者交辦的事當成內建項刪掉。所以另立一欄記那一筆對應哪一支技能。

因為要加欄位,寫入端只能改存放那一支——它是唯一寫得出待辦檔的入口,這一輪沒有自己寫檔。連帶補上移除那個操作:規格要求不留孤兒,而原本六個操作一個都刪不掉。它對使用者交辦的那幾筆一律擋下、除非人親自帶強制旗標,而種入這一支一次都不帶。擋在單一寫入者這裡最省,那條規定寫在呼叫端的話,呼叫端每多一個就要各自再實作一次。

交出方式與動作是兩套詞彙,對映是這一輪的重點。含觸發就取技能名,其餘一律取提醒。觸發的定義就是呼叫既有技能、內容照那支技能自己的流程走,所以填技能名等於照判定結果做。巡檢與提醒那兩種沒有獨立入口——沒有任何腳本或技能名代表得了某一支技能的唯讀切片——這時候填技能名,助理下一輪就會把整支技能一路跑完,那正是切片交要防的事。所以填提醒:照時程提醒、指出入口,不動手。實測十六筆裡五筆是技能名、十一筆是提醒。

條件式交的三支一律不種入,但逐支吵出來。條件本身是散文,清單裡沒有機器讀得懂的條件欄位,所以條件成立了沒有現在只有人答得出來。種進去的代價有現成例子:其中一支的條件明寫要等它自家路徑不再帶版本號,而那個條件現在不成立,種進去助理每輪都會叫它、每輪停在第一支自家腳本,沒有錯誤、沒有輸出、心跳照寫,看起來完全正常。一個會無聲卡死的項目比一個缺掉的項目難查得多。不種入的代價是看不出為什麼少了它,用逐支印一行保留原因補掉。人確認過某一支條件成立就一支一支帶旗標放行,不給全部放行的旗標,那等於用一個決定蓋掉三個不同的條件。

種入與重建是同一段程式,啟動時每次都跑。不記跑過沒有,也沒有第一次旗標,要不要動手完全由現況決定。分成兩段的話,兩段各自回答該有哪幾筆這同一個問題,等其中一段改了判準就會一邊加一邊刪同一筆,每輪反覆。啟動時跑實際套用、查現況時跑唯讀預覽,巡檢那一輪兩個都不跑——無人值守那一輪移除一筆會把那一筆的執行紀錄與失敗次數一起弄丟,而清單同步到一半就會刪錯,破壞性清理留給人。

清單讀不到的三種情況一筆都不移除。讀不到時,清單上沒有與這台機器沒裝那個外掛分不出來,照字面跑會把所有內建項一次刪光,而且結束碼看起來完全成功。

清單上還在、還可交,只是時間點或週期換了值的那種情況,規格三條規則一條都沒講到。處置是預設只報差異不改:存放那一支刻意沒有編輯操作,改值只能移除再重登,那會換識別碼、把執行紀錄與失敗次數歸零,一個已經連續失敗五次的項目會看起來像全新的。人要換就帶旗標。

跨外掛相依宣告到清單所在的那個外掛,版本下限取現行的發行版——那是清單與它的欄位說明都已經在上面的版本。寫更低的下限會讓一台裝著舊版、清單還不存在或欄位不同的機器通過相依檢查,然後在讀清單那一步才失敗。清單路徑照今天剛改的規則走,由叫用時餵進來的根目錄組成,那一支自己不解。

三份 manifest 的版號一併從 0.1.8 升到 0.1.9,並在相依欄加上清單所在的那個外掛。這一次沒有把版號分成獨立一筆:相依宣告與版號是同一個決定的兩半,宣告了新相依卻不升版,安裝端不會知道要重新檢查相依。
This commit is contained in:
2026-09-03 19:05:07 +08:00
parent e2d7454ee6
commit a168c5904c
7 changed files with 692 additions and 43 deletions
+52 -12
View File
@@ -1,6 +1,6 @@
---
name: assistant
description: '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 five independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, the heartbeat''s own report, and the status event stream that jsc-hooks/tools/report-status.sh drains and rotates, whose starts with no matching end are the only evidence an earlier skill run aborted - 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 - and upserts its MONITOR_CONTENTS entry through jsc-gitea/tools/wiki-contents.sh, which reads the separate CONTENTS wiki repo and keeps one H2 block per machine - the heading is the monitor page''s own name, the fields are bullets under it, and one of them links that page by its absolute wiki-url. Every link on either page is written as [text](URL) and is verified by jsc-gitea/tools/link-check.sh before that page is written, so a dead link stops the write instead of landing on the page. 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).'
description: '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. Before that first round, start reconciles the built-in check items against jsc-meta''s delegation list through tools/seed-tasks.sh - seeding and rebuilding are one idempotent call, conditional rows are held back with their condition printed, and an origin user entry is never touched; status reports the same comparison through plan, which writes nothing, and a patrol round runs neither. One round reads five independent sources - skill and chain usage, version gaps and the restart gate, SDLC stage and work-package locks, the heartbeat''s own report, and the status event stream that jsc-hooks/tools/report-status.sh drains and rotates, whose starts with no matching end are the only evidence an earlier skill run aborted - 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 - and upserts its MONITOR_CONTENTS entry through jsc-gitea/tools/wiki-contents.sh, which reads the separate CONTENTS wiki repo and keeps one H2 block per machine - the heading is the monitor page''s own name, the fields are bullets under it, and one of them links that page by its absolute wiki-url. Every link on either page is written as [text](URL) and is verified by jsc-gitea/tools/link-check.sh before that page is written, so a dead link stops the write instead of landing on the page. 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
@@ -67,15 +67,19 @@ Every tool below is addressed through `{CURRENT}/{plugin}`, with `{CURRENT}` sta
| --- | --- |
| one patrol round | `{CURRENT}/jsc-assist/tools/patrol.sh` |
| the system scheduler | `{CURRENT}/jsc-assist/tools/schedule.sh` |
| the task book, the only writer there is | `{CURRENT}/jsc-assist/tools/tasks.sh` |
| the built-in check items, reconciled against the delegation list | `{CURRENT}/jsc-assist/tools/seed-tasks.sh` |
| the heartbeat | `{CURRENT}/jsc-hooks/hooks/heartbeat.sh` |
| the status event stream | `{CURRENT}/jsc-hooks/tools/report-status.sh` |
| the wiki, through `jsc-gitea:wiki` | `{CURRENT}/jsc-gitea/tools/gitea.sh` |
| the `MONITOR_CONTENTS` entry | `{CURRENT}/jsc-gitea/tools/wiki-contents.sh` |
| the link check every write depends on | `{CURRENT}/jsc-gitea/tools/link-check.sh` |
**The delegation list is read across a plugin boundary, and the same rule applies to it.** `seed-tasks.sh` reads `{CURRENT}/jsc-meta/tools/delegate-spec.tsv`, so the root goes to it as `--root {CURRENT}` — the literal absolute path step 0 already took — and it resolves nothing for itself. `jsc-meta` is declared in this plugin's `jsc.requires` for that reason; when it is not installed the script exits 1 and changes not one entry, because a list that cannot be read is indistinguishable from a list saying nothing is delegable, and acting on the second reading deletes every built-in item at once.
**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, the directory entry depends on `wiki-contents.sh` carrying one too, and both writes depend on `link-check.sh` carrying one — without them the round is refused locally, before any request leaves the machine, and the 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 seven 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`.
**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 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 `{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.
@@ -127,7 +131,8 @@ The call never changes the outcome: it returns 0 even when it cannot write, and
| --- | --- | --- |
| `$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/tasks/{id}` | this skill reads it directly; every write goes through `tasks.sh` | `key=value` lines, one task per file: `id`, `created`, `kind` (`check` / `todo`), `title`, `action`, `trigger`, `recur`, `repo`, `due`, `state` (`pending` / `done` / `paused`), `last_run`, `next_run`, `fail_count`, `origin` (`user` / `assistant`), `spec_key` (`jsc-{domain}:{skill}` for a built-in item, empty for anything a person asked for) |
| `{CURRENT}/jsc-meta/tools/delegate-spec.tsv` | `seed-tasks.sh` only, read-only | the delegation list, tab-separated, one skill per row. `verdict`, `way`, `trigger` and `recur` are what decide whether a skill gets a built-in check item and on what schedule |
| `$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-entry.md` |
| `$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 |
@@ -226,6 +231,34 @@ One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3
| 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 |
## Built-in check items and the delegation list
The delegation list holds one row per jsc skill and records whether that skill can be handed to the background assistant. Every row that is delegable and carries a `trigger` and a `recur` earns one `check` entry in the task book, with `origin: assistant` and `spec_key` set to `jsc-{domain}:{skill}`. `{CURRENT}/jsc-assist/tools/seed-tasks.sh` is the only thing that creates or removes those entries, and it does it by reconciling the two sides rather than by remembering whether it has run before:
| Situation | What the reconcile does |
| --- | --- |
| The list has a delegable row, the task book has no entry for it | add one, `origin: assistant`, `spec_key` set |
| The task book has an entry whose `spec_key` is no longer a delegable row — removed from the list, or changed to `none` | remove that entry, so no orphan is left pointing at a skill nobody delegates any more |
| The entry is `origin: user` | leave it exactly as it is, always. A skill being re-judged is never a reason to delete something a person asked for |
| The row is still delegable but its `trigger`, `recur` or `way` changed | report it as `drift=` and change nothing, unless `--refresh` was passed |
| The row's `verdict` is `cond` | hold it: seed nothing, print a `held=` line carrying the condition text, unless `--allow-cond {key}` named that row |
**Seeding and rebuilding are the same call.** There is no first-run flag and no second code path: what happens is decided by comparing the list against the task book, so the first call seeds every item and every later call is a no-op until the list actually changes. That is why `start` runs it every time rather than only once.
**A `cond` row is held back on purpose, and the report has to say so.** The condition lives in prose in the list, so no script can evaluate it, and one of them says in as many words that its own scripts still resolve through version-carrying paths — seeding it would have the assistant invoke, every single round, something that stops on its first script call with no error, no output and a heartbeat that still looks healthy. So the default is to hold, and every held row is printed with its condition and the half that stays with a human. Never quietly drop them: a missing item nobody can account for is the failure this reporting prevents.
## seed-tasks.sh exit codes
| Code | Meaning | What to do |
| --- | --- | --- |
| 0 | The two sides are reconciled — `plan` printed its verdict, or `apply` made the changes. Zero changes is this code too | Carry on, and carry the `added=`, `removed=`, `kept=`, `drift=`, `held=` and `bad=` counts into the report |
| 1 | The delegation list could not be read, so **nothing was touched** | Report `jsc-meta` as missing or unreadable and say the built-in items were left exactly as they were. Never report this as "the list has no delegable skills" |
| 2 | The list was read but not one delegable row came out of it, so **nothing was touched** | Report the list itself as suspect — a half-synced or damaged list would otherwise delete every built-in item. Point at the file and stop |
| 3 | `tasks.sh` was not found, so **nothing was touched** | Report the installation as incomplete: the task book has exactly one writer and it is missing |
| 4 | At least one `add` or `remove` failed; the rest were done | Report every `add_failed=` and `remove_failed=` line with the `tasks.sh` exit code it carries, and judge each by that script's own code table |
| 5 | Filesystem failure — the scratch directory or a scratch file could not be written | Report it with the path; the reconcile could not even be computed |
| 6 | Usage error — an unknown subcommand or option, a `--root` that is not absolute, or an `--allow-cond` value that is not `jsc-{domain}:{skill}` | A defect in the call. Correct it and run it once more |
## Boundaries
The six limits in `AGENTS.md`「助理的界線」 hold for all four operations. Four of them need saying out loud here:
@@ -234,6 +267,7 @@ The six limits in `AGENTS.md`「助理的界線」 hold for all four operations.
- **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`. A command that is not on the allow list is a question too — the permission prompt is one, and it is the one nobody sees — which is why step 0 hands that round its root instead of letting it resolve one. 界線 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 directory page is a separate write in a separate wiki repo, and `wiki-contents.sh` does it: that page keeps one H2 block per machine, and this machine's block is the only one that is updated. 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.
- **Only a human-initiated operation reconciles the built-in items; an unattended round reports the difference and stops there.** `start` runs `seed-tasks.sh apply`, because somebody asked for it and is there to read what it added and removed. `status` runs `seed-tasks.sh plan`, which writes nothing. `patrol` runs neither: removing a check entry destroys that entry's `last_run` and `fail_count` history, and 界線 5 keeps destructive cleanup with the human — a round that deletes a row at three in the morning because the list was mid-sync leaves nobody able to see that the row ever existed. The consequence is worth stating: on a machine nobody starts or inspects, a list change reaches the task book only at the next `start`. 界線 3 and 界線 5.
- **`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
@@ -246,23 +280,27 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
## 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.
`start` proves the loop works before it schedules it: the built-in items first, then one patrol round, 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 write of the monitor page, a directory-entry failure other than exit 3, 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.
1. **Reconcile the built-in check items against the delegation list.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh apply --root {CURRENT}`. This comes before the round, so the round's own task-book section already shows the items this machine is supposed to be checking. Judge the result by the seed-tasks.sh exit-code table, and keep every `add=`, `remove=`, `drift=`, `held=`, `bad=`, `dup=` and `skip_user=` line plus the summary counts for the report. **Exit 1, 2 and 3 do not stop the start.** Nothing was touched in any of those cases, so the assistant still has whatever items it had before and the round is still worth running: record what the code means, put it into the closing report, and carry on to step 2. Exit 4 is the same — the entries that did get added or removed stand, and the failed ones are named. Never pass `--force` and never pass `--allow-cond` on your own initiative: the first would let this step delete something a person asked for, and the second asserts a condition only a person can check. Completion condition: the exit code and the summary counts are recorded, with every `held=` row's skill name kept for the report, or the code was recorded as "nothing was touched" and step 2 was reached anyway.
2. **Confirm the heartbeat.** Run `{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.
2. **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 write of the monitor page, a directory-entry failure other than exit 3, or `finish` exit 2, 4 or 5), the start has failed: report the round's outcome and the code, do not run step 4, 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 3 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.
3. **Install the patrol entry.** Run `{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=`, `patrol_root=`, 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, the tool root the entry carries and the allow rules are recorded with it.
3. **Confirm the heartbeat.** Run `{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.
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, the `patrol_root=` the entry carries — that is what every later round reads its tool root from — 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:
4. **Install the patrol entry.** Run `{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=`, `patrol_root=`, 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 5 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, the tool root the entry carries and the allow rules are recorded with it.
| Step 3 | Notice |
5. **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, the `patrol_root=` the entry carries — that is what every later round reads its tool root from — 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.
**Then report step 1's reconcile in its own block**, because it is the only place the built-in items are accounted for: how many were added, how many removed, how many left alone, then every held `cond` row by name with the reason it was held, every `bad=` row as a defect in the list rather than in this machine, every `drift=` row with the change the list now asks for and the note that `--refresh` is what applies it, and every `dup=` or `skip_user=` row as an entry a person has to settle. An exit of 1, 2 or 3 is reported here as "the built-in items were left as they were" with the reason, never as "there is nothing to check". Close with the notice that matches step 4's outcome, printed literally with `{ttl}` replaced by the TTL just read and `{period}` by the derived period:
| Step 4 | 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 tool root the entry carries, the allow rules and the snapshot reminder, and exactly one notice above appears with the real numbers.
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 tool root the entry carries, the allow rules and the snapshot reminder; the reconcile block carries the three counts and one line per held, drifted, rejected or duplicated row; and exactly one notice above appears with the real numbers.
## patrol
@@ -324,7 +362,7 @@ One round: read five sources, record the result, then beat. Everything before th
## status
Read-only throughout. This operation creates, modifies and deletes nothing under `$JSC_HOME`, and it never calls `write` or `clear`.
Read-only throughout. This operation creates, modifies and deletes nothing under `$JSC_HOME`, and it never calls `write` or `clear`. It compares the built-in items against the delegation list with `seed-tasks.sh plan`, never with `apply`.
1. **Read the heartbeat through the script.** Run `{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.
@@ -356,7 +394,9 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
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.
7. **Check the built-in items against the delegation list, read-only.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh plan --root {CURRENT}`. `plan` writes nothing at all — it prints what a reconcile would do and stops — which is what makes it safe here, and `apply` must never be run from `status`. Judge the code by the seed-tasks.sh table and report the difference: the count of items the list expects but the task book lacks, the count of orphans the task book still holds, every `held=` row by name, and every `drift=` row with the change the list asks for. Say plainly that `start` is what applies any of it. On exit 1, 2 or 3 report that the comparison could not be made and why, and never present that as an aligned task book. Completion condition: the difference is reported with its counts and the held rows named, or the reason it could not be computed is reported, and nothing under `$JSC_HOME` was written.
8. **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