fix(assistant): 巡檢改用字面絕對路徑,根目錄由排程條目帶進來
無人看管的排程輪次在第一支腳本就被權限層擋下。心跳因此寫不出來,助理斷了兩個多小時,沒有人發現。 2026-09-02 到 09-03 用非互動模式比照排程環境實測七種寫法,歸納出兩條判準。一、無人值守時只有允許清單上的完整字面指令跑得動,沒有「預設安全的唯讀指令」這回事,連 readlink 與 ls 都要有自己的規則。二、路徑中段的萬用字元不匹配,規則與指令都必須是完整字面,所以帶版本號的快取路徑放不進允許清單。「先解路徑再用」因此不成立:解路徑的指令自己就過不了,而路徑能寫成字面就不必解。 助理技能新增路徑守則與 Step 0。守則寫明權限層比對的是指令還沒展開的字面字串,帶未展開變數或波浪號的路徑一律要核准,並附上七列實測佐證表。Step 0 從「自己跑 readlink 解路徑」改成「從叫用文字的『工具根目錄=』取字面絕對路徑」,排程那一輪一個解析指令都不跑;人在現場叫用才用 readlink 解一次,那一次有人可以按同意。取不到根目錄就停下回報,收尾狀態取 aborted,不猜也不退回帶變數的路徑。全篇 46 處腳本呼叫改成字面絕對路徑。 排程工具在安裝時把解好的字面根目錄寫進條目的提示文字,並印成 patrol_root=。條目與允許規則共用同一個值,兩邊各算各的就會差開,而差開的那一輪是被靜靜擋掉。允許規則的提示改成完整字面路徑,不寫變數、波浪號與萬用字元。JSC_HOME 解不出絕對路徑時回結束碼 6,不讓相對路徑寫進條目。自訂巡檢指令沒帶那一段只警告、不中止。 行為契約四列與說明文件兩處敘述一併跟上。
This commit is contained in:
+87
-42
@@ -7,33 +7,77 @@ description: 'Start, inspect, patrol or stop the background assistant: jsc-hooks
|
||||
|
||||
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.
|
||||
`{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.
|
||||
`{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 five 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.
|
||||
`{CURRENT}/jsc-assist/tools/patrol.sh` owns one patrol round: taking the round lock, reading the five 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.
|
||||
|
||||
## Step 0 — take the tool root from the invocation
|
||||
|
||||
Every operation starts here, before its own step 1. **This document calls the tool root `{CURRENT}`**, and every `{CURRENT}` below is replaced by it character for character: `{CURRENT}/jsc-assist/tools/patrol.sh` is run as `/root/.jsc/current/jsc-assist/tools/patrol.sh`.
|
||||
|
||||
The root comes from outside this skill. `schedule.sh` resolves it while a person is installing the schedule, and writes it into the entry's prompt as `工具根目錄={literal absolute path}`, so the round that entry wakes reads the root out of the text that woke it and runs no command at all.
|
||||
|
||||
| Who invoked this round | Where `{CURRENT}` comes from |
|
||||
| --- | --- |
|
||||
| the schedule — an unattended round, the `patrol` whose trigger is 排程 | the path after `工具根目錄=` in the invocation text, taken verbatim. No command is run |
|
||||
| a person, in front of the terminal | the same token when the invocation carries one; otherwise `readlink -f "$JSC_HOME/current"`, run once |
|
||||
|
||||
**An unattended round that finds no root in its invocation stops there.** Report that the scheduled entry carries no `工具根目錄=` — an entry written by an older `schedule.sh` — say the fix is to run `start` again, or `jsc-assist/tools/schedule.sh install patrol` under `$JSC_HOME/current`, so the entry is rewritten with the root in it. Then take the operation's `aborted` status, write the `skill-end`, and stop.
|
||||
|
||||
Never work the root out instead. `readlink -f "$JSC_HOME/current"`, `ls -d "$JSC_HOME/current"` and every other resolve are refused in an unattended session — measured, see the table below — so running one does not produce a root, it produces a round that stops one step earlier having recorded nothing. Never fall back to `$JSC_HOME/current` as a written-out path either, never take a path from the plugin prompt or a previous transcript, and never guess.
|
||||
|
||||
**Only an attended invocation may resolve the root itself.** `readlink -f "$JSC_HOME/current"` covers the documented `~/.jsc` fallback in the same call and prints one literal absolute path. It raises one permission prompt, and a person is there to answer it once. That is the whole reason the branch exists: portability survives where somebody can approve it, and nowhere else.
|
||||
|
||||
Take the root once per invocation and reuse that one answer. Never resolve it again per call, never print it as a report line of its own, and never add a tool that prints it. Never test the root with a command either — an unattended round cannot, and the first script call is the test that matters anyway.
|
||||
|
||||
An empty token, an empty `readlink` result, or a path that is not absolute means there is no root to work with. Report it, say `jsc-cli:deploy` has to run, take the operation's `aborted` status, and stop. Never fall back to a cache path, and never create the root here. Completion condition: one literal absolute path is in hand and every later command line carries it, or the missing root was reported and the operation stopped.
|
||||
|
||||
## Every script call carries a literal absolute path
|
||||
|
||||
**No command line in this skill carries a variable or a tilde, and the one resolve above is the single exception, allowed only when a person is watching.** Never type `$JSC_HOME`, `${JSC_HOME}` or `~` into any other command line.
|
||||
|
||||
The reason is the permission layer: it matches its rules against the command text as written, before the shell expands anything. Two properties follow from what was measured on this machine, and every rule in this skill rests on them:
|
||||
|
||||
1. **Unattended, only a full literal command that is on the allow list runs.** There is no such thing as a read-only command that is safe by default: a bare `ls -d` is refused exactly like everything else, and a refusal in a session with nobody in it is silent.
|
||||
2. **A wildcard in the middle of a path does not match.** The rule and the command both have to be complete literals. A rule holding `*` where a version number goes matches nothing, so a cache path is refused however the rule is written.
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `/root/.jsc/current/jsc-assist/tools/patrol.sh` — a literal rule for it is on the allow list | ran |
|
||||
| `$JSC_HOME/current/jsc-assist/tools/patrol.sh` | refused |
|
||||
| `~/.jsc/current/jsc-assist/tools/patrol.sh` | refused |
|
||||
| `readlink -f "$JSC_HOME/current"` | refused |
|
||||
| `ls -d "$JSC_HOME/current"` | refused |
|
||||
| `ls -d /root/.jsc/current` — literal, read-only, no rule for it | refused |
|
||||
| `/root/.claude/plugins/cache/jsc/jsc-assist/0.1.0/tools/patrol.sh` — rule written with `*` for the version segment | refused |
|
||||
|
||||
A literal allow rule that itself starts with `$JSC_HOME` was added to the settings file and the same call was still refused, so no permission rule makes the variable form work either. The literal path is the whole fix, on both sides.
|
||||
|
||||
**These rules outrank portability, and the next maintainer is the one who has to know why.** A variable in the path reads as the portable choice and costs nothing while a person is watching: the prompt appears, somebody approves it, the round carries on. The scheduled round has nobody to approve it. It stops at its first script call, records nothing, writes no heartbeat, and the machine then reads as a stopped assistant with no trace of the refusal anywhere. And the resolve is no way out of that, because row 4 of the table is the resolve itself: a round that cannot run a script cannot run the command that would have told it which script to run. That is why the root is handed in by whoever installed the schedule, and why anything written into a command line here is already literal.
|
||||
|
||||
## 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:
|
||||
Every tool below is addressed through `{CURRENT}/{plugin}`, with `{CURRENT}` standing for the literal path step 0 took:
|
||||
|
||||
| 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 status event stream | `$JSC_HOME/current/jsc-hooks/tools/report-status.sh` |
|
||||
| the wiki, through `jsc-gitea:wiki` | `$JSC_HOME/current/jsc-gitea/tools/gitea.sh` |
|
||||
| the `MONITOR_CONTENTS` entry | `$JSC_HOME/current/jsc-gitea/tools/wiki-contents.sh` |
|
||||
| the link check every write depends on | `$JSC_HOME/current/jsc-gitea/tools/link-check.sh` |
|
||||
| one patrol round | `{CURRENT}/jsc-assist/tools/patrol.sh` |
|
||||
| the system scheduler | `{CURRENT}/jsc-assist/tools/schedule.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` |
|
||||
|
||||
**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`.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
@@ -41,9 +85,9 @@ Both scripts check this for themselves: run from anywhere outside `$JSC_HOME/cur
|
||||
|
||||
Both pages this round writes carry links, and both rules below hold for every one of them — the monitor page and the directory entry alike.
|
||||
|
||||
**Rule A — a link is always written as `[{text}]({URL})`.** The wiki's own `[[page]]` and `[[text|page]]` forms are not used here at all, and neither is the split between "same repo" and "cross repo" writing. The URL comes from `$JSC_HOME/current/jsc-gitea/tools/gitea.sh wiki-url {repo} {page}`; never assemble a path by hand. `[[...]]` resolves only inside the wiki it sits in: the monitor page and the directory page live in two different repos, so a `[[MONITOR_{HASH}]]` written into the directory entry renders as an ordinary-looking link that goes nowhere, and nothing reports it.
|
||||
**Rule A — a link is always written as `[{text}]({URL})`.** The wiki's own `[[page]]` and `[[text|page]]` forms are not used here at all, and neither is the split between "same repo" and "cross repo" writing. The URL comes from `{CURRENT}/jsc-gitea/tools/gitea.sh wiki-url {repo} {page}`; never assemble a path by hand. `[[...]]` resolves only inside the wiki it sits in: the monitor page and the directory page live in two different repos, so a `[[MONITOR_{HASH}]]` written into the directory entry renders as an ordinary-looking link that goes nowhere, and nothing reports it.
|
||||
|
||||
**Rule B — a link is verified before it is written, never after.** Collect every link that is about to go into the page, hand the whole set to `$JSC_HOME/current/jsc-gitea/tools/link-check.sh`, and write only on exit 0. The script prints one `{OK|DEAD|SKIP}<TAB>{URL}<TAB>{note}` line per URL and checks Gitea URLs through the API, never through the web status code — a private repo answers 404 to a logged-out web request, so a status-code check condemns live pages.
|
||||
**Rule B — a link is verified before it is written, never after.** Collect every link that is about to go into the page, hand the whole set to `{CURRENT}/jsc-gitea/tools/link-check.sh`, and write only on exit 0. The script prints one `{OK|DEAD|SKIP}<TAB>{URL}<TAB>{note}` line per URL and checks Gitea URLs through the API, never through the web status code — a private repo answers 404 to a logged-out web request, so a status-code check condemns live pages.
|
||||
|
||||
| Exit | Meaning | Do |
|
||||
| --- | --- | --- |
|
||||
@@ -63,7 +107,7 @@ Run exactly one operation per invocation. Take it from the request: starting, la
|
||||
|
||||
The last thing any of the four operations does, after its report is printed, is write its own end event:
|
||||
|
||||
`$JSC_HOME/current/jsc-hooks/tools/report-status.sh skill-end jsc-assist:assistant {status} {exit} "{one line}"`
|
||||
`{CURRENT}/jsc-hooks/tools/report-status.sh skill-end jsc-assist:assistant {status} {exit} "{one line}"`
|
||||
|
||||
The `start` half is already on record — a hook writes it when this skill loads — so this call is what tells the difference between an operation that finished and one that stopped half way. **Skipping it makes this skill's own run look aborted**, and the next patrol round reports it as such, on the page this skill writes. Pick the status from what actually happened:
|
||||
|
||||
@@ -110,7 +154,7 @@ Every call in every operation below is judged by this table. Report the code you
|
||||
|
||||
## 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`:
|
||||
Nothing in a background assistant runs on its own. The system scheduler is what makes it periodic, and `{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` |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -130,11 +174,12 @@ Four properties of that script matter enough to state here, because a report tha
|
||||
- **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*` — `JSC_WIKI_REPO`, `JSC_WIKI_REPO_MONITOR` for the monitor page and `JSC_WIKI_REPO_CONTENTS` for the directory page, which the script picks up from the environment rather than from a hardcoded list), 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.
|
||||
- **`install` prints the permission rules that round needs.** One `allow_rule=` line each, every path a full literal under `current` — no variable, no tilde, and no wildcard inside the path, because a rule holding one matches nothing. 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.
|
||||
- **`install` writes the tool root into the entry.** The round it schedules cannot resolve the root for itself, so `schedule.sh` puts the literal path into the entry's prompt as `工具根目錄={path}` and prints the same value as `patrol_root=`. Report that value, and treat any hand-edit of the entry that drops it as breaking every future round: from then on each one stops at step 0 with nothing recorded.
|
||||
|
||||
### 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:
|
||||
The heartbeat is written in exactly one place: `{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.
|
||||
@@ -148,17 +193,17 @@ The failure this design buys is the one worth having: a round that cannot read i
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| 2 | `{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 |
|
||||
| 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, that CLI's executable is not on `PATH` so no absolute path can be written, or `JSC_HOME` does not resolve to an absolute path so no literal root can go into the entry | 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 |
|
||||
|
||||
## The status event stream — the fifth source
|
||||
|
||||
`$JSC_HOME/usage/events.jsonl` is where every skill and every hook records how its run ended. A hook writes a skill's `start` for free; the matching `end` can only be written by the skill itself, in its own closing step. **So a `start` with no matching `end` is an aborted run, and it is the only evidence of one that exists anywhere.** That is what this source is for; the counts around it are secondary.
|
||||
|
||||
`$JSC_HOME/current/jsc-assist/tools/patrol.sh collect` owns the whole of it — it calls `$JSC_HOME/current/jsc-hooks/tools/report-status.sh drain`, then `rotate`, then does the pairing, and writes the 執行狀態事件 subsection into `latest_file`. **Never run `drain` from this skill.** Four properties make that the only safe arrangement, and each one is a way to lose events:
|
||||
`{CURRENT}/jsc-assist/tools/patrol.sh collect` owns the whole of it — it calls `{CURRENT}/jsc-hooks/tools/report-status.sh drain`, then `rotate`, then does the pairing, and writes the 執行狀態事件 subsection into `latest_file`. **Never run `drain` from this skill.** Four properties make that the only safe arrangement, and each one is a way to lose events:
|
||||
|
||||
- **`drain` is a consuming read.** It prints everything written since the last drain and then moves the offset in `$JSC_HOME/usage/scan-state/events.offset`. The same events never come back. Read into a transcript instead of a file, they are one dropped line away from gone; a second `drain` in the same round returns exit 3 and the first drain's events are already spent.
|
||||
- **Exit 3 means there were no new events, and that is a normal round, not a failure.** Most rounds have nothing new. The script also uses 3 when the stream file does not exist yet.
|
||||
@@ -175,7 +220,7 @@ One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3
|
||||
| --- | --- | --- |
|
||||
| 0 | `collect`: all five 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 |
|
||||
| 2 | `finish`: `{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 `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {id}` to release the lock |
|
||||
| 3 | `collect`: all five items failed | **Write the page anyway**, with verdict 異常. A page listing five 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 |
|
||||
@@ -186,7 +231,7 @@ One table for all three subcommands. Read `collect`'s codes carefully: **1 and 3
|
||||
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 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.
|
||||
- **`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`.
|
||||
@@ -197,7 +242,7 @@ An assistant that is killed, crashes, or dies with the machine writes no farewel
|
||||
|
||||
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.
|
||||
That property holds only while nothing fakes a heartbeat. **`write` is called by `{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
|
||||
|
||||
@@ -205,11 +250,11 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
| Step 3 | Notice |
|
||||
| --- | --- |
|
||||
@@ -217,17 +262,17 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
|
||||
| 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.
|
||||
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.
|
||||
|
||||
## patrol
|
||||
|
||||
One round: read five 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.
|
||||
1. **Collect.** Run `{CURRENT}/jsc-assist/tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). That is the same split step 0 branched on: 排程 is the unattended round that read its root out of the invocation text, 手動 the round somebody asked for. 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.
|
||||
|
||||
**The status event lines come out of the same call.** `collect` drained the stream and rotated it (see 「The status event stream」 above), so record `events_total=`, `events_bad=`, `events_unpaired=`, `events_running=`, `events_rotated=` and `events_file=` alongside the rest, and read `item=D-11` for whether that source was readable at all. The 執行狀態事件 subsection of `latest_file` already carries the two detail tables — the non-`ok` events and the starts with no matching end — so never rebuild either by hand and never call `report-status.sh` yourself: a second `drain` this round would either return exit 3 or eat events that then reach no page at all.
|
||||
|
||||
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.
|
||||
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 `{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, and never work the hash out by hand** — a hand-made name lands the content on a page nobody reads. `hash-id` hashes `{host}/{user}` and prints the full 40-character uppercase hexadecimal SHA-1: no truncation to 8, no `H` prefix, and an empty input exits 2 rather than hashing the empty string. So `page=` is either `MONITOR_` plus that 40-character string, exactly as the script printed it, or nothing at all. Completion condition: `page=` holds a `MONITOR_{HASH}` name taken verbatim from `collect`, or the abort ran and the round was reported as unrecorded.
|
||||
|
||||
@@ -239,19 +284,19 @@ One round: read five sources, record the result, then beat. Everything before th
|
||||
| 最新一輪 | 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 |
|
||||
|
||||
**Verify the page's links before the write.** List every link the rebuilt body carries — the ones the latest-round block brought in, and any that survived in the block carried over from the old page — and run `$JSC_HOME/current/jsc-gitea/tools/link-check.sh` over the whole list. Exit 0 is the only result that permits the write. On exit 1 report the `DEAD` lines verbatim, then run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}` and stop: a round that writes a dead link records a false trail nobody can follow back. Exits 2, 3 and 7 take the same abort, each reported by the rule B table above. A body carrying no link at all needs no call — say so in the report rather than claiming a check that never ran.
|
||||
**Verify the page's links before the write.** List every link the rebuilt body carries — the ones the latest-round block brought in, and any that survived in the block carried over from the old page — and run `{CURRENT}/jsc-gitea/tools/link-check.sh` over the whole list. Exit 0 is the only result that permits the write. On exit 1 report the `DEAD` lines verbatim, then run `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}` and stop: a round that writes a dead link records a false trail nobody can follow back. Exits 2, 3 and 7 take the same abort, each reported by the rule B table above. A body carrying no link at all needs no call — say so in the report rather than claiming a check that never ran.
|
||||
|
||||
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**, and that verdict belongs to this step alone: the round's result lives on this page, so a repo this step cannot resolve leaves the round with nowhere to be recorded. Step 4 is judged on its own terms. Completion condition: `link-check.sh` exited 0 over the body's links or the body carried none, 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.
|
||||
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 `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. **No record, no heartbeat**, and that verdict belongs to this step alone: the round's result lives on this page, so a repo this step cannot resolve leaves the round with nowhere to be recorded. Step 4 is judged on its own terms. Completion condition: `link-check.sh` exited 0 over the body's links or the body carried none, 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 block in `MONITOR_CONTENTS`, through `jsc-gitea/tools/wiki-contents.sh`.** That page is a directory every machine writes to, and it lives in the repo `gitea.sh wiki-repo CONTENTS` resolves — `JSC_WIKI_REPO_CONTENTS`, then `JSC_WIKI_REPO`, then exit 3, and never a fallback to `JSC_WIKI_REPO_MONITOR`. The page carries no table: it is an H1, a `>` preamble, and then one H2 block per machine — the heading is that machine's monitor page name, and the fields are one `- {name}:{value}` bullet each underneath. The script owns the read-match-write of one block, so never read this page and rebuild it by hand, never write it through `jsc-gitea:wiki`, and never rebuild it the way step 3 rebuilds the content page — every other block here belongs to a machine that is not this one, and one careless whole-page write deletes their records.
|
||||
|
||||
**Finish the block first.** `contents_file` holds this machine's whole block — `## MONITOR_{HASH}`, a blank line, then the bullets — and its 監控頁 bullet already carries the rule A shape `[{page name}]({URL})` with the placeholder `{監控頁絕對網址}` standing in for the URL, because the absolute URL cannot be known until step 3 has actually put the page. Run `$JSC_HOME/current/jsc-gitea/tools/gitea.sh wiki-url {the MONITOR repo step 3 resolved} MONITOR_{HASH}`, replace the placeholder with what it prints, and write the finished block to a file. Exit 4 there means step 3's write has not landed — go back to step 3 rather than writing a block. Exit 5 means the page carries no `html_url`: report it and never assemble a URL by hand. Exit 7 or 8: report the code and take the abort row below. **Any other non-zero exit takes the same abort row**, a missing argument included — a URL that never arrived would otherwise leave that bullet holding the raw placeholder, and the block would still be written.
|
||||
**Finish the block first.** `contents_file` holds this machine's whole block — `## MONITOR_{HASH}`, a blank line, then the bullets — and its 監控頁 bullet already carries the rule A shape `[{page name}]({URL})` with the placeholder `{監控頁絕對網址}` standing in for the URL, because the absolute URL cannot be known until step 3 has actually put the page. Run `{CURRENT}/jsc-gitea/tools/gitea.sh wiki-url {the MONITOR repo step 3 resolved} MONITOR_{HASH}`, replace the placeholder with what it prints, and write the finished block to a file. Exit 4 there means step 3's write has not landed — go back to step 3 rather than writing a block. Exit 5 means the page carries no `html_url`: report it and never assemble a URL by hand. Exit 7 or 8: report the code and take the abort row below. **Any other non-zero exit takes the same abort row**, a missing argument included — a URL that never arrived would otherwise leave that bullet holding the raw placeholder, and the block would still be written.
|
||||
|
||||
**Then verify that URL before the block goes anywhere.** Run `$JSC_HOME/current/jsc-gitea/tools/link-check.sh {the URL just substituted}` and read the exit code by the rule B table above. Exit 0 is the only result that permits the upsert. On exit 1 the directory would gain a block pointing at a page that is not there: report the `DEAD` line verbatim, write no block, and treat the directory entry as not updated — the round's own result is already on `MONITOR_{HASH}`, so carry on to step 5 and write the heartbeat, exactly as exit 3 from the upsert does, and put the dead link into the 待人處理 rows. Exits 2, 3 and 7 are reported the same way and the block is left unwritten. Never write the block first and check afterwards: the directory is what other people read to find this machine, and a dead link there sends every one of them to a page that does not exist.
|
||||
**Then verify that URL before the block goes anywhere.** Run `{CURRENT}/jsc-gitea/tools/link-check.sh {the URL just substituted}` and read the exit code by the rule B table above. Exit 0 is the only result that permits the upsert. On exit 1 the directory would gain a block pointing at a page that is not there: report the `DEAD` line verbatim, write no block, and treat the directory entry as not updated — the round's own result is already on `MONITOR_{HASH}`, so carry on to step 5 and write the heartbeat, exactly as exit 3 from the upsert does, and put the dead link into the 待人處理 rows. Exits 2, 3 and 7 are reported the same way and the block is left unwritten. Never write the block first and check afterwards: the directory is what other people read to find this machine, and a dead link there sends every one of them to a page that does not exist.
|
||||
|
||||
Then run, with the template as the fifth argument every time:
|
||||
|
||||
`$JSC_HOME/current/jsc-gitea/tools/wiki-contents.sh upsert MONITOR 1 "MONITOR_{HASH}" {block file} $JSC_HOME/current/jsc-assist/templates/monitor-contents.md`
|
||||
`{CURRENT}/jsc-gitea/tools/wiki-contents.sh upsert MONITOR 1 "MONITOR_{HASH}" {block file} {CURRENT}/jsc-assist/templates/monitor-contents.md`
|
||||
|
||||
**The key is the H2 heading — the page name `MONITOR_{HASH}`**, taken from `collect`'s `page=` line verbatim, with no link, no brackets and no URL around it. The script compares the heading text, so the 監控頁 bullet cannot be the key: it holds `GITEA_HOST` and the wiki's encoding of the page name, so a changed host, a `JSC_WIKI_REPO_MONITOR` pointed at another repo, or a different URL encoding changes that text and stops it matching. This page is written once every round, so from the moment matching breaks every round appends one more block for this same machine and the old block is never updated again. The page name depends on `{host}/{user}` alone, which none of those three touch. That bullet's link stays in the block for people to click, and never for matching. A key typed by hand matches nothing either, and appends the same duplicate block.
|
||||
|
||||
@@ -260,7 +305,7 @@ One round: read five sources, record the result, then beat. Everything before th
|
||||
| Exit | Do |
|
||||
| --- | --- |
|
||||
| 0 | The block is in place. The script prints `updated` or `added` plus the page it wrote — carry that word into the report, and carry on to step 5 |
|
||||
| 1 | The page content could not be built, or the write failed. Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. A page with no matching block is not this code: an unmatched key is an append |
|
||||
| 1 | The page content could not be built, or the write failed. Run `{CURRENT}/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop. A page with no matching block is not this code: an unmatched key is an append |
|
||||
| 2 | An argument was rejected and nothing was written. A template path that does not exist lands here too, and means the plugin installation is incomplete. Correct the call and run it once more; report a second exit 2 as a defect in this skill, then abort and stop |
|
||||
| 3 | No `CONTENTS` wiki repo is configured. **This one does not stop the round.** Carry on to step 5 and write the heartbeat: the round's result is already on `MONITOR_{HASH}`, and that is exactly what a heartbeat stands for. Report the directory entry as not updated, name `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set, and add that to the 待人處理 rows. Never abort a recorded round over the directory page — a missing directory block loses one index entry, an aborted round loses the whole round, and the patrol cannot ask anybody for the missing setting |
|
||||
| 4 | The page is absent and no template reached the script. The call above always passes the template as its fifth argument, so this code cannot come out of it — getting it means that argument was dropped, so restore it and run the call once more. A template path that does not exist is rejected as exit 2, never as 4 |
|
||||
@@ -269,7 +314,7 @@ One round: read five sources, record the result, then beat. Everything before th
|
||||
|
||||
Completion condition: `link-check.sh` exited 0 over the block's URL and the script exited 0 with exactly one `## MONITOR_{HASH}` block on the page carrying this round's values, or exit 3 from the upsert or a non-zero `link-check.sh` was reported as an unwritten directory entry and the round carried on, or one of the other non-zero codes — `wiki-url`'s included — was reported after the abort ran.
|
||||
|
||||
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.
|
||||
5. **Write the heartbeat.** Run `{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 five 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, the link-check verdict for each of the two writes — passed, skipped for a body with no link, or refused with its exit code and its `DEAD` lines — and the directory entry as `updated`, `added`, or not written with the exit code and the reason; 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.
|
||||
|
||||
@@ -281,7 +326,7 @@ One round: read five sources, record the result, then beat. Everything before th
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -294,7 +339,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
||||
|
||||
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.
|
||||
3. **Read the schedule.** Run `{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.
|
||||
|
||||
@@ -315,11 +360,11 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
|
||||
|
||||
## 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.
|
||||
1. **Record what is being stopped.** Run `{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.
|
||||
2. **Remove the schedule first.** Run `{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.
|
||||
3. **Clear the heartbeat.** Run `{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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user