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 row through jsc-gitea/tools/wiki-contents.sh, which reads the separate CONTENTS wiki repo and links the monitor 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. 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
@@ -26,10 +26,10 @@ Every tool below is addressed through `$JSC_HOME/current/{plugin}`, and `$JSC_HO
| 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`row | `$JSC_HOME/current/jsc-gitea/tools/wiki-contents.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` |
**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 row 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.
**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`.
@@ -39,9 +39,9 @@ Both scripts check this for themselves: run from anywhere outside `$JSC_HOME/cur
## Two rules bind every link this skill writes
Both pages this round writes carry links, and both rules below hold for every one of them — the monitor page and the directory row alike.
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 row 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 `$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 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.
@@ -72,7 +72,7 @@ The `start` half is already on record — a hook writes it when this skill loads
| `ok` | the operation reached its own completion condition |
| `blocked` | the round stood down because another round holds the lock, or `install heartbeat` was refused — nothing was done and nothing is wrong |
| `failed` | a step returned a code that stopped the operation: `collect` exit 5 or 6, a monitor-page write that could not be made, `remove` non-zero in `stop` |
| `degraded` | the operation finished with a known gap: the directory row was left unwritten, or the schedule entry was installed but the cron service is stopped |
| `degraded` | the operation finished with a known gap: the directory entry was left unwritten, or the schedule entry was installed but the cron service is stopped |
| `aborted` | the operation stopped because a precondition did not hold, such as an empty `hash=` or a missing `current` link |
The call never changes the outcome: it returns 0 even when it cannot write, and a non-zero from it is reported as a defect in the reporting chain, never as a failure of the operation that just succeeded. Completion condition for all four operations: exactly one `skill-end` was written, and its status matches the outcome that was reported.
@@ -85,7 +85,7 @@ The call never changes the outcome: it returns 0 even when it cannot write, and
| `$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/patrol.lock/` | `patrol.sh` only | the round lock, a directory. `info` holds `round`, `pid`, `started` |
| `$JSC_HOME/assistant/patrol/` | `patrol.sh` only | one round's scratch files, including `latest.md`, `summary.md`, `summary-row.md`, `newpage.md` and `contents.tsv` |
| `$JSC_HOME/assistant/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 |
| `$JSC_HOME/usage/events.jsonl` | `report-status.sh` only, never this skill and never `patrol.sh` by hand | one JSON object per line: `ts`, `cli`, `session`, `kind`, `name`, `phase`, `status`, `exit`, optional `ms` and `detail` |
| `$JSC_HOME/assistant/events-open.tsv` | `patrol.sh` only | the starts still waiting for a matching end, carried from round to round: `session`, `name`, `kind`, first-seen epoch, the event's own `ts` |
@@ -187,7 +187,7 @@ The six limits in `AGENTS.md`「助理的界線」 hold for all four operations.
- **This skill never judges a gate.** It maintains the heartbeat and prints what the heartbeat says. Whether a stale heartbeat blocks a skill call is decided by a hook, synchronously and offline; nothing in this skill blocks or waves through anything. 界線 2.
- **A patrol round asks nothing.** It runs from cron with nobody present, so there is no one to answer and a question hangs the round. Every branch in the patrol steps below resolves without a question: a missing source is recorded as missing, an ambiguous result is recorded verbatim, and a round that cannot proceed aborts and reports. Never call `jsc-ask:ask` from `patrol`. 界線 1.
- **A patrol round rewrites the monitor page as three fixed blocks.** Read the old page back first; keep 本頁基本資料 as it stands, replace 最新一輪 whole, put this round's row on top of the summary table and cut it to 24; then put the whole page. The directory page is a separate write in a separate wiki repo, and `wiki-contents.sh` does it: this machine's row is updated and nobody else's. A page that could not be read is a page that does not get written — the summary table only survives if the old one came back. 界線 4.
- **A patrol round 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`.
@@ -203,7 +203,7 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
`start` proves the loop works before it schedules it: one patrol round first, then the scheduled entry. It installs no daemon and writes no bare heartbeat.
1.**Run one patrol round.** Follow every step of the `patrol` operation below, start to finish. This is what writes the first heartbeat — there is no shortcut past it, because a heartbeat that no round produced is exactly the lie this design removes. When that round ends without a heartbeat for any reason (`collect` exit 4, 5 or 6, an empty `hash=`, a failed write of the monitor page, a directory-row 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.**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.
@@ -243,33 +243,35 @@ One round: read five sources, record the result, then beat. Everything before th
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.
4.**Update this machine's row 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 script owns the read-match-write of one row, 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 row here belongs to a machine that is not this one, and one careless whole-page write deletes their records.
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 row first.**The `row=` line in `contents_file` already carries the rule A shape `[{page name}]({URL})` in its first cell, 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 row to a file. Exit 4 there means step 3's write has not landed — go back to step 3 rather than writing a row. 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 the link cell holding the raw placeholder, and the row 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 `$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.
**Then verify that URL before the row 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 row pointing at a page that is not there: report the `DEAD` line verbatim, write no row, and treat the directory row 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 row is left unwritten. Never write the row first and check afterwards: the directory is what other people read to find this machine, and a dead row there sends every one of them to a page that does not exist.
**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 run, with the template as the fifth argument every time:
**The key is column 2, the bare `HASH` cell** — the 40-character string `collect` printed as `hash=`, copied verbatim, with no link, no brackets and no URL around it. The script compares the whole cell text, so column 1 cannot be the key: that cell 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 the text and stops it matching. This page is written once every round, so from that moment on every round appends one more row for this same machine and the old row is never updated again. The bare `HASH` depends on `{host}/{user}` alone, which none of those three touch. Column 1's link stays in the row for people to click, and never for matching. A key typed by hand matches nothing either, and appends the same duplicate row.
**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.
**The `1` is the third argument, `key-col`, and it only matters while an old page is still a table.** A directory page written in the previous format holds a markdown table, and the script converts the whole page to blocks before it upserts; `key-col` tells it which column of that table held the identity, counting from 1. Column 1 of this page's old table was the monitor-page link, whose cell is `[MONITOR_{HASH}]({URL})`, and the conversion takes the text out of it as the H2 heading. Once the page is in the block format the argument is ignored — pass `1` regardless, and never a column number worked out from the current page.
| Exit | Do |
| --- | --- |
| 0 | The row 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 write failed, or the directory page holds no markdown table. Run `$JSC_HOME/current/jsc-assist/tools/patrol.sh abort --round {round}`, report the code, and stop |
| 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 |
| 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 row 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 row loses one index line, an aborted round loses the whole round, and the patrol cannot ask anybody for the missing setting |
| 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 |
| 7 | The token is invalid or lacks permission, so the other machines' rows are unknown. The script wrote nothing, which is what keeps those rows alive. Abort, report the key problem, and stop |
| 7 | The token is invalid or lacks permission, so the other machines' blocks are unknown. The script wrote nothing, which is what keeps those blocks alive. Abort, report the key problem, and stop |
| 8 | Some other API failure. Abort, report the status, and stop |
Completion condition: `link-check.sh` exited 0 over the row's URL and the script exited 0 with exactly one row carrying this machine's bare `HASH`in column 2 with this round's values, or exit 3 from the upsert or a non-zero `link-check.sh` was reported as an unwritten directory row and the round carried on, or one of the other non-zero codes — `wiki-url`'s included — was reported after the abort ran.
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.
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 row 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.
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.
**The event numbers get their own line, and the unpaired starts get their own list.** Print `events_total=` and `events_bad=` as this round's event count and its non-`ok` count, then every non-`ok` event with its `kind`, `name`, `status`, `exit` and `detail`, then — separately, never folded into the same list — every start with no matching end, by `name` and `session`. A non-zero `events_unpaired=` is the round's most important finding: each row is a skill run that started and never reached its closing step. Say `events_rotated=` too when it is `rotated` or `failed`. When `item=D-11` failed, say the source could not be read rather than reporting zero events — zero read events and zero existing events look identical in a report and mean opposite things.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.